Part of the Salesforce Admin Git & sf CLI series. Articles are standalone; read in any order. Foundation: Why Version Control · Environment Setup · Connect Your Org
TL;DR
- Make your change in the sandbox, then retrieve with
sf project retrieve start --source-dir force-app --target-org sandbox-dev --source-dir force-appscopes the retrieve to files already tracked inside that directory. It does not pull everything in the org- Use
sf project retrieve preview --target-org sandbox-devfirst to see exactly what will change before any files are written - Stage only
force-app/in git. Nevergit add ., which can accidentally include unrelated org metadata - Open a merge request, get it reviewed, then deploy to production with
sf project deploy start
What You'll Learn
- What an org-dependent unlocked package is and how it differs from a managed package
- How to configure
sfdx-project.jsonfor an org-dependent package - How to preview and retrieve your sandbox changes with sf CLI
- How to commit only package source files and open a merge request
- What happens after the MR is merged and how to deploy to production
- How to handle org-specific metadata (Custom Metadata records, Custom Settings)
- What you can and cannot retrieve from managed packages
- Troubleshooting common retrieve and deploy errors
The Problem
You've made changes to your Salesforce org (updated a validation rule, tweaked a Flow, added a field) and now you need those changes to go through a proper review process before landing in production. If your project uses an org-dependent unlocked package, the path from "I changed it in the sandbox" to "it's reviewed, merged, and deployed" is not immediately obvious.
Common Questions This Article Answers:
- How do I pull sandbox changes into my local project?
- What does
--source-dir force-appactually do? Does it retrieve everything in the org? - Why should I use a branch instead of committing to main?
- What do I stage and what do I leave out?
- What does the reviewer do with my merge request?
- How do I deploy to production after the merge?
Quick Answer
If you already know the concepts and just need the command sequence:
# Preview what would be retrieved
sf project retrieve preview --target-org sandbox-dev
# Retrieve package source
sf project retrieve start --source-dir force-app --target-org sandbox-dev
# Review changes
git status
git diff
# Branch, stage, commit, push
git checkout -b feature/my-change
git add force-app/
git commit -m "feat: describe what you changed"
git push origin feature/my-change
Then open a merge request in your git provider (GitHub, GitLab, Bitbucket). After approval and merge, deploy:
git checkout main && git pull origin main
sf project deploy start --source-dir force-app --target-org prod
The sections below explain every step in detail.
What Is an Org-Dependent Package?
An org-dependent unlocked package is a type of Salesforce second-generation package (2GP) designed for metadata that depends on the specific org it lives in. Standard unlocked packages are self-contained and can be installed in any org; org-dependent packages can reference unpackaged metadata: things like custom fields from a managed package already installed in the org, multi-currency settings, or other configuration that is unique to that org.
The key practical difference for your workflow:
| Package type | Can you edit directly in the org? | Can you retrieve edits with sf CLI? | Installable in any org? |
|---|---|---|---|
| Org-dependent unlocked | Yes | Yes | No (this org only) |
| Standard unlocked | Yes (in scratch org) | Yes | Yes |
| Managed (ISV) | Only your customisations | Only your customisations | Yes |
Org-dependent vs managed packages: the crucial distinction:
- Org-dependent unlocked packages contain your metadata. You own it. You can edit it, retrieve it, deploy it.
- Managed packages (ISV products from AppExchange) contain vendor metadata. The namespace-prefixed components are locked and read-only. You can retrieve managed package components to inspect them, but you cannot modify and redeploy them. Only your customisations of managed package behaviour (such as custom fields you added to a managed object) are yours to retrieve and commit.
This article is entirely about your own org-dependent unlocked package. The Managed Package Caveat section covers the edge cases.
Setting Up sfdx-project.json
Your project needs a sfdx-project.json file in the root of your git repository. This file tells sf CLI where your package source lives and what the package is called.
Check your current file:
cat sfdx-project.json
For an org-dependent package, a minimal working configuration looks like this:
{
"packageDirectories": [
{
"path": "force-app",
"default": true,
"package": "My Package",
"versionName": "ver 1.0",
"versionNumber": "1.0.0.NEXT"
}
],
"namespace": "",
"sfdcLoginUrl": "https://login.salesforce.com",
"sourceApiVersion": "59.0"
}
Key fields:
path: the local directory that contains your package source. This is the directory you will pass to--source-dirin retrieve and deploy commands. All files inside this path belong to the package.package: the name of your package as registered in the Salesforce Dev Hub.versionNumber: current version.NEXTis a placeholder that auto-increments when you create a new package version.namespace: leave blank for org-dependent packages. A namespace locks you to ISV packaging.sourceApiVersion: the API version sf CLI uses when converting metadata. Match this to your org's API version or a recent release.
If your project has multiple package directories (for example a shared library and an org-specific layer), each gets its own entry in packageDirectories. The "default": true flag marks which directory sf project retrieve start targets by default when you do not specify --source-dir.
Step 1: Make Your Changes in the Sandbox
Log in to your sandbox and make your changes in the Salesforce UI as you normally would: update a validation rule, edit a Flow in Flow Builder, add a custom field, or adjust a picklist.
There is nothing special about this step. Org-dependent packages are built on the assumption that admins and developers work directly in sandbox orgs. The retrieve step (Step 3) is what pulls those changes into your local project.
One thing to confirm before you proceed: make sure the metadata you changed is inside your package's source directory scope. If your package covers force-app/main/default/objects/MyObj__c and you changed a validation rule on MyObj__c, that change will be captured. If you changed something on a standard object or a managed package object that is not in your package scope, it will not appear in the retrieve.
Step 2: See What Would Be Retrieved (Preview)
Before pulling anything down, run the preview command. It queries the org and shows you exactly what would change locally. No files are written.
sf project retrieve preview --target-org sandbox-dev
The output is a table with three columns:
- State: whether the component is new locally, changed remotely, or in conflict
- Full Name: the API name of the component
- Type: the metadata type (CustomObject, Flow, ValidationRule, etc.)
Row colours (in a terminal that supports them):
- Green: remote change, not yet local
- Blue: local-only, not in the org
- Yellow: conflict (differs in both places)
Use the preview to confirm that the components you changed are in the list, and that nothing unexpected is about to be overwritten. If you see components you did not touch, investigate before retrieving. Someone else may have made changes, or you may have sandbox drift from a previous retrieve.
Non-source-tracking sandboxes: Developer sandboxes and scratch orgs have source tracking enabled by default, which is what powers the preview. Full sandboxes and production orgs typically do not have source tracking. If you are working against a non-tracked org, the preview will not show remote-only changes. Use
--metadataor--source-dirflags explicitly and rely ongit diffafter the retrieve to see what changed.
Step 3: Retrieve Your Changes
There are two ways to retrieve. Choose the one that fits your situation.
Option A: Retrieve the full package source directory
sf project retrieve start --source-dir force-app --target-org sandbox-dev
This is the standard approach. The --source-dir force-app flag tells sf CLI to scope the retrieve to components already tracked inside the force-app directory. It does not pull down everything in the org. It is not a full org backup. It retrieves only the components whose type and API name match files that already exist under force-app/.
If a component exists in the org but there is no corresponding file under force-app/, it will not be retrieved. To add new components to your package, you first need to create the placeholder file structure locally (or add them explicitly with --metadata).
Option B: Retrieve specific changed components
sf project retrieve start --metadata "CustomObject:MyObj__c" "Flow:My_Updated_Flow" --target-org sandbox-dev
Use this when you know exactly what you changed and want to be precise. You can pass multiple --metadata values separated by spaces. This approach works even if the target org does not have source tracking.
Useful when:
- You want to verify you are only pulling the components you actually touched
- You are working against a full sandbox (no source tracking)
- You want to avoid overwriting local uncommitted edits to other components
After the retrieve completes, sf CLI prints a summary of what it wrote. If nothing was written, the org and local files are already in sync.
Step 4: See What Changed (git diff)
After retrieving, check what actually changed on disk:
git status
This shows which files were modified, added, or deleted by the retrieve. You should see only files under force-app/. If you see files outside that directory, something unexpected happened (possibly a retrieve that went broader than intended).
Then look at the actual content changes:
git diff
This is your review step before committing. Read through the diff and confirm:
- The validation rule logic changed in the way you intended
- The Flow XML reflects the steps you added or removed
- No unrelated configuration has been overwritten
If git diff shows a change you did not make, stop and investigate before proceeding. Another team member may have changed the same component in the sandbox, or a previous uncommitted retrieve may have left stale local changes.
Step 5: Create a Branch
Never commit directly to main. Work on a feature branch so your changes can be reviewed before they land:
git checkout -b feature/update-myobj-validation
Branch naming conventions vary by team. Common patterns:
feature/description-of-changefix/bug-descriptionchore/maintenance-task
Pick whatever convention your team uses. What matters is that it is not main.
Step 6: Stage Only Package Source Files
This is the most common mistake in Salesforce git workflows: running git add . and accidentally staging files that should not be committed.
Do this:
git add force-app/
Do not do this:
# Dangerous — stages everything including unrelated org metadata, temp files, etc.
git add .
Why does this matter? When you run sf project retrieve start, sf CLI writes files into your project's source directories. If you have previously run broad retrieves without committing, your working tree may contain uncommitted metadata from other parts of the org: metadata that is not part of your package and should not go into this commit. Staging only force-app/ ensures you commit exactly what belongs in your package.
After staging, verify what is included:
git status
The output should show only force-app/ paths under "Changes to be committed." If anything else appears, remove it from staging with git restore --staged <file>.
Step 7: Commit and Push
Commit with a clear, descriptive message:
git commit -m "feat: update MyObj__c validation rule and My_Updated_Flow"
Commit message conventions that work well for Salesforce changes:
feat:new functionality or configurationfix:correcting a bug or incorrect settingchore:housekeeping, metadata cleanuprefactor:restructuring without behaviour change
Push the branch to the remote:
git push origin feature/update-myobj-validation
If this is the first push for this branch, git may prompt you to set the upstream. Accept the suggestion or run:
git push --set-upstream origin feature/update-myobj-validation
Step 8: Create the Merge Request
Open your git provider (GitHub, GitLab, Bitbucket) and create a merge request (or pull request) from your feature branch into main.
A good merge request description gives the reviewer everything they need to understand and test the change without going to the org. Use a template like this:
## What changed
- Updated validation rule on MyObj__c to require Phone when Status = Active
- Added email notification step to My_Updated_Flow
## How to test
1. Log into [sandbox name] at https://[sandbox-url].sandbox.my.salesforce.com
2. Create a MyObj__c record with Status = Active and no Phone
3. Verify the validation error appears
4. Change Status to Inactive: verify record saves without Phone
## Deployment notes
- No test class changes required (declarative only)
- Deploy to UAT sandbox before production
What the reviewer is looking for:
- The diff: does the XML change match what is described in the MR? This is where your clear commit message and scoped staging pay off: the diff only shows what you changed.
- The test steps: can they verify the behaviour in the sandbox?
- Deployment notes: anything that needs to happen before or after deploying to production
Once approved, merge the MR into main. Most teams use a squash merge for Salesforce metadata to keep a clean linear history.
Step 9: After the Merge: Deploy to Production
After the merge, your main branch has the reviewed and approved changes. Now deploy them to production.
# Pull the latest main
git checkout main
git pull origin main
# Dry run first — see what would be deployed without actually deploying
sf project deploy start --source-dir force-app --target-org prod --dry-run
# If the dry run looks correct, deploy for real
sf project deploy start --source-dir force-app --target-org prod
The --dry-run flag runs the deployment validation without committing changes to the org. It checks that the metadata is valid and that there are no missing dependencies. Always run the dry-run first. It catches problems before they touch production.
After deployment, verify in the production org that the validation rule or Flow behaves as expected.
Teams with CI/CD pipelines: If your repository has a CI/CD integration (GitHub Actions, GitLab CI, Bitbucket Pipelines, Copado, Gearset), the deployment to production may happen automatically when main is updated. Check your team's workflow. In that case, you do not need to run the deploy command manually; the pipeline handles it.
The Full Command Sequence
Here is every command from start to finish:
# ── SETUP: Verify sfdx-project.json is configured ─────────────────────
cat sfdx-project.json
# ── STEP 2: See what's in scope before retrieving ────────────────────
sf project retrieve preview --target-org sandbox-dev
# ── STEP 3: Retrieve changed components ──────────────────────────────
# Option A: Retrieve the full package source directory
sf project retrieve start --source-dir force-app --target-org sandbox-dev
# Option B: Retrieve specific changed components
sf project retrieve start --metadata "CustomObject:MyObj__c" "Flow:My_Updated_Flow" --target-org sandbox-dev
# ── STEP 4: See what changed ─────────────────────────────────────────
git status
git diff
# ── STEP 5: Create a branch ───────────────────────────────────────────
git checkout -b feature/update-myobj-validation
# ── STEP 6: Stage ONLY package source files ──────────────────────────
git add force-app/
# Do NOT: git add . (stages unrelated org metadata)
# ── STEP 7: Commit ───────────────────────────────────────────────────
git status
git commit -m "feat: update MyObj__c validation rule and My_Updated_Flow"
# ── STEP 8: Push ──────────────────────────────────────────────────────
git push origin feature/update-myobj-validation
# ── AFTER MERGE: Deploy to production ────────────────────────────────
git checkout main
git pull origin main
sf project deploy start --source-dir force-app --target-org prod --dry-run
sf project deploy start --source-dir force-app --target-org prod
Handling Org-Specific Metadata (Custom Metadata Records, Custom Settings)
Some metadata types hold values that differ between environments and require special handling.
Custom Metadata Records (CustomMetadata)
Custom Metadata records are full metadata. They are retrieved and deployed just like objects or validation rules. They show up under force-app/main/default/customMetadata/ after a retrieve.
The challenge: the values inside Custom Metadata records may be environment-specific. A Custom Metadata record that holds an API endpoint URL might point to a staging endpoint in your sandbox and a live endpoint in production.
Options:
- Commit the sandbox values and update manually in production: simplest approach, acceptable for small teams.
- Use Custom Metadata record overrides in the deployment pipeline: some CI/CD tools support replacing field values during deployment.
- Store environment-specific records in separate branches or deployment profiles: more complex, appropriate for regulated environments.
Custom Settings
Custom Settings (Hierarchy or List type) are not metadata. They are data. sf project retrieve start will not retrieve Custom Setting values. They need to be managed through:
- Data deployment tools (Salesforce Data Loader, Copado, dataloader.io)
- Setup → Custom Settings → Manage, then re-entering values in each environment
- A setup script using Salesforce Apex or a data loader CSV
If your package depends on Custom Setting values being present, document this in your MR deployment notes so the reviewer and deployer know to set them manually in production.
Managed Package Caveat (What You Can and Cannot Retrieve)
If your org has managed packages installed (for example, Salesforce CPQ, nCino, Veeva), you will encounter namespace-prefixed metadata in your org. It is important to understand the boundary.
What you CAN retrieve and commit:
- Custom fields you added to managed package objects (e.g., a custom field
My_Field__con a CPQ quote line object) - Validation rules, Flows, and page layouts that you created on managed package objects
- Custom Metadata records that reference managed package fields
- Any metadata you created that happens to live on a managed package object
What you CANNOT retrieve, modify, or deploy:
- Namespace-prefixed components (e.g.,
SBQQ__Quote__c,SBQQ__QuoteLine__c) - Page layouts that ship with the managed package
- Flows or Apex classes that have a namespace prefix
- Permission sets, profiles, or record types that are part of the managed package
When you retrieve, sf CLI may pull down namespace-prefixed files depending on what is in your force-app directory. Do not commit namespace-prefixed metadata as if it were yours. It is vendor code, it is locked, and committing it creates a false impression that you can deploy it. If you see namespace-prefixed files appearing in git status, add them to your .gitignore or remove them from force-app/ immediately.
A safe pattern for mixed environments: keep your force-app/ directory strictly to your own metadata and never let managed package files land there. If a managed component sneaks in during a retrieve, delete it locally before staging.
Troubleshooting
"ERROR running project retrieve start: No components were returned from the retrieve"
The components in the org match what you already have locally. There is nothing new to retrieve. Confirm your sandbox change was saved correctly in the Salesforce UI. Also check that the metadata type you changed is supported by the API version in sfdx-project.json.
Retrieve completes but git diff shows nothing
Either the change you made in the sandbox is already reflected locally (perhaps you retrieved earlier without noticing), or the metadata type does not serialise the field you changed into the XML. Some picklist field changes, for example, may not appear until you also retrieve the parent object's field definition.
"The metadata component type is not available in API version X"
Raise the sourceApiVersion in sfdx-project.json to match your org's API version. Find your org's version in Setup → Company Information → Salesforce.com API Version.
"git push" rejected: "Updates were rejected because the tip of your current branch is behind its remote counterpart"
Someone else pushed to the same branch. Run git pull origin feature/your-branch first to merge their changes, then push again.
sf project deploy start fails with "UNKNOWN_EXCEPTION"
This almost always means a dependency is missing in the target org. The deploy sequence matters. If your Flow references a custom field that is not yet in production, the Flow deploy will fail. Check the full error message for the specific component that could not resolve.
"Conflict detected on retrieve"
This happens when a component differs between your local file and the org. The preview command shows conflicts in yellow before you retrieve. To resolve: decide which version is correct, either discard your local change (git restore <file>) and retrieve from the org, or keep your local version and push it to the org with a deploy.
sf project retrieve preview shows components you did not touch
In a shared sandbox, your colleagues may have made changes. This is expected behaviour. Use --metadata to retrieve only the specific components you changed, rather than doing a broad --source-dir retrieve, to avoid pulling in changes that belong to someone else's MR.
Frequently Asked Questions
Q: Does --source-dir force-app retrieve everything in the org, or only what's already in force-app?
Only what is already in force-app. The --source-dir flag uses the files already on disk as a manifest. sf CLI reads the directory, builds a list of component types and API names from the existing files, then retrieves only those components from the org. If a component exists in the org but has no corresponding file under force-app/, it will not be retrieved. This is by design: your package scope is defined by what is already in the directory.
To add a new component to your package, either create the placeholder file manually and then retrieve, or use --metadata "Type:ApiName" to retrieve it explicitly, which will write the file under force-app/ for the first time.
Q: What is the correct sf CLI v2 command to list package versions?
sf package version list --target-dev-hub DevHub
To filter by a specific package:
sf package version list --packages "My Package" --target-dev-hub DevHub
To show only released versions:
sf package version list --released --target-dev-hub DevHub
Note that --target-dev-hub points to your Dev Hub org, not the sandbox. Package version management always goes through the Dev Hub.
Q: Should I create a new package version every time I make a change?
Not necessarily. For an org-dependent package used internally, many teams skip package version creation for day-to-day changes and simply deploy source directly with sf project deploy start. Package version creation (sf package version create) is valuable when you want a formal release artefact, a point-in-time snapshot, or when you need to install the package in another org.
If your team does want to create a version:
sf package version create --package "My Package" --installation-key-bypass --wait 10 --target-dev-hub DevHub
Q: Can two developers work on the same org-dependent package at the same time?
Yes, but you need to manage the shared sandbox carefully. The typical pattern is:
- Both developers work in the same sandbox (or separate developer sandboxes refreshed from production)
- Each retrieves and branches independently
- Merge requests are reviewed and merged in sequence
- Both developers pull
mainand sync their local projects before starting new work
Conflicts happen when two people change the same component in the same sandbox without coordination. Clear communication about who owns which components on a given day reduces conflicts significantly.
Q: What if I want to undo a change I already committed and pushed?
Create a new commit that reverts the change. Do not force-push or rewrite history on a shared branch. In git:
git revert <commit-hash>
git push origin feature/your-branch
For a revert of a metadata change in Salesforce specifically, you also need to deploy the reverted source back to the org:
sf project deploy start --source-dir force-app --target-org sandbox-dev
Q: My team doesn't have a Dev Hub set up. Can I still use this workflow?
Yes. The core workflow (retrieve, branch, stage, commit, push, MR) works entirely without a Dev Hub. The Dev Hub is only required for package version creation and scratch org management. For org-dependent packages where you are not creating formal package versions, you can retrieve from and deploy to sandboxes using only --target-org, with no Dev Hub involvement.
Key Takeaways
--source-dir force-appscopes, it does not expand: the retrieve is bounded by files already in that directory. New components need an explicit--metadataflag the first time.- Preview before you retrieve:
sf project retrieve previewis free and shows you exactly what will change. Make it a habit. - Branch for everything:
mainis your production-ready branch. Every change goes through a feature branch and MR, even single-field changes. - Stage only
force-app/: nevergit add .. Scope your commit to your package source. - Org-dependent = yours to own: you can retrieve, modify, and deploy it. Managed package components are vendor code and belong to them.
- Custom Settings are data, not metadata: they will not appear in a retrieve. Document them in MR deployment notes.
- Dry-run before production deploys:
--dry-runvalidates without touching the org. Always run it first.
What's Next?
This article builds on the foundations covered earlier in the series:
- Article 10: Git Basics for Salesforce Admins: if the branching and staging steps felt unfamiliar, start here for a grounded explanation of git concepts in a Salesforce context
- Article 11: Deploying Metadata Back to Salesforce: deeper coverage of
sf project deploy start, destructive changes, deployment ordering, and handling test failures
For CI/CD automation of the deploy step, the Salesforce DX Developer Guide covers pipeline setup for GitHub Actions and GitLab CI with examples for both scratch org and sandbox-based flows.
Resources & References
- Salesforce DX Developer Guide: Create Org-Dependent Unlocked Packages
- Salesforce DX Developer Guide: Project Configuration File for Unlocked Packages
- sf CLI Command Reference: project retrieve start
- sf CLI Command Reference: project deploy start
- GitHub: salesforcecli/plugin-deploy-retrieve
- Salesforce Metadata Coverage Report
Tags: #salesforce #sfcli #unlocked-packages #git #devops #admin