Skip to Content
CLICommand ReferenceMetadata & Source Control

Metadata & Source Control


sfdt manifest

Generates a package.xml from a git diff. Understands Salesforce metadata file naming to map changed files to their types and member names. Optionally runs AI to flag likely missing dependencies.

sfdt manifest # diff main...HEAD → preview-package.xml sfdt manifest --base develop # diff from develop sfdt manifest --base abc1234 # diff from a specific commit SHA sfdt manifest --name 1.2.0 # named release → rl-1.2.0-package.xml sfdt manifest --package feature-a # scope diff to one package directory sfdt manifest --package all --name 1.2.0 # all packages → rl-1.2.0-package.xml sfdt manifest --output deploy/pkg.xml # custom output path sfdt manifest --destructive dist/del.xml # also write destructiveChanges.xml sfdt manifest --print # print to stdout instead of writing sfdt manifest --ai-cleanup # run AI dependency check
OptionDescription
--base <ref>Base git ref to diff from (default main); branch names or SHAs
--head <ref>Head git ref to diff to (default HEAD)
--name <label>Release label for the filename: semver, free-form, or today (→ YYYY-MM-DD)
--package <name|all>Scope the diff to one package directory (matched by last path segment)
--output <path>Output path, overriding the computed path
--destructive <path>Also write a destructiveChanges.xml for deleted components
--ai-cleanupRun AI dependency analysis on the manifest
--no-ai-cleanupSkip AI analysis even when features.ai is on
--printPrint to stdout instead of writing a file

Output filename convention:

ScenarioOutput path
No --namemanifest/release/preview-package.xml
--name 1.2.0 (all packages)manifest/release/rl-1.2.0-package.xml
--name 1.2.0 --package feature-amanifest/release/rl-1.2.0-feature-a-package.xml
With manifestLayout: subpathmanifest/release/feature-a/rl-1.2.0-package.xml

Merge-base resolution: when --base is a branch name, sfdt computes the merge-base with HEAD so commits already on the base branch are excluded. Pass an explicit SHA to diff from a branch tip directly. The --name value must start with an alphanumeric character.

See Multi-package projects for the scoped workflow.


sfdt pull

Pulls metadata from the configured default org into your local source, using a SQLite-backed cache so only components that changed in the org since the last pull are re-fetched.

sfdt pull # incremental — only changed components sfdt pull --full # force a full retrieve and rebuild the cache sfdt pull --status # show cache status (last pull time, component counts) sfdt pull --dry-run # preview without writing

What gets pulled is defined in .sfdt/pull-config.json:

{ "metadataTypes": ["ApexClass", "ApexTrigger", "LightningComponentBundle", "CustomObject", "CustomField", "Layout", "FlexiPage", "PermissionSet", "Flow"], "targetDir": "force-app/main/default" }

Cache behavior is tuned via pullCache in config (enabled, parallelism, batchSize). See Incremental Pull.


sfdt drift

Detects metadata drift between your local source files and a target org — i.e. changes made directly in the org without being committed. Result written to logs/drift-latest.json.

sfdt drift sfdt drift --org staging
OptionDescription
--org <alias>Target org (defaults to config.defaultOrg)
--jsonStructured output

Drift does a deep per-component content diff — it tells you which components differ.


sfdt compare

Compares the full metadata inventory between two orgs, or between local source and an org. Unlike drift, compare checks for the presence or absence of entire members.

sfdt compare # local source vs default org sfdt compare --target sandbox-uat # local source vs a named sandbox sfdt compare --source prod --target sandbox # org-to-org comparison sfdt compare --output deploy/missing.xml # write source-only items as package.xml
OptionDescription
--source <alias|local>Source side; local or an org alias (default local)
--target <alias>Target org (defaults to config.defaultOrg)
--output <file>Write a package.xml of source-only items to this path

Results land in logs/compare-latest.json and the dashboard’s Compare page (filter by source-only / target-only / both, plus a per-component XML diff).

Drift vs Compare

QuestionTool
Has anyone changed metadata in the org directly?sfdt drift
What metadata exists in one org but not the other?sfdt compare
Is my local source the authoritative version of what’s deployed?sfdt drift
What needs deploying to bring a sandbox up to par with prod?sfdt compare
Generate a manifest of what’s missing from the target?sfdt compare --output missing.xml

