sf-project-structure
Skill2 starsChanged 2 months agoMITView on GitHub
Structuring a Salesforce DX repository — the full `sfdx-project.json` schema (packageDirectories, default, sourceApiVersion, namespace, sfdcLoginUrl, plugins, packageAliases, replacements, sourceBehaviorOptions, oauthLocalPort, pushPackageDirectoriesSequentially), source-format directory layout for every metadata type (`objects/<Obj>/fields`, `classes`, `triggers`, `lwc`, `aura`, `flows`, `permissionsets`, `layouts`, `flexipages`, `staticresources`, `labels`, `settings`), decomposed metadata types, multiple package directories and modularisation, `.forceignore` semantics and precedence, `package.xml` and `destructiveChangesPre/Post.xml` manifests with `sf project generate manifest`, the source-tracking model (`sf project deploy|retrieve start|preview`, `delete source`, `reset tracking`, `--ignore-conflicts`), source/metadata format conversion, team repo conventions, and the `.vibeforce/` layout. Use this skill when creating or reorganising a DX project, deciding where a metadata file belongs, writing or auditing `.forceignore` or a manifest, resolving source-tracking conflicts, splitting a monolith into package directories, or reviewing metadata XML diffs.
In this repository34 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- sf-agentforce-developmentSkill
- sf-apex-developmentSkill
- sf-apex-testingSkill
- sf-async-apex-patternsSkill
- sf-cli-operationsSkill
- sf-code-analyzer-qualitySkill
- sf-data-cloudSkill
- sf-data-managementSkill
- sf-debugging-logsSkill
- sf-deployment-strategiesSkill
- sf-fflib-domain-service-uowSkill
- sf-fflib-foundationsSkill
- sf-fflib-operationsSkill
- sf-fflib-selector-layerSkill
- sf-fflib-testingSkill
- sf-flow-automationSkill
- sf-governor-limitsSkill
- sf-integration-patternsSkill
- sf-local-developmentSkill
- sf-lwc-developmentSkill
- sf-lwc-jest-testingSkill
- sf-minimal-changeSkill
- sf-org-security-auditSkill
- sf-packaging-releaseSkill
- sf-post-deploy-verificationSkill
- sf-process-builder-migrationSkill
- sf-scratch-orgs-sandboxesSkill
- sf-security-modelSkill
- sf-setup-automationSkill
- sf-soql-sosl-optimizationSkill
- sf-technical-debt-auditSkill
- sf-ui-test-automationSkill
- sf-workflow-orchestrationSkill
The file
We split this skill into the rules it gives an agent
1 rule. About 0 are common, 0 are distinctive, and 1 is very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
1
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 101
Keep sourceApiVersion equal to the vibe-force apiVersion (config/vibe-force.defaults.json) and to org-api-version.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Salesforce DX Project Structure
A directory is a Salesforce DX project because it contains sfdx-project.json. That file tells the
CLI which directories hold source (packageDirectories), what metadata shape to speak
(sourceApiVersion), and where to log in (sfdcLoginUrl).
When to use
| Situation | Action |
| Creating a repo or adding a package directory | sfdx-project.json schema + multiple-package-directory rules below |
| "Where does this metadata file go?" | references/metadata-directory-map.md |
| Source keeps getting deployed that should not be | .forceignore precedence, pattern 4, and sf project list ignored |
| Building a deployment or deletion manifest | sf project generate manifest, references/forceignore-and-manifests.md |
sf project deploy start reports conflicts | Pattern 5 — resolve, then deploy or retrieve with --ignore-conflicts |
| Importing an existing metadata-format repo | sf project convert mdapi (pattern 6) |
| Setting team conventions, review rules, branch model | references/repo-conventions.md |
Related skills: sf-cli-operations (flags, config, API version resolution),
sf-deployment-strategies (deploy/validate/quick-deploy mechanics and test levels),
sf-scratch-orgs-sandboxes (org lifecycle, source tracking availability),
sf-packaging-release (package directories becoming 2GP/unlocked packages),
sf-security-model (profile vs permission-set policy).
Canonical layout
my-project/
├── sfdx-project.json project definition (commit)
├── .forceignore what the project commands exclude (commit)
├── .gitignore what git excludes (commit)
├── package.json LWC Jest, eslint, prettier dev deps (commit)
├── jest.config.js sfdx-lwc-jest config (commit)
├── config/
│ ├── project-scratch-def.json scratch org definition (commit)
│ └── dev-sandbox-def.json sandbox definition (commit)
├── manifest/
│ ├── package.xml deployment manifest (commit)
│ └── destructiveChangesPost.xml (commit when a release deletes metadata)
├── force-app/ default package directory
│ └── main/default/
│ ├── classes/ ApexClass: Foo.cls + Foo.cls-meta.xml
│ ├── triggers/ ApexTrigger: Foo.trigger + Foo.trigger-meta.xml
│ ├── lwc/<bundle>/ LightningComponentBundle (strict directory name)
│ ├── aura/<bundle>/ AuraDefinitionBundle (strict directory name)
│ ├── objects/<Object>/ decomposed: fields/, recordTypes/, listViews/, ...
│ ├── flows/ Flow: Foo.flow-meta.xml
│ ├── permissionsets/ PermissionSet: Foo.permissionset-meta.xml
│ ├── layouts/ Layout: Object-Layout Name.layout-meta.xml
│ ├── flexipages/ FlexiPage: Foo.flexipage-meta.xml
│ ├── labels/ CustomLabels: CustomLabels.labels-meta.xml
│ ├── staticresources/ StaticResource: Foo.resource-meta.xml + payload
│ └── settings/ Settings: Case.settings-meta.xml
├── .vibeforce/ vibe-force config + state + reports
├── .sf/ .sfdx/ .localdevserver/ CLI-generated; never edit, never commit
└── node_modules/ never commitsfdx-project.json quick reference
| Property | Required | Purpose |
packageDirectories | yes | Array of { "path": "...", "default": true }. Relative paths only. Exactly one default; with a single entry it is implied |
sourceApiVersion | recommended | API version the metadata conforms to — becomes <version> in the generated manifest. vibe-force sets 67.0 |
namespace | optional | Namespace applied to scratch orgs created from this project; must be linked to the Dev Hub |
sfdcLoginUrl | optional | Login URL for sf org login *; default https://login.salesforce.com. Use the My Domain form for sandboxes |
oauthLocalPort | optional | OAuth callback port; default 1717. Must match the connected app callback URL |
packageAliases | optional | CLI-maintained map of readable names to 0Ho/04t package ids |
plugins | optional | Per-plugin settings you want version-controlled |
replacements | optional | Deploy-time string/regex substitution driven by env vars or files |
sourceBehaviorOptions | optional (Beta) | Extra decomposed types. Never hand-edit — run sf project convert source-behavior |
pushPackageDirectoriesSequentially | deprecated | Only affected the retired force:source:push; has no effect on sf project deploy start |
Full property tables, packaging fields, and replacements variants:
references/sfdx-project-schema.md.
Core patterns
1. Start the project, pin the API version
sf template generate project --name my-project --default-package-dir force-app \
--manifest --api-version 67.0
cd my-project{
"packageDirectories": [{ "path": "force-app", "default": true }],
"namespace": "",
"sfdcLoginUrl": "https://login.salesforce.com",
"sourceApiVersion": "67.0"
}Keep sourceApiVersion equal to the vibe-force apiVersion (config/vibe-force.defaults.json) and
to org-api-version; sf doctor runs a sourceApiVersion matches apiVersion check and warns when
they diverge. sourceApiVersion governs the shape of the metadata: deploying a file that contains
a field introduced in a later API version fails if sourceApiVersion is older, and retrieving with
an older value silently drops newer subelements.
2. Model the domain with multiple package directories
{
"packageDirectories": [
{ "path": "force-app", "default": true },
{ "path": "sales-ext" },
{ "path": "service-ext" },
{ "path": "unpackaged" }
],
"sourceApiVersion": "67.0"
}Each directory repeats the standard main/default/... tree, so MyObject__c can live in
force-app while a new field on it lives in sales-ext/main/default/objects/MyObject__c/fields/.
Facts that matter:
- The split is client-side only. Deploying does not associate a directory with a package in the org.
sf project deploy startwith no targeting flag deploys all package directories in one transaction. Order is not guaranteed, so sequence explicitly when you need it:
sf project deploy start --source-dir force-app --target-org vf-dev
sf project deploy start --source-dir sales-ext --source-dir service-ext --target-org vf-dev- The default directory is the retrieve target and the default output directory for conversions.
- These directories are the natural boundary for later unlocked/2GP packages (skill
sf-packaging-release), and the natural ownership boundary for the vibe-force wave-1 agents.
3. Know the source format rules that surprise people
| Rule | Detail |
| Custom objects and custom object translations are always decomposed | objects/<Object>/<Object>.object-meta.xml plus fields/, recordTypes/, listViews/, validationRules/, compactLayouts/, fieldSets/, businessProcesses/, indexes/, sharingReasons/, webLinks/ |
| Optional decomposition is opt-in and beta | CustomLabels, PermissionSet, SharingRules, Workflow, ExternalServiceRegistration via sf project convert source-behavior --behavior <value> |
| Bundles need exact directory names | lwc, aura, staticresources, experiences, documents, objects, objectTranslations, sites, emailservices, waveTemplates, lightningTypes, genAiFunctions, restrictionRules, bots, aiAuthoringBundles are strictDirectoryName types — a bundle in the wrong folder is invisible to the CLI |
Static resources live only in <pkg>/main/default/staticresources | .zip/.jar archives are auto-expanded on retrieve; an archive that exists as a single file stays a single file. New resources use their MIME extension (.gif, .png); legacy .resource files keep that extension |
| Content types have two files | ApexClass, ApexTrigger, ApexPage, ApexComponent, EmailTemplate, StaticResource ship a payload plus -meta.xml. Treat them as a pair in .forceignore and in reviews |
| Names with special characters are URL-encoded on disk | Custom: Marketing Profile becomes Custom%3A Marketing Profile.profile-meta.xml, and .forceignore must use the encoded name |
| Folder-based types nest one level | reports/<Folder>/<Report>.report-meta.xml, same for dashboards, documents, email |
4. Write .forceignore deliberately
.forceignore uses .gitignore syntax and is honoured by project deploy start,
project retrieve start, project convert source, and project delete source.
# Never deploy tooling and local artifacts
**/jsconfig.json
**/.eslintrc.json
**/__tests__/**
.sf/
.sfdx/
.localdevserver/
# Org-owned, human-managed metadata
**/profiles/**
**/*.settings-meta.xml
**/labels/CustomLabels.labels-meta.xml
# A specific component (MetadataWithContent needs both files, or a trailing *)
force-app/main/default/classes/LegacyBatch.cls*
# A whole bundle
**/lwc/legacyComponentPrecedence: when the commands decide whether to exclude a file they walk up from that file until
they find the first .forceignore, consult it, and stop. A .forceignore inside sales-ext/
therefore fully overrides the project-root one for files under sales-ext/. Put the primary file at
the project root; a per-package file is legitimate but must be documented.
Independently of .forceignore, the project commands always skip dot-files and dot-directories,
files ending in .dup, package2-descriptor.json, and package2-manifest.json.
Verify, never guess:
sf project list ignored
sf project list ignored --source-dir force-app/main/default/classes
sf project deploy preview --target-org vf-dev # shows Will Deploy / Will Delete / Conflicts / Ignoredvf-check static fails when a changed file is silently ignored by .forceignore but expected to be
part of the story, which is the common cause of "it works in my scratch org but not in UAT".
5. Operate source tracking honestly
| Org type | Source tracking |
| Scratch org | Always available, on by default |
| Developer / Developer Pro sandbox | Available when the production org has source tracking enabled |
| Developer Edition, production, Partial Copy, Full sandbox | Not supported |
sf project deploy preview --target-org vf-dev # local changes, deletions, conflicts, ignored
sf project retrieve preview --target-org vf-dev # remote changes
sf project deploy start --target-org vf-dev # deploy exactly the changed, tracked source
sf project retrieve start --target-org vf-dev # retrieve exactly the remote changes
sf org enable tracking --target-org vf-sandbox
sf org disable tracking --target-org vf-sandbox
sf project reset tracking --target-org vf-dev --revision 12345
sf project delete tracking --target-org vf-dev # forget all local tracking stateConflict resolution: preview reports Conflicts [n]. Resolve the file, then overwrite one side
deliberately and narrowly:
# Keep the local version of one class, then continue normally.
sf project deploy start --metadata ApexClass:WidgetClass --ignore-conflicts --target-org vf-dev
# Or keep the org version of that class.
sf project retrieve start --metadata ApexClass:WidgetClass --ignore-conflicts --target-org vf-dev
sf project deploy start --target-org vf-dev--ignore-conflicts silently discards the other side's work. Scope it to the component you decided
about; never add it to a whole-project command "to make the error go away". vibe-force hooks warn on
--ignore-conflicts in standard mode and block it against a production alias in strict mode.
Deleting metadata needs sf project delete source, which removes it from the org and the local
project; plain file deletion plus deploy start does not delete anything in the org unless the
component is tracked as deleted.
sf project delete source --metadata ApexClass:ObsoleteService --target-org vf-dev --check-only
sf project delete source --metadata ApexClass:ObsoleteService --target-org vf-dev --no-promptPerformance: tracking adds SourceMember queries and polling. A "medium" project is 30+ components
or 50+ tests, "large" is 600+ components or 150+ tests; above that, deploy in smaller sets or tune
SF_SOURCE_TRACKING_BATCH_SIZE (see skill sf-cli-operations).
6. Convert between formats when you inherit a repo
# Metadata format (retrieve output, change-set export) -> source format
sf project convert mdapi --root-dir ./mdapi-out --output-dir force-app
# Source format -> metadata format, for a tool that only speaks Metadata API
sf project convert source --source-dir force-app --output-dir mdapi-package --package-name MyAppFor project convert mdapi, the applicable .forceignore is the one in the metadata retrieve
directory (next to package.xml), not the project root.
7. Keep .vibeforce/ and generated directories straight
| Path | Commit? | Content |
.vibeforce/config.json | yes | Project gates, org aliases, hook mode — reviewed like code |
.vibeforce/state/ownership.json | no | Wave-1 path ownership for the current story |
.vibeforce/state/deploy-jobs.json | no | Validation job ids for quick deploy |
.vibeforce/state/contract.md | optional | Cross-slice contract written in wave 0; commit it when it documents a decision worth keeping |
.vibeforce/reports/*.json | no | Check-runner output |
.sf/, .sfdx/ | no | CLI internals, auth, local config — never edit by hand |
.localdevserver/ | no | Local dev server cache (skill sf-local-development) |
node_modules/, coverage/, *.log | no | Build artifacts |
vf-init seeds a consumer project from this plugin's templates/ directory (project config seed,
formatter and lint configuration, .forceignore/.gitignore fragments) and creates
.vibeforce/; the exact inventory is whatever templates/ in the installed plugin contains.
Anti-patterns
| Anti-pattern | Consequence | Fix |
Two "default": true package directories | The CLI cannot resolve the retrieve/convert target | Exactly one default |
Absolute paths in packageDirectories | Breaks on every other machine and in CI | Relative paths ("force-app", "./force-app") |
Hand-editing sourceBehaviorOptions | Project claims decomposition that the files do not have; deploys fail | sf project convert source-behavior --behavior <value> (--dry-run first) |
| Deleting a metadata file and deploying | Component stays in the org | sf project delete source, or a destructiveChanges*.xml manifest |
--ignore-conflicts on a whole-project deploy | Silently overwrites teammates' org changes | Resolve per component, then deploy |
Committing .sf/ or .sfdx/ | Leaks auth material and machine-local config | Gitignore both; rotate anything already pushed |
| Profiles deployed from source alongside permission sets | Profile XML is org-wide and merge-hostile; diffs are unreviewable | Permission sets + permission set groups; ignore **/profiles/** (skill sf-security-model) |
One giant force-app for a multi-team org | Every story touches the same tree; ownership and packaging are impossible | Package directory per domain (pattern 2) |
sourceApiVersion left at whatever the project template shipped | Newer subelements are dropped on retrieve and rejected on deploy | Pin to the vibe-force apiVersion |
.forceignore entry naming only Foo.cls | Foo.cls-meta.xml still deploys | Foo.cls* or list both files |
Verification
# Project definition parses and resolves the way you think
node -e "const p=require('./sfdx-project.json');console.log(p.sourceApiVersion, p.packageDirectories)"
sf project list ignored
sf project deploy preview --target-org vf-dev
sf project retrieve preview --target-org vf-dev
# Manifest round-trip
sf project generate manifest --source-dir force-app --name manifest/package.xml
sf project deploy start --manifest manifest/package.xml --dry-run --target-org vf-dev
# vibe-force gates
node "$VF_ROOT/scripts/checks/vf-check.mjs" static # format + lint + analyzer on changed files
node "$VF_ROOT/scripts/checks/vf-check.mjs" local # static + jest
node "$VF_ROOT/scripts/checks/vf-check.mjs" deploy-validate --target-org vf-intvf-check format and vf-check lint resolve files through packageDirectories, so a directory
missing from sfdx-project.json is also invisible to the gates — a strong reason to keep the file
accurate.
References
references/sfdx-project-schema.md— every property, packaging fields,replacementsvariants, worked examples.references/metadata-directory-map.md— metadata type → directory → file suffix → notes, including decomposed children and strict-directory bundles.references/forceignore-and-manifests.md—.forceignorepatterns and precedence,package.xml,destructiveChangesPre/Post.xml, wildcard support.references/xml-token-economy.md— measuring, reading and patching metadata XML without loading whole files; thexml-bulk-readguard and/vf-xml.references/repo-conventions.md— branch-per-story, ownership, metadata review checklist, profile policy, monorepo tradeoffs, gitignore companion.- Salesforce DX Project Structure and Source Format: https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_source_file_format.htm
- Salesforce DX Project Configuration: https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_ws_config.htm
- Multiple Package Directories: https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_ws_mpd.htm
- How to Exclude Source When Syncing: https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_exclude_source.htm
- Track Changes Between Your Project and Org: https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_source_tracking.htm
- Metadata API Developer Guide, Deploying and Retrieving Metadata: https://developer.salesforce.com/docs/atlas.en-us.api_meta.meta/api_meta/meta_deploy.htm
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

