How to Use sf project deploy start Safely: Flags, Dry Runs, Test Levels and the Errors It Throws

Reading Time: 16 min
Author: William Watson Published: October 6, 2026 System: Salesforce CLI source and metadata deployments from a Salesforce DX project Scale: Scratch orgs through production, by hand and in CI Failure: Rejected flags, conflicts, empty component sets, timeouts and failed components Audience: Salesforce developers and release engineers who type the command
Current: Top of article
Direct answer

Run sf project deploy start from the project root against a named target org, pick the source with --source-dir, --manifest or --metadata, set --test-level deliberately rather than inheriting the default, prove it first with --dry-run, and size --wait to the test run or go --async and watch with report.

sf project deploy start is the command you type a hundred times a week and still get wrong once a month: the flag that silently overwrote a colleague’s work, the test level you inherited, the timeout that left a deployment running with nobody watching. This article is the command on its own terms: what it needs, what each flag does and when it hurts, what a dry run proves, which test level to pass, and what every error it throws means.

Every command below was written against @salesforce/cli 2.152.14, which bundles @salesforce/plugin-deploy-retrieve 4.2.2, and checked against that version’s command reference and the plugin’s message sources rather than run in an org. If you are deciding between this command and sf project deploy validate, that comparison has its own article: sf project deploy start vs sf project deploy validate.

What does sf project deploy start do, and what does it need?

It deploys metadata from your local project to an org, and it needs three things: a Salesforce DX project to run from, a target org, and a way of saying which components to send.

The reference states each requirement. You must run the command from within a project, or it stops with the RequiresProject error quoted in the errors section below. The target org comes from --target-org or from the target-org configuration variable; without either you get the No default environment found error, also below. The components come from --source-dir, --manifest or --metadata, which the reference says not to combine, or from --metadata-dir for a directory or zip of metadata-format files.

Two defaults matter before you type anything. Metadata is deployed in source format unless you pass --metadata-dir. And if the org allows source tracking, the command tracks the changes in your source; the reference notes that scratch and sandbox orgs have it on by default and that production orgs never allow it. Source tracking is what makes sf project deploy start with no component flag deploy only your local changes, and it is also what produces the conflict and “nothing to deploy” errors below.

# @salesforce/cli 2.152.14
sf project deploy start \
  --source-dir force-app \
  --target-org my-sandbox \
  --test-level RunLocalTests \
  --wait 30

Which sf project deploy start flags matter, and which ones bite?

Six flags decide what gets deployed and how safely, and four of them can overwrite work or hide failures if you reach for them out of habit. The table is drawn from the command reference; the “when it hurts” column is where the flag’s own description warns you, or where I have seen it go wrong.

Flag What it does When it hurts
--source-dir Deploys a file or a folder and its subdirectories Combined with --metadata or --manifest; the reference says pick one
--manifest Deploys the components in a package.xml, child components included A stale manifest deploys stale components
--metadata Deploys named components, wildcards allowed in quotes such as 'ApexClass:MyClass*' A wide wildcard sends more than you reviewed
--metadata-dir Deploys metadata-format files from a directory or zip Mixing it with source-format flags; it is a different mode
--test-level and --tests Sets the Apex test level; --tests names the tests for RunSpecifiedTests Inheriting the default: NoTestRun in development orgs, RunLocalTests in production with Apex
--dry-run Validates and runs tests but saves nothing Treating it as a quick deploy; see the next section
--ignore-conflicts Deploys local files even if they overwrite changes in the org Someone else’s change in a shared sandbox is gone; the flag has no effect on production, which does not track source
--ignore-warnings Sets the success status to true even when a warning occurs A pipeline goes green on a warning you needed to read
--ignore-errors Deploys the components without errors and skips the ones with errors, no roll back The reference says never use it on production: the org ends up inconsistent
--purge-on-delete Deleted components skip the Recycle Bin There is no undo
--wait and --async --wait polls for the given minutes, 33 by default; --async returns the job ID at once A --wait shorter than the test run ends with a timeout while the deployment carries on
--target-org The org to deploy to, or the target-org config value A default org set months ago is the wrong org today
--json Machine-readable output for scripts Reading the exit code without reading the result