sfdt scan

Fetches a complete metadata inventory from an org and writes it to a file (logs/scan-latest.json when launched from the dashboard).

sfdt scan sfdt scan --org production --output inventory.json --format json sfdt scan --format table
OptionDescription
--org <alias>Org to scan (defaults to config.defaultOrg)
--output <file>Output file
--format <json|table>Output format

sfdt dependencies

Lists what a metadata component references and what references it, via the Tooling API’s MetadataComponentDependency object — the same data the Salesforce “Where is this used?” view draws on. Resolution and grouping are shared with the Chrome extension and dashboard through @sfdt/flow-core, so all three report identically.

sfdt dependencies AccountTriggerHandler # ApexClass (default) sfdt dependencies AccountTrigger --type ApexTrigger sfdt dependencies My_Flow --type Flow sfdt dependencies My_Custom_Field --type CustomField # resolve a field by its DeveloperName sfdt dependencies myComponent --type LightningComponentBundle --org production --json
Argument / OptionDescription
<name>Developer name of the component to resolve
--type <MetadataType>Metadata type to resolve (default ApexClass)
--org <alias>Target org (defaults to config.defaultOrg)
--jsonStructured output

Supported --type values: ApexClass, ApexTrigger, ApexPage, ApexComponent, Flow, LightningComponentBundle, AuraDefinitionBundle, CustomField. The list is driven by the shared METADATA_TYPE_REGISTRY in @sfdt/flow-core, so the CLI and the dashboard graph never drift.

Output is two grouped lists — references (this component → others) and referenced-by (others → this component) — each bucketed by metadata type. An unsupported --type or a name that resolves to nothing surfaces a clean error / not-found message.

Source-parsed gaps (--gaps)

The Tooling API’s MetadataComponentDependency is statically incomplete — it never records references that only exist in source in forms it can’t resolve: dynamic Apex (Type.forName, Database.query), LWC @salesforce/apex imports, formula-field references, and Flow subflow/action references. --gaps finds those by parsing your local source.

sfdt dependencies AccountService --type ApexClass --gaps # offline: list inferred refs sfdt dependencies AccountService --type ApexClass --gaps --org prod # diff: mark MISSING vs confirmed
  • Offline by default — parses the local source tree, no org needed. Each row shows the reference kind (apex-dynamic, lwc-apex, formula, flow-subflow, …), the target, and the source evidence.
  • --org <alias> additionally queries the Tooling API and tags each inferred reference MISSING (a real gap the API doesn’t record) or confirmed (already known to the API).
  • --json emits the structured { from, org?, gaps[] } report.

--gaps is a heuristic regex report — it can surface false positives (e.g. a class name in a comment) and doesn’t resolve fully-dynamic references (a class name built from a variable). Treat it as a lead for impact analysis, not an exhaustive list.


sfdt soql

The SOQL/SOSL toolkit — the full query & schema lifecycle: find sObjects, describe fields and relationships, validate a query without running it, inspect the org’s query plans, and execute with a row bound enforced. Every subcommand is read-only against the org and supports --json.

sfdt soql search invoice --category custom # which objects match "invoice"? sfdt soql describe Account --filter phone # field inventory, filtered sfdt soql relationships Contact # parent lookups + child subqueries sfdt soql validate "SELECT Id FROM Account" # local checks + org LIMIT 0 round-trip sfdt soql plan "SELECT Id FROM Case WHERE Status='Open'" # REST explain: cost/selectivity sfdt soql query "SELECT Id, Name FROM Account" --limit 50 # bounded execution sfdt soql query "SELECT Id FROM Contact" --out contacts.csv # export raw rows sfdt soql sosl "FIND {Acme} IN ALL FIELDS RETURNING Account(Id, Name)"
SubcommandDescription
search [term]Find sObjects by case-insensitive name substring (--category all|custom|standard, --limit <n>)
describe <sobject>Fields (type, picklists, references), key prefix, child relationships (--filter <term>, --tooling)
relationships <sobject>Parent lookups (dot notation) and child relationships (subqueries) (--direction parent|child|both)
validate <query>Local static checks plus an org LIMIT 0 round-trip; exits non-zero when invalid (--local-only, --tooling)
plan <query>Org query plans via the REST explain endpoint — the query is never executed (--api-version <ver>)
query <soql>Bounded SOQL execution (--limit <n>, --tooling, --all-rows, --out <file>, --format json|csv)
sosl <search>Bounded SOSL execution (--limit <n>, --out <file>, --format json|csv)

