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| Option | Description |
|---|---|
--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-cleanup | Run AI dependency analysis on the manifest |
--no-ai-cleanup | Skip AI analysis even when features.ai is on |
--print | Print to stdout instead of writing a file |
Output filename convention:
| Scenario | Output path |
|---|---|
No --name | manifest/release/preview-package.xml |
--name 1.2.0 (all packages) | manifest/release/rl-1.2.0-package.xml |
--name 1.2.0 --package feature-a | manifest/release/rl-1.2.0-feature-a-package.xml |
With manifestLayout: subpath | manifest/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 writingWhat 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| Option | Description |
|---|---|
--org <alias> | Target org (defaults to config.defaultOrg) |
--json | Structured 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| Option | Description |
|---|---|
--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
| Question | Tool |
|---|---|
| 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| Option | Description |
|---|---|
--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 / Option | Description |
|---|---|
<name> | Developer name of the component to resolve |
--type <MetadataType> | Metadata type to resolve (default ApexClass) |
--org <alias> | Target org (defaults to config.defaultOrg) |
--json | Structured 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 referenceMISSING(a real gap the API doesn’t record) orconfirmed(already known to the API).--jsonemits 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)"| Subcommand | Description |
|---|---|
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"| Option | Description |
|---|---|
--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-commit | Retrieve only; leave changes uncommitted for review |
--no-deploy | Retrieve and commit, but do not deploy to a target |
--execute | Perform a real deploy to the target (default is validate-only) |
--skip-preflight | Skip preflight checks before deploying |
--json | Structured 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| Option | Description |
|---|---|
--org <alias> | Org alias (defaults to config.defaultOrg) |
--output <file> | Write the report to this path (default logs/flow-scan-latest.json) |
--json | Emit 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| Option | Description |
|---|---|
--org <alias> | Org alias (defaults to config.defaultOrg) |
--output <file> | Write the report to this path (default logs/flow-conflicts-latest.json) |
--json | Emit 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.