Best Practice: Put --target-org on every command in a script, even when a default is set. The default is for your terminal, not for the pipeline.

How is a dry run different from sf project deploy validate?

A dry run is this command with nothing saved; validate is the production preflight that gives you a job ID to quick deploy. The reference describes --dry-run as “validate deploy and run Apex tests but don’t save to the org”, and because start accepts every test level, a dry run can skip tests entirely with NoTestRun. That makes it the right check for a sandbox pipeline. validate requires tests, is intended for production orgs, and returns a job ID that sf project deploy quick can deploy without re-running tests for 10 days. A CLI maintainer has said a dry run can produce a quick-deployable ID but is not guaranteed to, so build the release path on validate. The full decision is in sf project deploy start vs sf project deploy validate.

# @salesforce/cli 2.152.14
sf project deploy start \
  --source-dir force-app \
  --target-org my-sandbox \
  --test-level NoTestRun \
  --dry-run

Which test level should you pass to sf project deploy start?

Pass one on purpose every time, because the default changes with the org. From the reference’s --test-level description:

  • NoTestRun runs nothing and applies only to development environments such as sandboxes, Developer Edition and trial orgs, where it is the default.
  • RunSpecifiedTests runs 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.
  • RunLocalTests runs every test in the org except those from installed managed and unlocked packages, and is the default for production deployments that include Apex classes or triggers.
  • RunAllTestsInOrg runs everything, managed package tests included.
  • RunRelevantTests (Beta) lets Salesforce choose the tests for the payload, with the same per-class rule as RunSpecifiedTests.

The production rule behind the defaults, from the Metadata API Developer Guide, is overall coverage of at least 75% with some coverage on every Apex trigger. Salesforce Help adds that the coverage restriction is not enforced for sandbox or Developer Edition orgs. Two details from the command’s source are worth knowing. If you pass --tests without --test-level, the command defaults the level to RunSpecifiedTests for you. If you pass RunSpecifiedTests without --tests, it stops with the You must specify tests error quoted below.

# @salesforce/cli 2.152.14
# Hotfix to production: name the tests, accept the per-class 75% rule
sf project deploy start \
  --metadata ApexClass:OrderSyncJob ApexClass:OrderSyncClient \
  --target-org production \
  --test-level RunSpecifiedTests \
  --tests OrderSyncJobTest \
  --wait 45

What are sf project deploy preview, report and resume for?

preview shows what a deploy would send, report shows the status of one that ran, and resume watches one that outlived your --wait. All three come from the same plugin, and readers search for them too.

sf project deploy preview prints a table of what will deploy or be deleted, the conflicts between local files and the org, and the forceignored files. The reference ties the conflicts part to source-tracked orgs, and the nothingToDeploy error points you here.

# @salesforce/cli 2.152.14
sf project deploy preview --source-dir force-app --target-org my-sandbox

sf project deploy report checks or polls the status of a deploy operation by job ID, or with --use-most-recent for one started in the past 3 days. With --wait it polls every second until the timeout; without it, one check. Run it on a different machine from the one that deployed and you must pass --target-org, because the local cache that maps the job to the org is not there.

# @salesforce/cli 2.152.14
sf project deploy report --job-id "$SF_DEPLOY_JOB_ID" --target-org production --wait 20 --junit --results-dir deploy-results

sf project deploy resume picks up watching a deploy, quick deploy, validation or cancellation that timed out or was started with --async. The reference is careful here: it does not resume the operation itself, because the operation always continues once started; it resumes your view of it, and updates source tracking when it finishes. If the job already completed, it says so and shows the result.

# @salesforce/cli 2.152.14
sf project deploy resume --job-id "$SF_DEPLOY_JOB_ID"

What do the sf project deploy start errors mean?

Most of them are the command refusing to guess. Each message below is quoted from the plugin’s message sources or the libraries it uses, with the cause and the fix.

