The sfdx CLI reached end-of-life in December 2024. If you’re still running sfdx force:source:deploy in your pipelines, you’re on borrowed time - the binary still works, but it’s no longer receiving security patches or feature updates. The unified sf CLI has been the recommended path since Salesforce CLI v2, and by now there’s no good reason to delay the migration.
This post covers the full command mapping, the flag style changes, plugin system differences, and how to update GitHub Actions and other CI configurations.
Why sf Instead of sfdx
The sf CLI isn’t just a rename. It was a ground-up redesign with a few concrete improvements:
- Unified command surface -
sfcovers Sales Cloud, Service Cloud, Marketing Cloud, and Commerce Cloud under one binary - Topic-based commands -
sf project deploy startreads like English instead ofsfdx force:source:deploy - Named flags everywhere - no more positional arguments that change meaning depending on context
- Better error messages - structured output and clearer failure modes
- Plugin architecture - plugins are now first-class and the ecosystem has matured
Command Mapping Reference
The table below covers the commands you’re most likely running in CI and day-to-day development.
| sfdx (deprecated) | sf (current) |
|---|---|
sfdx force:source:deploy -p force-app | sf project deploy start --source-dir force-app |
sfdx force:source:retrieve -m ApexClass | sf project retrieve start --metadata ApexClass |
sfdx force:source:push | sf project deploy start |
sfdx force:source:pull | sf project retrieve start |
sfdx force:org:create -f config/project-scratch-def.json | sf org create scratch --definition-file config/project-scratch-def.json |
sfdx force:org:delete -u MyScratch | sf org delete scratch --target-org MyScratch |
sfdx force:org:list | sf org list |
sfdx force:org:open | sf org open |
sfdx force:apex:test:run -l RunLocalTests | sf apex run test --test-level RunLocalTests |
sfdx force:apex:execute -f scripts/apex/setup.apex | sf apex run --file scripts/apex/setup.apex |
sfdx force:data:soql:query -q "SELECT Id FROM Account" | sf data query --query "SELECT Id FROM Account" |
sfdx force:data:record:create -s Account -v "Name=Acme" | sf data create record --sobject Account --values "Name=Acme" |
sfdx force:auth:jwt:grant --clientid ... --jwtkeyfile ... | sf org login jwt --client-id ... --jwt-key-file ... |
sfdx force:auth:web:login | sf org login web |
sfdx force:package:create | sf package create |
sfdx force:package:version:create | sf package version create |
sfdx force:limits:api:display | sf limits api display |
Flag Style Changes
The most disorienting part of the migration isn’t the command names - it’s the flag conventions.
--targetusername is now --target-org
# old
sfdx force:source:deploy -p force-app -u MyOrg
# new
sf project deploy start --source-dir force-app --target-org MyOrg
--targetdevhubusername is now --target-dev-hub
# old
sfdx force:org:create -f config/scratch-def.json -v DevHub
# new
sf org create scratch --definition-file config/scratch-def.json --target-dev-hub DevHub
Short flags still work for common options, but don’t rely on them in scripts. The named long-form flags are stable across versions; short flags occasionally change.
Plugin System Changes
Under sfdx, plugins were managed with sfdx plugins:install. Under sf, it’s simply:
sf plugins install @salesforce/plugin-packaging
sf plugins list
sf plugins update
The plugin namespace also changed. If you were using sfdx-git-delta for delta deployments, the updated version is invoked as:
sf sgd source delta --to HEAD --from HEAD~1 --output ./output
Check your package.json devDependencies for any @salesforce/sfdx-* plugins - most have been republished under @salesforce/plugin-*.
Updating GitHub Actions
Here’s a before/after for a typical CI deployment workflow:
# Before - using sfdx (deprecated)
- name: Deploy to Staging
run: |
sfdx force:auth:jwt:grant \
--clientid ${{ secrets.SF_CLIENT_ID }} \
--jwtkeyfile server.key \
--username ${{ secrets.SF_USERNAME }} \
--setdefaultdevhubusername
sfdx force:source:deploy \
-p force-app \
-u ${{ secrets.SF_USERNAME }} \
-l RunLocalTests \
--wait 30
# After - using sf CLI
- name: Install Salesforce CLI
run: npm install --global @salesforce/cli@latest
- name: Authenticate
run: |
sf org login jwt \
--client-id ${{ secrets.SF_CLIENT_ID }} \
--jwt-key-file server.key \
--username ${{ secrets.SF_USERNAME }} \
--set-default
- name: Deploy to Staging
run: |
sf project deploy start \
--source-dir force-app \
--target-org ${{ secrets.SF_USERNAME }} \
--test-level RunLocalTests \
--wait 30
Setting a Default Org
With sfdx, you’d use --setdefaultusername. With sf, the flag is --set-default, and it applies to the current project directory (tracked in .sf/config.json):
sf config set target-org MyOrg --global
sf config set target-dev-hub DevHub --global
The --global flag sets it across all projects. Without it, the config is scoped to the current project directory.
Verifying the Migration
Run this to confirm you’re on the right binary and version:
sf --version
# @salesforce/cli/2.x.x darwin-arm64 node-v20.x.x
which sf
# /usr/local/bin/sf
# Confirm sfdx is no longer aliased to anything unexpected
which sfdx
# sfdx not found ← ideal outcome
If sfdx still resolves, it may be aliased to the new sf binary (Salesforce set this up as a compatibility shim for a while). Verify with sfdx --version - if it returns a @salesforce/cli version string, the shim is in place and your old scripts will mostly still work, but you should migrate anyway for maintainability.
The Bottom Line
The migration is tedious but mechanical. The command mappings are consistent, the flag renames follow a clear pattern, and the new CLI is genuinely better - faster startup, cleaner output, and a more sensible command hierarchy. Budget an afternoon to update your CI workflows and local aliases. There’s no architectural change required; it’s a find-and-replace job with a few edge cases to watch for.