forcedotcom/sf-skills12 files

Automation Sandbox Post Copy Configure

Apply a Salesforce sandbox post-copy automation JSON config against a target org. For each entry, the skill derives the correct Tooling API sobject from the entry's `ConfigurationName`, verifies the derivation via a describe probe, resolves the record Id via SOQL-over-REST, then PATCHes the record via the compound `Metadata` field using `sf api request rest`. Use when the user asks to apply, run, execute, dry-run, or preview a post-copy or post-refresh config file (e.g. `post-copy-config.json`) against a sandbox. Trigger phrases: \"apply post-copy config\", \"run post-copy automation\", \"execute sandbox post-refresh JSON\", \"apply sandbox refresh config\", \"configure sandbox after refresh\". DO NOT TRIGGER for generating the config JSON from an SOP (delegate to `automation-sandbox-post-copy-config-generate`), or for deploying metadata XML.

Specification
Skill ID
forcedotcom/sf-skills/automation-sandbox-post-copy-configure
Publisher
forcedotcom
Repository
sf-skills
Installs
0
Files
12
Synced
Sep 23, 2026
How to use it

Open any RiverX project, open the Skills panel in the chat, and search for this identifier. The files are fetched from the source repository at install time.

forcedotcom/sf-skills/automation-sandbox-post-copy-configureInstalls these files
  • SKILL.md
  • assets/api_request_templates.json
  • examples/sample_config_input.json
  • examples/sample_execution_summary.md
  • references/api_endpoints.md
  • references/authentication.md
  • references/execution_phasing.md
  • references/rules_gotchas.md
  • scripts/classify-patch-result.mjs
  • scripts/map-metadata-key.mjs
  • scripts/plan-phases.mjs
  • scripts/resolve-target-org.mjs

What this skill tells the agent

Automation: Sandbox Post-Copy Configure

Apply a Salesforce sandbox post-copy automation JSON config to a target org. The skill pins the Tooling API sobject and the record-lookup SOQL filter for the two canonical ConfigurationName values it has been calibrated against (OutboundMessages, RemoteSiteSettings); for any other ConfigurationName it derives a candidate from the entry value and verifies it against the live org's describe endpoint. Every entry — pinned or derived — must still pass Step B (describe returns 200 with a Metadata compound field) before any PATCH is planned. Entries whose API cannot be identified or verified are surfaced in the summary and skipped — they are never guessed at.

Tool Restrictions

Use ONLY the Bash tool to execute sf CLI commands (sf data query --use-tooling-api, sf api request rest, sf org display). Do NOT use MCP tools like execute_soql — ignore them completely; the compound Metadata PATCH pattern this skill requires is not available through MCP tool wrappers. If the target org alias is not explicitly named by the user, invoke sf commands without --target-org — the harness has already set the CLI's default target-org. Never pass --target-org default — default is not an alias and will fail with NamedOrgNotFoundError.

SOQL-over-REST (sf data query --use-tooling-api ...) is treated as an API call — same OAuth session, same authorization boundary as the subsequent PATCH. No direct database / non-Salesforce SQL access.

STOP — do this before making any API call

Never call the org from memory. Before the first request:

  1. Read the config JSON end-to-end from the exact path the user gave (default ./post-copy-config.json). Every entry must have all five keys (ConfigurationName, Label, Fields, IsActive, ExecutionOrder). If any entry is malformed, abort and surface the file path + entry index — do not partially apply. Do not invent entries. If the file is missing, stop and ask; never fabricate a plan against synthetic labels.
  2. For each distinct ConfigurationName in the config, run the derivation + describe-verify step (below) before planning any PATCH. An entry whose API cannot be verified must never appear as a planned PATCH — it is rejected up front.
  3. Confirm the target org alias with the user unless they supplied one explicitly. This skill mutates a live org — writing to the wrong org (e.g., a production alias set as default) is the most expensive failure mode.

If you announce "I will apply … now" without having read the config file and run the describe-verify for every distinct ConfigurationName, stop and do those first.


Per-entry procedure (the core of this skill)

The skill carries a pinned mapping for the canonical types below, and a derive-then-verify path for anything else. Follow these steps for every entry, in order. Step B (describe-verify) is mandatory regardless of whether the mapping came from the pinned table or the derive path — its HTTP status must appear in the summary.

Step A — Resolve the Tooling API sobject and lookup filter

Pinned canonicals (authoritative — use these exactly, do NOT substitute a different sobject name):