Component failures

There is no single “deploy failed” sentence. When the org rejects components, the CLI prints a Component Failures table with the type, the name, the problem and the line and column; in my pipelines the non-zero exit that follows is what fails the stage. The deployed-files table prints by default; --verbose adds passing tests, code coverage and string replacements; --concise drops the deployed-files table, so the log shows the failures and the test summary, which is what you want in a pipeline. Fix the components the table names, then deploy again; with --ignore-errors the good components would have gone in and the bad ones skipped, which is why the reference says never to use it against production.

The project and the org

This command is required to run from within a Salesforce project directory.
No default environment found. Use -o or --target-org to specify an environment.

The first is RequiresProjectError: run from the directory that holds sfdx-project.json. The second means no --target-org and no target-org config value; name the org.

Test level and tests

Expected --test-level=RunTests to be one of: NoTestRun, RunSpecifiedTests, RunLocalTests, RunAllTestsInOrg, RunRelevantTests
You must specify tests using the --tests flag if the --test-level flag is set to RunSpecifiedTests.

The first is the CLI’s flag parser rejecting a value that is not in the list the command defines. The second is the command’s own check. Pass a listed level, and name tests when the level is RunSpecifiedTests.

The wait ran out

The command has timed out, although the deployment is still running. Use "sf project deploy resume" to resume watching the deployment.

--wait defaults to 33 minutes and the test run took longer. Nothing was cancelled. Run sf project deploy resume --job-id <id>, or report if you only need the status, and size the next --wait from the measured run.

Conflicts and nothing to deploy

There are changes in the org that conflict with the local changes you're trying to deploy.
No local changes to deploy.

Both come from source tracking. For the conflict, the reference offers two ways out: rerun with --ignore-conflicts to overwrite the org, or run sf project retrieve start --ignore-conflicts to overwrite your local files. Decide which side is right before you pick. For the second, source tracking sees nothing changed; run sf project deploy preview with a manifest, directory or metadata flag to see conflicts and ignored files, or name the components explicitly.

No components

No source-backed components present in the package.
force-app/main/default/clases: File or folder not found

Both come from the source-deploy-retrieve library under the command. The first means the path or manifest resolved to nothing deployable, often a .forceignore rule or a folder with no metadata in it. The second is a typo in the path.

Job IDs

Invalid deploy ID: 0Af... for org: [email protected]

From report: the ID is wrong, or it belongs to a different org than the one you pointed at, and the org shown is the username. The reference’s actions are to check the ID and the --target-org value.

What should you check first, and what changes at scale?

Run this list before you change anything:

  1. Are you in the project root, with sfdx-project.json beside you?
  2. Which org did the command target, and is it the one you meant?
  3. Which test level ran, including a default you did not set?
  4. Did --wait outlast the test run, and is the deployment still running in the org?
  5. Is the org source-tracked, and is a conflict or an empty change set the real message?
  6. Does sf project deploy preview show what you expected to send?

At scale the same command needs a different posture. Long test runs do not fit inside --wait or a pipeline runner’s own timeout, so start with --async, capture the job ID from the --json output, and poll with sf project deploy report --wait in a later step; --junit and --results-dir give the pipeline its test artefacts, and --coverage-formatters writes coverage in the format your tooling reads. Pass --target-org on every call, because the stage that polls usually runs on a different machine from the one that deployed. Pin the CLI version so that flag lists and defaults do not change under you. And keep --ignore-errors, --ignore-conflicts and --purge-on-delete out of any script that can reach production. The wider release flow, and how this command sits beside validate and quick in it, is in the Salesforce CLI DevOps Playbook.

Top Tip: Print the resolved target org, the test level and the job ID at the top of every pipeline log. Those three lines answer most deployment questions before anyone opens the org.

References

Need a Salesforce deploy pipeline that fails less?

I take short specialist contracts to harden Salesforce CLI pipelines, size test runs and stop repeat deployment failures.

Discuss your deployment pipeline