Dylan AndersenDylan Andersen's Docs
Cursor + SalesforceGetting Started with Cursor

Source Tracking & .forceignore

Scratch orgs vs. sandboxes, push vs. deploy, retrieve vs. pull, and the .forceignore file that saves your deploys

Source Tracking & .forceignore

Every SE hits the same wall around week two: a deploy fails with a metadata error that has nothing to do with what they changed, or a retrieve pulls down ten thousand lines of managed-package scar tissue. The fix is understanding how Salesforce tracks what's in your project versus what's in the org, and what .forceignore is for.

This page is the mental model. Read it once, and the rest of the CLI stops feeling random.

Not comfortable with the CLI? Let the Agent drive.

The sf commands below can all be delegated to Cursor's Agent. A good prompt is:

"Deploy my local changes to the org aliased demo and tell me what changed. Do a dry run first and ask me to confirm before the real deploy."

The Agent reads the output, asks for confirmation before destructive operations, and explains errors. The commands below are here so you can read and understand what it's doing.

Scratch org vs. sandbox: the big distinction

There are two worlds in Salesforce DX, and they behave differently.

ConceptScratch orgSandbox
Source trackingBuilt in, two-wayOff by default, enable with sf project deploy preview workflows
Lifespan1–30 daysPer customer policy
Metadata starting stateClean, defined by scratch definitionWhatever the customer has
Typical commandssf project deploy start, sf project retrieve startSame commands, but you act like you don't own the org
Who breaks itYouTwelve admins and a managed package

The source-tracking loop (scratch orgs)

Scratch orgs track every metadata change on the server and on your laptop. You get a real two-way flow:

sf project deploy start               # push local changes up
sf project retrieve start             # pull server changes down
sf project deploy preview             # "what would happen if I deployed?"
sf project deploy report              # status of the last async deploy

Because the scratch org knows what's changed, you can ask it which metadata is "out of sync" and decide which way to reconcile. This is the right world to learn in.

The no-tracking world (sandboxes and production)

Sandboxes and production don't track changes per-user by default. You treat the org as an external system that you deploy to and retrieve from.

sf project deploy start --target-org acme-uat              # push
sf project deploy start --dry-run --target-org acme-uat    # safe validate
sf project retrieve start --metadata "ApexClass:AccountService" --target-org acme-uat  # pull

The --dry-run flag is your seatbelt. Use it before any deploy to a customer org. It validates the deploy without actually writing anything, and it catches the overwhelming majority of mistakes.

The number one sandbox gotcha

You pull a Profile, you change one field, you push it back, and you've quietly overwritten seventy unrelated settings some admin set last week. Never retrieve full Profiles from a customer sandbox. Use Permission Sets and Permission Set Groups, and add Profile to .forceignore to stop yourself from retrieving them by accident.

sfdx-project.json: your project's contract with the CLI

At the root of every Salesforce DX project there's an sfdx-project.json. It tells sf where your source lives, what API version to use, and which namespace applies.

A typical file:

{
  "packageDirectories": [
    { "path": "force-app", "default": true }
  ],
  "namespace": "",
  "sfdx-project.json": "55.0",
  "sourceApiVersion": "62.0",
  "sourceBehaviorOptions": ["decomposePermissionSetBeta2"]
}

The fields worth knowing:

  • packageDirectories is where your source code lives. Multiple directories means multiple packages, which is how 2GP projects organize themselves.
  • sourceApiVersion is the API version every deploy uses unless a specific metadata file overrides it. When you bump this, bump the apiVersion in each .js-meta.xml to match.
  • namespace is empty for unmanaged work and populated for managed packages.
  • sourceBehaviorOptions turns on newer, better behaviors. The decomposePermissionSet flag, for example, breaks a giant .permissionset-meta.xml into per-object files so diffs are readable. Turn it on for new projects.

.forceignore: the list of things you don't want on your laptop

.forceignore is a .gitignore-style file at the root of your project. It tells sf which metadata not to retrieve, deploy, or track. This is the single most important file once you start working against real customer orgs.

A starter .forceignore for any project:

# Don't pull full profile dumps from sandboxes
**/profiles/**

# Managed package metadata scars
**/installedPackages/**
**/*.mpd-meta.xml

