Salesforce CLI Automation
This skill activates when the task is to design, refactor, or troubleshoot automation that drives Salesforce through the CLI—not when the primary question is “which CI YAML product to use,” but when the core problem is how sf behaves under scripts, how to get stable machine-readable output, and how to avoid the sharp edges of async commands, org selection, and deprecated sfdx syntax. Official behavior, flags, and command families are defined in the Salesforce CLI Reference; project and auth models are covered in the Salesforce DX Developer Guide; metadata semantics that the CLI ultimately invokes belong in the Metadata API Developer Guide.
The Salesforce CLI v2 exposes capabilities under the sf executable. Legacy sfdx force:* style commands remain for compatibility in many environments, but new investment and features ship in sf, and automation should standardize on sf so scripts do not break when legacy paths narrow. Treat the CLI as a contract with stdout/stderr and exit codes: in CI, prefer --json on commands that support it when you parse results, and pair long-running operations with --wait or explicit async job polling (--async plus follow-up commands) so pipelines do not finish “green” while work is still running server-side.
Org targeting should always be explicit in shared automation: use --target-org <alias> (or the supported environment variables your runner sets) so a developer laptop default alias cannot silently change production. Plugins extend sf; pin versions when a script depends on a plugin command, because plugin updates can change flags or output shape. For data at scale, favor the families documented under data commands (sf data tree vs bulk paths per the CLI reference) instead of ad-hoc REST scripts that duplicate CLI-tested flows.
This skill complements platform-specific CI skills (which show where to store secrets and how to structure jobs) and interactive CLI essentials (day-to-day auth and scratch org creation). Here the focus stays on portable CLI automation patterns any environment can reuse.
Before Starting
Gather this context before working on anything in this domain:
- Executable and version: Is the runner using global
sffrom the Salesforce CLI installers or a pinned npm@salesforce/cli? Mismatched versions explain flag drift between laptop and CI. - Auth model: Interactive
sf org login webcannot complete in headless automation—CI needs JWT or a pre-provisioned auth file on the runner. Know which org alias each step must target. - Output contract: Does a downstream step parse JSON with
jq, archive JUnit, or only need a pass/fail exit code? That determines--json,--result-format, and whether you must scrape job IDs from async responses. - Most common wrong assumption: That a zero exit code always means “deploy/tests fully succeeded” without
--waitor without reading JSON status fields—many flows enqueue work and return before completion unless configured otherwise. - Limits: Platform test timeouts, concurrent deployment limits, and data API bulk thresholds still apply; the CLI surfaces errors, but automation must retry and backoff responsibly.
Core Concepts
Stable, parseable CLI output
Human tables and spinners are convenient locally but brittle in logs. For automation, prefer --json where the command supports it so your script inspects structured status, result, and error arrays instead of regexing prose. When a command offers --result-format (for example Apex tests), pick json or junit to match the consumer. Keep SF_USE_PROGRESS_BAR=false (or equivalent environment conventions your org standardizes) so progress rendering does not corrupt captured output in CI logs.
Synchronous completion vs async job IDs
Deploy and test commands can return before server-side work completes unless you set --wait with an adequate timeout or deliberately run --async and poll with the companion “report” or “resume” style commands documented in the CLI reference for that command family. Scripts that omit waiting are a frequent source of flaky pipelines that merge broken metadata because validation had not finished.
Org aliases, default orgs, and CI isolation
Automation must not rely on whatever sf config get target-org returns on a shared runner. Pass --target-org on every mutating command, derive dynamic values (instance URL, org id) with sf org display --json when needed, and avoid storing long-lived refresh tokens in repo files—use the secret mechanism of the hosting platform, surfaced to the process environment for sf org login jwt or similar non-interactive flows described in the Salesforce DX Developer Guide.
Common Patterns
JSON gate wrapper
When to use: A shell script must fail the build if a deploy or test command reports failure in JSON even when stderr looks clean.
How it works: Run sf <command> ... --json, capture stdout, parse with python3 -c or jq to check top-level status (and nested result.success where applicable), and exit non-zero on failure. Keep CLI stderr visible for supportability.
Why not the alternative: Grepping for the word Error in human output breaks on localized CLI messages and minor formatting changes.
Replace legacy sfdx force:* in scripts
When to use: Maintenance on older bash or npm scripts still calling sfdx force:source:deploy or similar.
How it works: Map to sf project deploy start, sf project retrieve start, sf apex run test, and other unified topics listed in the CLI reference migration sections. Install only the modern CLI toolchain on runners to avoid conflicting executables.
Why not the alternative: Keeping sfdx indefinitely stores technical debt and duplicates auth/config behavior between two entry points.
The sf data Command Families
Automation that moves records instead of metadata lives under the sf data topic. Picking the wrong family is a common footgun: single-record commands do not scale to volume, and bulk commands are overkill (and slower to spin up a job) for one row. The topic breaks into four families, all defined in the sf data command reference.
| Family | Commands | Shape | When to use |
|---|---|---|---|
| Single-record CRUD | sf data create record, sf data get record, sf data update record, sf data delete record, sf data create file |
One record (or one file upload, e.g. a ContentVersion) against a Salesforce or Tooling API object |
Smoke-test steps, one-off fixups, uploading a single asset—not loops over many rows |
| Ad-hoc query / search | sf data query (SOQL), sf data search (SOSL) |
Read-only; pairs with --json to feed downstream steps |
Deriving IDs, counts, or gate conditions inside a script |
| Tree JSON import/export | sf data export tree, sf data import tree |
One or more JSON files carrying related records | Seeding a scratch org or dev sandbox with a small, relationship-aware fixture set |
| Bulk API 2.0 (CSV) | sf data export bulk, sf data import bulk, sf data update bulk, sf data upsert bulk, sf data delete bulk |
CSV files in and out; each verb runs a Bulk API 2.0 job | Volume data movement between orgs where record-by-record REST would blow limits |
Bulk jobs are asynchronous by contract. Every bulk verb has a matching ... resume command (sf data import resume, sf data upsert resume, and so on) that reattaches to a job you already started by its job ID, so a script can start a load, exit the step, and poll status later. sf data resume reports the status of a running bulk job or batch, and sf data bulk results retrieves the success/failure detail of an ingest job you ran earlier. This is the same async discipline the rest of this skill applies to deploys and tests: either wait long enough for the job to finish or capture the job ID from --json and poll with the companion resume/results command before asserting success. Do not treat a returned prompt as a completed load.
Legacy note: the predecessor sfdx force:data:* family is deprecated in favor of this unified sf data topic (deprecated force:data reference). Treat any force:data:tree:import or force:data:bulk:upsert you find in older scripts the same way you treat force:source:deploy—map it to the sf data equivalent.
Decision Guidance
| Situation | Recommended Approach | Reason |
|---|---|---|
| Need structured output for tooling | Add --json (and parse explicitly) |
Stable schema-oriented integration per CLI reference |
| Long deploy or full test suite in CI | Use --wait with a timeout above peak runtime |
Prevents returning before completion |
| Very large data movement between orgs | Use the sf data ... bulk verbs (Bulk API 2.0, CSV) and poll with resume/bulk results |
Designed for volume instead of record-by-record scripts |
| Seeding a scratch org with related fixture records | Use sf data export tree / sf data import tree (JSON) |
Preserves relationships in a small, versionable fixture set |
| A single record or file to create/read/fix | Use sf data create/get/update/delete record or sf data create file |
Avoids spinning up a bulk job for one row |
| Developer-only convenience script | Human output acceptable; still pin --target-org |
Reduces accidental cross-org execution |
Recommended Workflow
- Confirm the automation environment:
sfversion, plugins, headless vs interactive, and which org aliases exist on the runner. - Identify the operation family (project deploy, Apex test, data, org login) in the Salesforce CLI Reference and list required flags:
--target-org,--jsonor--result-format, and wait/async behavior. - Rewrite or validate commands to avoid legacy
sfdx force:*namespaces unless a documented exception applies. - Add explicit completion handling (
--waitor async polling) and assert success using structured output, not log scraping. - Run the skill’s
check_salesforce_cli_automation.pyagainst the repository root to catch common automation footguns in CI and shell files. - Document the final command snippets and environment variables for operators who will extend the script later.
Review Checklist
Run through these before marking work in this area complete:
- Every automated command specifies
--target-org(or equivalent explicit targeting). - Machine-readable output format matches what parsers expect (
--json, JUnit, etc.). - Long-running commands either wait or implement async polling with timeouts.
- No interactive
sf org login websteps remain in headless paths. - Legacy
sfdx force:usage is removed or tracked as debt with a migration issue. - Secrets are injected via the host platform, not committed keys or tokens in scripts.
Salesforce-Specific Gotchas
- Silent async returns — Some commands enqueue work and can return before the org finishes processing unless
--waitis set appropriately; pipelines may pass while deployments or tests are still running or failing afterward. - Default org leakage — Omitting
--target-orgon shared runners can apply metadata to whatever alias happens to be authorized last, which is a severe reliability and security risk. - Output format drift — Parsing human-formatted tables breaks across CLI versions;
--jsonfields are the supported integration surface where available.
Output Artifacts
| Artifact | Description |
|---|---|
sf command snippets |
Copy-pasteable commands with automation-oriented flags for scripts and jobs |
| JSON parsing notes | Which top-level keys to assert after --json for the command families in use |
| Migration notes | Legacy sfdx → sf mapping relevant to the scripts under review |
Related Skills
apex/sf-cli-and-sfdx-essentials— interactive CLI setup, scratch org basics, and everydaysfcommands when automation is not the primary concerndevops/continuous-integration-testing— Apex test levels, coverage gates, and CI-specific testing patternsdevops/github-actions-for-salesforce(or GitLab / Bitbucket counterparts) — platform YAML, secrets, and runner configuration when the deliverable is a full pipeline file