Bounded execution. query and sosl never run unbounded: the effective row cap is --limit, defaulting to the soql.defaultLimit config key (200) and clamped to soql.maxLimit (2000). A LIMIT already present in the query is kept only when it’s at or under the cap, and results carry bound + truncated metadata so scripts can tell a complete result from a capped one.

validate degrades gracefully: with no reachable org it reports its local-only verdict with a warning — it never fabricates an org pass. --out exports write the raw records (JSON, or CSV with parent fields flattened to dot-path columns); the { status, result, warnings } envelope stays a stdout-only contract.

The read-only pieces are exposed over MCP as sfdt_soql_search, sfdt_soql_describe, sfdt_soql_validate, sfdt_soql_plan, and sfdt_soql_query, and in the VS Code command tree as SOQL Toolkit.


sfdt retrofit

Retrofits metadata that was changed directly in a source org (e.g. an admin editing production) back into source control and forward to another org: retrieve → commit → smart-deploy. The deploy step is validate-only unless you pass --execute.

sfdt retrofit --source prod --target uat # validate-only by default sfdt retrofit --source prod --target uat --execute # real deploy sfdt retrofit --source prod --no-deploy # retrieve + commit only sfdt retrofit --source prod --no-commit # retrieve only (review yourself) sfdt retrofit --source prod --target uat --metadata "CustomField,ValidationRule,Layout"
OptionDescription
--source <alias>Org to retrieve changes FROM (required)
--target <alias>Org to deploy changes TO (omit with --no-deploy)
--metadata <types>Comma-separated metadata types to retrieve (defaults to a common admin-changed set)
--commit-msg <msg>Commit message for the retrofit commit (default chore: retrofit metadata from <source>)
--no-commitRetrieve only; leave changes uncommitted for review
--no-deployRetrieve and commit, but do not deploy to a target
--executePerform a real deploy to the target (default is validate-only)
--skip-preflightSkip preflight checks before deploying
--jsonStructured output

The default metadata set covers what admins typically change in an org directly: CustomField, ValidationRule, Layout, CustomLabel, RecordType, QuickAction, FlexiPage, CustomApplication, CompactLayout. Override it with --metadata.

When it will auto-commit, retrofit refuses to start on a dirty source tree — the retrieve overwrites files in place, so pre-existing uncommitted work would be silently bundled into the retrofit commit. Commit or stash first, or pass --no-commit to review and commit the result yourself. Only the configured package directories are ever staged (never git add -A).

The deploy step reuses the Smart Deploy engine, so overwrite protection and smart test selection apply. See Retrofit & PR Comments for the full workflow, including the cross-org release-mismatch check.


sfdt flow

Flow-specific analyses, powered by the same @sfdt/flow-core engine as the Chrome extension — so a Flow scores identically in the CLI, the dashboard, and the extension. Two subcommands: scan and conflicts.

sfdt flow scan

Runs the flow-core health analysis on every Flow with an active version and writes a scored report to logs/flow-scan-latest.json.

sfdt flow scan sfdt flow scan --org staging sfdt flow scan --output reports/flows.json sfdt flow scan --json
OptionDescription
--org <alias>Org alias (defaults to config.defaultOrg)
--output <file>Write the report to this path (default logs/flow-scan-latest.json)
--jsonEmit the report to stdout instead of writing a file

The terminal summary shows the flow count, average health score, total errors, and the five worst-scoring flows.

sfdt flow conflicts

Lists record-triggered Flow groups that fire on the same object + timing + event — the classic “two before-save flows on Account update” situation where execution order is non-deterministic. Report written to logs/flow-conflicts-latest.json.

sfdt flow conflicts sfdt flow conflicts --org production --json
OptionDescription
--org <alias>Org alias (defaults to config.defaultOrg)
--output <file>Write the report to this path (default logs/flow-conflicts-latest.json)
--jsonEmit the report to stdout instead of writing a file

Each conflict group lists the flows involved with their entry-criteria summary — flows with no entry criteria are highlighted, since they run on every record change in the group. See flow-core for how the scoring and conflict detection work.

Last updated on