# Reports and dashboards tend to explode on retrieve
**/reports/**
**/dashboards/**

# Noisy personal or session-specific settings
**/settings/UserInterfaceSettings.settings-meta.xml
**/settings/LightningExperienceSettings.settings-meta.xml

# Flows authored by others that shouldn't travel with this project
# (Prefer scoping retrieves to specific flows you own)
# **/flows/*.flow-meta.xml

# Scratch org temp files
.sfdx/
.sf/

Add to this file the minute you find yourself deleting the same metadata after every retrieve.

Managed packages leave footprints

When a customer installs a managed package, pieces of that package show up in retrieves as read-only metadata. You can't deploy them back and you don't want to version them. .forceignore makes those retrieves clean.

Deploy, retrieve, push, pull: which is which

You'll see these words used loosely. Here's how sf actually uses them:

  • Deploy moves source from your laptop to the org.
  • Retrieve moves metadata from the org to your laptop.
  • Push is the scratch-org-only shorthand for "deploy everything the tracker says is different."
  • Pull is the scratch-org-only shorthand for "retrieve everything the tracker says is different."

In modern sf commands, you'll mostly use sf project deploy and sf project retrieve. The old sfdx force:source:push commands still work, but the new ones are clearer and better supported.

API versions and drift

Every LWC, Aura component, and Apex class has an apiVersion. When those get out of sync with your sourceApiVersion, you see deploy errors like The type ... was not found or API version X is not available in this org.

A clean bump:

# 1. Update sfdx-project.json's sourceApiVersion to 62.0
# 2. Bulk-update all .js-meta.xml files
find force-app -name "*.js-meta.xml" -exec \
  sed -i '' 's|<apiVersion>.*</apiVersion>|<apiVersion>62.0</apiVersion>|g' {} +
# 3. Do the same for .cls-meta.xml and .trigger-meta.xml
find force-app -name "*-meta.xml" -exec \
  sed -i '' 's|<apiVersion>.*</apiVersion>|<apiVersion>62.0</apiVersion>|g' {} +
# 4. Re-deploy
sf project deploy start --dry-run

On WSL and Linux, drop the '' after -i.

The safe deploy flow for customer orgs

When you're about to touch a customer sandbox, use this flow:

See what would move without actually moving anything:

sf project deploy preview --target-org acme-uat

Reads your local source, compares to the org, lists what would be added, changed, or deleted.

Run the actual deploy with validation only:

sf project deploy start --target-org acme-uat --dry-run

This runs the full deploy server-side, including any tests, but doesn't commit. Catches compile errors, missing dependencies, and test failures before they become a problem.

For production-style orgs, use a quick-deploy-ready validation:

sf project deploy validate --target-org acme-prod --test-level RunLocalTests

Run this ahead of a change window. If it passes, you can later use sf project deploy quick --job-id ... to deploy the already-validated payload in seconds.

When ready:

sf project deploy start --target-org acme-uat --test-level RunLocalTests

Or, if you validated earlier:

sf project deploy quick --job-id 0Af... --target-org acme-prod

Retrieve what you just deployed to prove it landed the way you expected:

sf project retrieve start --metadata "ApexClass:AccountService" --target-org acme-uat
git diff

If git diff is empty, the server matches your local source.

Troubleshooting patterns

  • "Entity type 'X' is not supported." Add that metadata type to .forceignore, or scope your retrieve to specific files with --metadata.
  • Deploy succeeds locally but the component doesn't show up in the UI. Check that the Lightning component has <isExposed>true</isExposed> and a matching target in .js-meta.xml, then verify the user has a permission set granting visibility.
  • "Value not found in picklist." You deployed metadata that references a picklist value the target org doesn't have. Either add the value first, or include the picklist metadata in the same deploy.
  • Retrieve pulls thousands of files you don't want. You ran a broad retrieve instead of a scoped one. Use --metadata "Type:Name" to scope, and add offending types to .forceignore.
  • Scratch org "won't let me push." Something changed server-side that conflicts with your local source. Run sf project retrieve start to pull the server state, reconcile manually, then push again.

What to do next

On this page