ConfigurationNameTooling API sobjectRecord-lookup SOQL
OutboundMessagesWorkflowOutboundMessageSELECT Id, FullName FROM WorkflowOutboundMessage WHERE EntityDefinition.QualifiedApiName = '<Fields.Object>' — then client-side pick the row whose FullName == '<Fields.Object>.<Label>'. SOQL cannot filter on FullName directly for this sobject.
RemoteSiteSettingsRemoteProxySELECT Id, SiteName FROM RemoteProxy WHERE SiteName = '<Label>'.

For canonical entries use the pinned sobject and SOQL as-is; skip "derive". Do not invent OutboundMessage, RemoteSiteSetting, or MasterLabel variants — plausible-looking but wrong.

Field-name resolution (overrides + case rule + existence check) is owned by scripts/map-metadata-key.mjs.

Non-canonical: derive a candidate (usually singular of the Metadata API type name, sometimes prefixed); Step B rules out wrong guesses. Use the generic SOQL patterns in Step C.

Step B — Verify the candidate exists and supports Metadata writes

For each candidate, GET the describe:

sf api request rest \
  "/services/data/v<apiVersion>/tooling/sobjects/<Candidate>/describe/"

Accept the candidate only if both are true:

  • HTTP 200 (the sobject exists on this org's Tooling API), AND
  • The describe response's fields array contains a field named Metadata (compound field — this is what PATCH writes through).

If no candidate passes both gates, mark the entry API_NOT_IDENTIFIED and skip. Do not guess a REST path — the wrong path 404s in the best case and updates the wrong record in the worst case.

Step C — Resolve the record Id (SOQL-over-REST)

For pinned canonicals: run the SOQL from the Step A table verbatim (substituting <Label> / <Fields.Object>). Do not substitute a different filter column such as MasterLabel — the pinned SOQL is the tested-and-correct filter for that sobject.

For non-canonical (derived) sobjects: query the verified sobject for the record identified by the entry's Label (and Fields.Object when present):

# SOQL to a variable + separate jq call — avoids the most common
# shell-quoting failure in this skill.
SOQL='SELECT Id, FullName FROM <VerifiedSobject> WHERE <UniqueFilter>'
sf data query --use-tooling-api --json --query "$SOQL" \
  > /tmp/entry-<Slug>-lookup.json
ID=$(jq -r '.result.records[0].Id // empty' /tmp/entry-<Slug>-lookup.json)

Pick <UniqueFilter> based on the queryable fields shown in the describe response from Step B. If the row contains a FullName field, SOQL usually rejects a direct FullName = ... filter — filter by whichever direct column the describe surfaces (e.g. EntityDefinition.QualifiedApiName, DeveloperName, SiteName) and apply the FullName match client-side (jq -r '.result.records[] | select(.FullName == "<Object>.<Label>") | .Id').

Outcomes:

  • Zero rows → NOT_FOUND (never fall back to insert).
  • Multiple rows after client-side filtering → AMBIGUOUS (surface all Ids in the summary and skip; a wrong Id is worse than no Id).
  • Exactly one row → proceed to Step D.

Step D — GET current Metadata, mutate, PATCH back

Metadata is replace-in-full — omitted keys are blanked on PATCH. Preserve every existing key from the Step D-1 GET; overlay only the mutated keys. The summary's "planned PATCH body" (apply and dry-run) is this full merged object — verbose or org-specific values may render as "<preserved-from-GET>" in dry-run, but every key must be present.

Every Step D filename must include the record <Id> from Step C (e.g. /tmp/entry-<Id>-meta.json) — phase-parallel entries share the working directory and would clobber a shared name.

  1. GET current Metadata → /tmp/entry-<Id>-meta.json.
  2. Resolve the mutation key — for each Fields.<Xxx>, run node scripts/map-metadata-key.mjs "<ConfigurationName>" "<ConfigFieldName>" /tmp/entry-<Id>-meta.json. On {"status":"OK","key":...} use the returned key. On {"status":"FIELD_MAP_UNKNOWN",...} mark the entry and skip.
  3. PATCH — heredoc-build /tmp/entry-<Id>-mutation.json, merge with jq --slurpfile m /tmp/entry-<Id>-mutation.json '. + $m[0] | {Metadata: .}' /tmp/entry-<Id>-meta.json > /tmp/entry-<Id>-patch.json (preserves JSON types), PATCH with -b @/tmp/entry-<Id>-patch.json, capture the response body to /tmp/entry-<Id>-response.json. Full bash in references/api_endpoints.md §Step D. Never use --arg (stringifies booleans/numbers, breaks on special chars). Wrap the PATCH in for attempt in 1 2; do <cmd> && break; done —

More from forcedotcom

Use this skill in a project

Open any RiverX project, search the Skills panel for forcedotcom/sf-skills/automation-sandbox-post-copy-configure, and install it in one click.

Use this skill