Validate with tests in the release pipeline, promote the validated job ID with sf project deploy quick inside its 10-day window, and keep sf project deploy start for environments where you mean to change state now.
If your team uses one deploy command for every environment, you are either too slow or too risky. sf project deploy start, sf project deploy validate and sf project deploy quick solve different problems, and the wrong choice shows up as a release that overruns its window, a quick deploy refused on release night, or a test level that passes in a sandbox and fails in production. For the flags, dry runs, test levels and error messages of start itself, see the post on sf project deploy start. This one is about which command to run.
This article expands the deploy strategy in the Salesforce CLI DevOps Playbook. Every command below was written against @salesforce/cli 2.152.14, the latest tag on npm on 6 October 2026, and checked against that version’s command reference rather than run in an org. Check yours with sf --version, because flag lists and defaults change between releases.
Why does this fail in production?
The same command behaves differently in a sandbox and in production because the defaults differ, and a validation is a dated artefact with conditions attached.
Three facts from the Salesforce CLI command reference drive most release incidents:
NoTestRunapplies only to development environments such as sandboxes, Developer Edition and trial orgs, and it is the default there.RunLocalTestsis the default for production deployments that include Apex classes or triggers. A sandbox pipeline that never sets a test level has never run your tests.- A validation’s job ID expires, and the validated deployment must itself have succeeded for the quick deploy to succeed. The constraints under quick deploy below give the limits.
--waitdefaults to 33 minutes onstart,validateandquick. When the CLI stops watching, the deployment carries on in the org without you.
What is the difference between deploy start and deploy validate?
deploy start performs a real deployment, while deploy validate checks whether the deployment would succeed and returns a job ID you can promote later without re-running tests.
Under the hood, validate is a start run as a dry run with tests made mandatory: the command’s source passes dry-run: true to the shared deploy routine and only accepts test levels that execute tests. The reference describes it as similar to deploy start, except that you are required to run Apex tests and the command returns a job ID rather than executing the deployment.
Quick comparison:
sf project deploy start: deploys metadata now.sf project deploy validate: runs the deployment with tests but saves nothing and returns the job ID thatquickpromotes.sf project deploy quick: deploys a successful validation by job ID without re-running tests.
When should I use sf project deploy validate?
Use validate for production preflight: run it when the release candidate is final, keep the job ID, and promote with quick inside the release window.
# @salesforce/cli 2.152.14
sf project deploy validate \
--source-dir force-app \
--target-org "$SF_PROD_ALIAS" \
--test-level RunLocalTests \
--wait 90 \
--json > validate.json
The reference is explicit about where this belongs: do not use the command on sandboxes, because it is intended for production orgs, and by default sandboxes do not run tests during a deploy. For a sandbox check with tests it points you to sf project deploy start --dry-run --test-level RunLocalTests instead. It also notes that validate does not support source tracking, so the source you later quick deploy overwrites the matching metadata in the org.
When should I use sf project deploy start?
Use start when you intend to change the org now: scratch orgs, developer and integration sandboxes, and approved release stages. In production it fits only when the whole test run sits inside the window and you accept that a failure rolls back at the end of it.
# @salesforce/cli 2.152.14
sf project deploy start \
--source-dir force-app \
--target-org "$SF_SANDBOX_ALIAS" \
--test-level RunLocalTests \
--wait 60
If the production test suite takes longer than your window, you want validate and quick, not a bigger --wait.
How does quick deploy fit into this workflow?
Quick deploy promotes a successful validation without re-running tests, which moves the slow part of a production release to a day of your choosing.
# @salesforce/cli 2.152.14
sf project deploy quick \
--job-id "$SF_VALIDATE_JOB_ID" \
--target-org "$SF_PROD_ALIAS" \
--wait 60
The constraints, each from the CLI reference for project deploy quick and project deploy validate:
- The job ID is valid for 10 days from when you started the validation.
- The validated deployment must have succeeded; a failed validation cannot be quick deployed.
--use-most-recentonly looks at job IDs validated in the past 3 days or less. Older than that and the flag finds nothing, even though the job ID itself is still valid.validateandquickare intended for production orgs, not sandboxes, because sandboxes do not run tests by default. The platform operation behindquickisdeployRecentValidation(), which the command’s source calls. Quick deployment itself is not limited to production. The DevOps Center help describes quick deploying a validated bundle to a pipeline stage as well as to the release environment, and it is clear that a validation run without tests cannot be quick deployed.
On what invalidates a validation before the job ID expires: Salesforce’s Trailhead unit on the org development model says the quick deploy window holds as long as you do not perform another deployment or make major changes to the org. The DevOps Center help says the same in its own terms: any subsequent promotion disqualifies an existing validation. The CLI reference does not spell out the rule. In my projects, anything else deployed to the same org between validation and quick deploy has meant validating again, so treat the job ID as single use and re-validate if the org has moved.
Best Practice: Validate early on release day rather than a week ahead. You stay inside the reach of
--use-most-recent, and fewer unrelated changes can land in between.
Which test level should I pair with these commands?
Match the test level to the org’s risk, and know which coverage rule applies, because the per-class rule and the overall rule are different and the CLI reference describes both.
From the --test-level description in the CLI reference:
NoTestRunruns nothing and applies only to development environments, where it is the default.RunSpecifiedTestsruns only the tests you name with--tests. Those tests must give each class and trigger in the deployment package at least 75% coverage individually, which the reference notes is different from the overall coverage percentage.RunLocalTestsruns every test in the org except those from installed managed and unlocked packages, and it is the default for production deployments that include Apex classes or triggers.RunAllTestsInOrgruns everything, including managed package tests.RunRelevantTests(Beta) lets Salesforce pick the tests relevant to the payload, with the same per-class 75% rule asRunSpecifiedTests.
The default production rule, from the Metadata API Developer Guide, is a minimum overall 75% across all classes and triggers, and no trigger at 0% coverage. Salesforce Help adds that the coverage restriction is not enforced for sandbox or Developer Edition orgs. So the common belief that every deployed class must hit 75% on its own under RunLocalTests is wrong: that is the RunSpecifiedTests rule.
Practical defaults:
- Scratch orgs and developer sandboxes:
NoTestRunfor speed, withstart --dry-run --test-level RunLocalTestsbefore merging. - Hotfixes to production:
RunSpecifiedTestsnaming the tests for the changed classes, accepting the per-class 75% bar. - Release candidates and production:
RunLocalTests. RunAllTestsInOrg: only when you need managed package tests in the gate. In my experience that is rare, and it hands your release window to code you do not own.
Which command should I run?
Pick the row that matches what you are trying to prove, not the org you happen to be pointed at.
Pick an environment and an objective. The command, its test level and what to watch appear here.
This tool builds commands from the guidance in this post. Check them against your own pipeline before you run them.
sf project deploy start --dry-runTest level
NoTestRun, or RunSpecifiedTests for the changed classes. From the failure modes: a dry run with NoTestRun is a test-free check, and it does not feed quick deploy.
What to watch
sf project deploy start --dry-run --test-level RunLocalTestsTest level
RunLocalTests. From the failure modes: a dry run does not feed quick deploy.
What to watch
- The test run outlasts
--wait(resume or report; set --wait from measured test duration). - validate refuses NoTestRun (a dry run does not feed quick deploy).
sf project deploy start --test-level RunLocalTestsTest level
RunLocalTests.
What to watch
- The test run outlasts
--wait(the deployment continues in the org; resume or report). - The validation fails with a generic message (InvalidProjectWorkspaceError means the command is not running inside a project).
sf project deploy validate
sf project deploy quick --job-id <job id>Test level
RunLocalTests.
What to watch
- The validation has expired or been invalidated (10 days; --use-most-recent reaches 3 days).
- The test run outlasts
--wait(validate and quick print Metadata API request failed: The client has timed out; resume or report). - Quick deploy exits with undefinedComponentSet (run report before any retry).
- validate refuses NoTestRun.
- The validation fails with a generic message.
sf project deploy validate --test-level RunSpecifiedTests --tests <test names>
sf project deploy quick --job-id <job id>start when every minute counts.
Test level
RunSpecifiedTests with --tests, or RunLocalTests if the suite is short.
What to watch
- The validation fails with a generic message (RunSpecifiedTests without --tests is a flag mistake).
- The test run outlasts
--wait(validate and quick print Metadata API request failed: The client has timed out; resume or report). - The validation has expired or been invalidated.
- Quick deploy exits with undefinedComponentSet.
The post pairs a pull request check with scratch org or developer sandbox. Pick scratch org or developer sandbox, or pick what the post proves on integration sandbox: a pull request check with full signal.
The post pairs a pull request check with scratch org or developer sandbox. Pick scratch org or developer sandbox, or pick what the post proves on UAT or staging sandbox: a release candidate.
The post pairs a pull request check with scratch org or developer sandbox. Pick scratch org or developer sandbox, or pick what the post proves on production: a production promotion or a hotfix.
The post pairs a pull request check with full signal with integration sandbox. Pick integration sandbox, or pick what the post proves on scratch org or developer sandbox: a pull request check.
The post pairs a pull request check with full signal with integration sandbox. Pick integration sandbox, or pick what the post proves on UAT or staging sandbox: a release candidate.
The post pairs a pull request check with full signal with integration sandbox. Pick integration sandbox, or pick what the post proves on production: a production promotion or a hotfix.
The post pairs a release candidate with UAT or staging sandbox. Pick UAT or staging sandbox, or pick what the post proves on scratch org or developer sandbox: a pull request check.
The post pairs a release candidate with UAT or staging sandbox. Pick UAT or staging sandbox, or pick what the post proves on integration sandbox: a pull request check with full signal.
The post pairs a release candidate with UAT or staging sandbox. Pick UAT or staging sandbox, or pick what the post proves on production: a production promotion or a hotfix.
The post pairs a production promotion with production. Pick production, or pick what the post proves on scratch org or developer sandbox: a pull request check.
The post pairs a production promotion with production. Pick production, or pick what the post proves on integration sandbox: a pull request check with full signal.
The post pairs a production promotion with production. Pick production, or pick what the post proves on UAT or staging sandbox: a release candidate.
The post pairs a hotfix with production. Pick production, or pick what the post proves on scratch org or developer sandbox: a pull request check.
The post pairs a hotfix with production. Pick production, or pick what the post proves on integration sandbox: a pull request check with full signal.
The post pairs a hotfix with production. Pick production, or pick what the post proves on UAT or staging sandbox: a release candidate.
Pick an environment and an objective. The command, its test level and what to watch appear here.
Which command and test level fit each environment and objective?
Pick the row that matches what you are trying to prove, not the org you happen to be pointed at.
| Objective | Environment | Test level | Command |
|---|---|---|---|
| Pull request check | Scratch org or developer sandbox | NoTestRun, or RunSpecifiedTests for the changed classes |
sf project deploy start --dry-run |
| Pull request check with full signal | Integration sandbox | RunLocalTests |
sf project deploy start --dry-run --test-level RunLocalTests |
| Release candidate | UAT or staging sandbox | RunLocalTests |
sf project deploy start --test-level RunLocalTests |
| Production promotion | Production | RunLocalTests |
sf project deploy validate, then sf project deploy quick --job-id |
| Hotfix | Production | RunSpecifiedTests with --tests, or RunLocalTests if the suite is short |
sf project deploy validate --test-level RunSpecifiedTests --tests ..., then quick; start when every minute counts |
What are the trade-offs between validate-then-quick and start?
Validate-then-quick buys a short, predictable production window at the price of a perishable artefact and a two-step pipeline. start with tests is simpler and slower, and it fails late: the release window carries the whole test run, and a failure rolls back after you have waited for it. The table above pairs each path with the environment it fits.
What is the difference between deploy validate and deploy start –dry-run?
Both check whether a deployment would succeed, but validate is the documented route to a quick-deployable job ID, and --dry-run is the documented route to a sandbox check.
The reference describes --dry-run as “validate deploy and run Apex tests but don’t save to the org”, and because start accepts NoTestRun, a dry run can skip tests entirely. A CLI maintainer answered the quick deploy question on the issue tracker: a --dry-run can produce a valid quick deploy ID, but is not guaranteed to, and the recommended route is validate then quick. The June 2023 fix in plugin-deploy-retrieve pull request #651 allowed metadata-format dry runs to be quick deployed when valid. Do not build a release pipeline on it.
# @salesforce/cli 2.152.14
# Lighter check: no quick deploy, but NoTestRun is allowed
sf project deploy start \
--source-dir force-app \
--target-org "$SF_SANDBOX_ALIAS" \
--test-level NoTestRun \
--dry-run \
--wait 30
What are the failure modes of validate and quick deploy?
Most release-night failures fall into five patterns, and each has a message you can search for, a cause, and a fix.
The validation has expired or been invalidated
Message, from the quick command’s error text:
Job ID can't be used for quick deployment. Possible reasons include the deployment hasn't been validated, has already been deployed, or the validation expired because you ran it more than 10 days ago.
Cause: the job ID has expired, the validation failed, it was already quick deployed, or another deployment to the org since the validation invalidated it.
Fix: run validate again against the current org and promote the new job ID. If the pipeline relied on --use-most-recent, remember that its reach is shorter than the life of the job ID; the constraints above have both numbers.
The test run outlasts --wait
Message, as sf project deploy start prints it:
The command has timed out, although the deployment is still running. Use "sf project deploy resume" to resume watching the deployment.
validate and quick do not map the timeout, so the message is the deploy library’s own. From the source rather than a run:
Metadata API request failed: The client has timed out.
Cause: --wait defaults to 33 minutes, and a RunLocalTests run on a large org can take longer. The deployment continues in the org regardless.
Fix: watch it again with sf project deploy resume --job-id <id>, or --use-most-recent if it started in the last 3 days. The reference notes that resume does not restart the operation, because the operation always continues once started. To poll without taking over the terminal, use sf project deploy report --job-id <id> --wait <minutes>, which checks every second until the timeout. Set --wait from measured test duration, and use --async with report when the pipeline runner has its own timeout.
Quick deploy exits with undefinedComponentSet
Message, as reported in the Salesforce CLI issue tracker:
Error (1): undefinedComponentSet
Cause: a CLI defect in sf project deploy quick, thrown from ComponentSetBuilder.build while the deployment itself completed successfully in the org. It was reported in April 2023 on sfdx-cli 7.197.8 with plugin-deploy-retrieve 1.8.12 (#2098), then on @salesforce/cli 1.79.5 with plugin-deploy-retrieve 1.9.1 (#2223); #2159 names no version. The maintainers fixed it in June 2023 in plugin-deploy-retrieve pull request #651, which made the component set optional for quick deploy, and #2223’s reporter confirmed the fix. A comment on #2098 from July 2024 reports the same error again on a newer CLI.
Fix: before you retry anything, run sf project deploy report --job-id <id>. If the deployment succeeded, do not deploy again. Pin the CLI version in CI, upgrade deliberately, and if you see it on a current CLI, add your version to #2098 on the forcedotcom/cli tracker.
validate refuses NoTestRun
Message, in the shape the CLI’s flag parser produces:
Expected --test-level=NoTestRun to be one of: RunAllTestsInOrg, RunLocalTests, RunSpecifiedTests, RunRelevantTests
Cause: validate defines only test levels that run tests, because quick deploy depends on the results.
Fix: use start --dry-run --test-level NoTestRun for a test-free check, and accept that it does not feed quick deploy.
The validation fails with a generic message
Messages, from the error text of the deploy plugin and of @salesforce/core:
Failed to validate the deployment (<job id>). Due To: <reason>
You must specify tests using the --tests flag if the --test-level flag is set to RunSpecifiedTests.
<directory> does not contain a valid Salesforce DX project.
Invalid deploy ID: <job id> for org: <org>
In order: FailedValidationError summarises a real metadata or test failure, so rerun with --verbose or read sf project deploy report --job-id <id>. RunSpecifiedTests without --tests is a flag mistake. InvalidProjectWorkspaceError means the command is not running inside a project. @salesforce/core looks for sfdx-project.json in the current directory and each directory above it, so any directory at or below the project root works. An invalid deploy ID usually means the wrong org: the report reference notes that on a different machine from the one that deployed, you must pass --target-org pointing at the same org. Both 15- and 18-character job IDs are accepted by --job-id.
What is the recovery playbook when the quick deploy path fails?
Work the scenario you are in from the top. Each step is ordered so that you learn the org’s true state before you change it.
Validation expired or invalidated
- Confirm with
sf project deploy report --job-id <id>: status, whether it was a validation, and when it started. - Check what has reached the org since, so you know whether to re-test the merge rather than just re-validate.
- Run
validateagain with the same test level and capture the new job ID as a pipeline artefact. - Promote with
quickwhile the window is open, then verify withreport.
Job ID lost
- Try
sf project deploy report --use-most-recenton the machine that ran the validation; it reaches back 3 days. - If that fails, look in the pipeline logs for the validation’s output. In my pipelines the ID is read from the
--jsonoutput and stored as a build artefact for exactly this reason. - If the ID is gone and the validation has expired anyway, re-validate.
Tests pass in the sandbox and fail in production
- Check what the sandbox actually ran. Sandboxes do not run tests by default, so “passed” may mean “not run”.
- Compare test levels:
RunLocalTestsin production runs every local test in the org, including ones your change never touched. - Pull the production failures with
sf project deploy report --job-id <id> --junit --results-dir <dir>. - Reproduce in a recently refreshed sandbox with
start --dry-run --test-level RunLocalTests, fix or quarantine the failing tests, and validate again.
Deployment stuck past –wait
- Do not start a second deployment of the same payload: you will not know which one changed the org.
sf project deploy resume --job-id <id>to watch it;report --waitif you only need the status.- If the resumed deployment fails, read the component and test failures from
report, fix, and go back tovalidate. - Set the pipeline’s
--waitfrom the measured run, and move the long test run intovalidateso it never sits inside the release window again.
What should I check first when a deployment misbehaves?
The checklist is the playbook’s: what to check first when a Salesforce CLI pipeline fails. It covers the CLI version, the authenticated user and instance, the org, the test level, the job ID, the org’s view of the result from report, later deployments to the org, and the runner’s timeout against --wait.
If your pipeline cannot authenticate reliably, fix that first with the headless JWT login for Salesforce CLI in GitHub Actions. The wider release flow, including how these stages fit around approvals, is in the Salesforce CLI DevOps Playbook.
What pipeline pattern works best for most teams?
Validate on the release candidate, store the job ID, and quick deploy after approval inside the same few days. The long test run stays out of the release window, and failures surface while there is still time to fix them.
# @salesforce/cli 2.152.14
# Stage 1: release candidate pipeline
sf project deploy validate --source-dir force-app --target-org "$SF_PROD_ALIAS" --test-level RunLocalTests --wait 90 --json > validate.json
# Stage 2: release pipeline, after approval, within 10 days of stage 1
sf project deploy quick --job-id "$SF_VALIDATE_JOB_ID" --target-org "$SF_PROD_ALIAS" --wait 60
# Stage 3: confirm the org's view of the result
sf project deploy report --job-id "$SF_VALIDATE_JOB_ID" --target-org "$SF_PROD_ALIAS"
Pass the job ID between stages as a pipeline artefact, and pass --target-org explicitly in the later stages, because they usually run on a different machine from the validation.
Top Tip: Treat the validation job ID like a release ticket: it has an owner, a start time and an expiry. Print all three in the pipeline summary so nobody has to guess on release night.
References
- Salesforce CLI Command Reference, project deploy validate: required tests, 10-day job ID, the sandbox note and the
--dry-runalternative. - Salesforce CLI Command Reference, project deploy quick:
--job-id,--use-most-recentand its 3-day reach, production intent. - Salesforce CLI Command Reference, project deploy start:
--test-levelvalues and coverage rules,--dry-run,--wait. - Salesforce CLI Command Reference, project deploy resume and project deploy report.
- Salesforce CLI deploy plugin message sources, which generate the reference text: deploy.metadata.md, deploy.metadata.validate.md, deploy.metadata.quick.md, deploy.metadata.resume.md, deploy.metadata.report.md, and the command source validate.ts and quick.ts for the test-level options, the 33-minute
--waitdefault, job ID handling and thedeployRecentValidationcall behindquick. @salesforce/corepollingClient.ts,@salesforce/source-deploy-retrievemetadataTransfer.ts and its sdr.md: the “The client has timed out.” polling error and the “Metadata API request failed: %s” wrapper thatvalidateandquickprint. Only start.ts maps it toerror.ClientTimeout.- oclif, parser errors: the “Expected –flag=value to be one of” message the CLI prints for a rejected flag value.
- Metadata API Developer Guide, Running Tests in a Deployment and Running a Subset of Tests in a Deployment: the test levels and the per-class rule for
RunSpecifiedTests. - Metadata API Developer Guide, deployRecentValidation(): the platform operation behind quick deploy, and the production rule of overall code coverage of at least 75% with some coverage on every Apex trigger.
- Salesforce Help, Deploy a Change Set: 75% coverage required for production, not enforced for sandbox or Developer Edition orgs.
- Salesforce Help, DevOps Center, Run a Validate-Only Deployment: validations are valid for 10 days, quick deploy requires tests to have run, and a subsequent promotion disqualifies an existing validation.
- Trailhead, Org Development Model, Test and Deploy Changes: validate then quick deploy within 10 days, as long as there is no other deployment or major change to the org in between; 75% coverage of the code being deployed.
@salesforce/coreconfig.md and internal.ts: theInvalidProjectWorkspaceErrortext and the upward search forsfdx-project.json.@salesforce/sf-plugins-coresfCommand.ts throws itsRequiresProjecterror only for commands that setrequiresProject, which start.ts,validate.tsandquick.tsdo not.- Salesforce CLI issue tracker: #2098, #2159 and #2223 on
undefinedComponentSetduring quick deploy, including the maintainer’s answer on dry-run job IDs; the fix, plugin-deploy-retrieve #651, merged 7 June 2023. - @salesforce/cli on npm: 2.152.14, the
latesttag on 6 October 2026, which these commands were written against. - On this site, the post on sf project deploy start: the flags, dry runs, test levels and error messages of
startin full.
Need a Salesforce release pipeline you can trust?
I take short specialist contracts to harden Salesforce CI/CD, cut release risk and stop repeat deployment failures.