Salesforce CLI in the Real World: A Deep Engineering Breakdown of Edition Constraints, Metadata Failures, and API Truth
Salesforce CLI is often described as a developer convenience. In reality, it is the only reliable way to understand how Salesforce behaves under real SaaS integration conditions – especially when metadata, profiles, licenses, and object availability differ across orgs. This article goes beyond the usual “run a few commands” tutorials. It examines the actual failure

Salesforce CLI is often described as a developer convenience.
In reality, it is the only reliable way to understand how Salesforce behaves under real SaaS integration conditions – especially when metadata, profiles, licenses, and object availability differ across orgs.
This article goes beyond the usual “run a few commands” tutorials. It examines the actual failure modes and insights exposed by the terminal session: object shadowing, ConnectedApp collisions, Tooling API metadata drift, edition-based feature gaps, and the mechanics behind user/license mismatches.
The content below reflects the exact engineering patterns required for stable, multi-edition Salesforce integrations.
1. API Truth vs UI Illusion
The UI shows a filtered, permission-adjusted, cached version of Salesforce’s schema.
The CLI exposes what the API actually returns – and these are often not the same.
Example: Opportunity availability
Running:
sf data query -o shq-ent -q "SELECT Id, Name FROM Opportunity LIMIT 1"
can return:
Total number of records retrieved: 0.
This means: object exists, but has no rows.
But an error like:
INVALID_TYPE: sObject type 'Opportunity' is not supported.
means: the object does not exist at all for this combination of edition + license + user.
UI screens can show Opportunities even when the API cannot access them.
This is one of the most common “invisible” failure points for CRM-based SaaS products.
2. Edition Differences Are Not Cosmetic – They Affect Metadata, Objects, and Tooling API Behavior
Scratch orgs revealed visible divergences:
Enterprise Edition
- Supports Opportunity
- Supports Apex
- Supports toolable sObject describes
- Accepts Connected Apps normally
Developer Edition
- Behaves like Enterprise, but ConnectedApp metadata behaves differently
- Tooling API exposes certain fields that Enterprise hides
Platform Edition
- Opportunity often missing
- Certain metadata deployments silently skipped
- VF pages may behave differently
Professional Edition
- API behavior depends on whether API access is purchased
- Many metadata features unavailable
- “Object visible in UI” ≠ “Object usable via API”
These differences cannot be detected reliably through UI.
They must be queried:
sf org display -o <alias>
And verified through SOQL, not assumptions.
3. Connected Apps: Why Deployments Fail Between Orgs
The terminal session exposed a problem most documentation hides:
The consumer key is already taken.
This represents one of the least intuitive metadata constraints:
ConnectedApps cannot share consumer keys across orgs.
Deploying a ConnectedApp from one org into another will fail unless:
- The ConnectedApp is renamed
- The target org has no old versions with the same key
- The metadata uses the correct ConnectedApp type (not the deprecated ConnectedApplication type)
The fix applied:
sf project retrieve start -m "ConnectedApp:SampleHQ_Integration_Flow"
followed by a fresh deployment ensured deterministic behavior.
This is the reason packaged integrations often break when customers modify a Connected App manually.
4. Metadata Drift: Reading Custom Objects Across Generations
Real debugging begins when comparing two object versions.
Example:
sf data query --use-tooling-api \
-q "SELECT Id, DeveloperName, LastModifiedDate
FROM CustomObject WHERE DeveloperName='Sample_Order'"
revealed:
01IG1000005CU5tMAG
01IG1000005CV1xMAG
Two versions of the same object – created minutes apart.
Why this matters
Salesforce can leave behind “ghost” metadata versions after deploy/redeploy cycles, especially in scratch orgs:
- UI will only show the current version
- Tooling API shows all versions
- Fields may duplicate between versions
- Automation referencing the older ID can break silently
Deep integrations must always inspect object IDs with Tooling API instead of trusting one path:
sf data query --use-tooling-api \
-q "SELECT Id, DeveloperName FROM CustomField WHERE TableEnumOrId='01IG1000005CU5tMAG'"
This confirms whether fields drifted, duplicated, or remained consistent.
5. The Hidden Layer: Profiles, UserTypes, and License Behavior
The CLI revealed how profile data queries behave differently between editions.
Incorrect assumption (common mistake)
SELECT UserLicenseId FROM User
Result:
No such column 'UserLicenseId' on entity 'User'
Truth:
User → Profile → UserLicense
not
User → UserLicense
Correct pattern:
sf data query \
-q "SELECT Id, Name, UserLicenseId
FROM Profile
WHERE Id='PROFILE_ID'"
The UserType column (Standard, PowerPartner, etc.) also changes capabilities in non-documented ways.
These details affect:
- SOQL capabilities
- object write access
- automation limits
- API quotas
This is why integrations must validate:
sf data query \
-q "SELECT Profile.Name, UserType FROM User WHERE Username='<token user>'"
instead of assuming the OAuth user is what it appears.
6. Tooling API: The Only Reliable Way to Inspect Custom Metadata
Salesforce’s Metadata API intentionally hides certain fields and older versions.
Tooling API exposes full lineage.
This command:
sf data query --use-tooling-api \
-q "SELECT Id, DeveloperName, Metadata FROM CustomObject WHERE Id='...' "
produced:
No such column 'Metadata'
Why?
Because Metadata is not available on CustomObject in Tooling API – instead it is exposed differently per object type.
The correct approach:
- Use
sobject describe - Use CustomField for field-level diffs
- Use Metadata API resources for deployable files
The reliable workflow:
sf sobject describe -s Sample_Order__c
This reveals:
- all fields
- all relationships
- internal structure not shown in UI
- which features the object supports
This is critical for SaaS systems that dynamically create or map objects.
7. Scratch Org Workflow: The Only Deterministic Testing Strategy
A clean pattern emerged from the session:
1. Generate a proper DX project
sf project generate -n samplehq-salesforce
2. Set global defaults
sf config set target-dev-hub=<email> --global
3. Create edition-specific scratch orgs
sf org create scratch -f config/enterprise.json -a shq-ent
sf org create scratch -f config/platform.json -a shq-platform
4. Validate with org display
Edition, user, and token must match expectations.
5. Deploy metadata
sf project deploy start -o <alias>
6. Run capability checks
- Opportunity
- Contact
- ApexPage
- CustomObject
- Profiles
- Licenses
7. Inspect objects with Tooling API
To detect drift and mismatches.
8. Regenerate passwords
sf org generate password -o <alias>
This workflow eliminates integration unpredictability across customer orgs.
8. Non-Obvious Patterns Extracted from the Logs
1. Salesforce UI can show objects the API cannot query.
Always trust SOQL.
2. Connected Apps cannot be moved between orgs with the same key.
Rename or regenerate.
3. Tooling API exposes “ghost” metadata that Metadata API hides.
Essential for diffing multi-version objects.
4. UserType drives hidden constraints.
E.g., API-only users break integrations silently.
5. Enterprise and Developer editions differ in Tooling API exposure.
Not documented.
6. Profile → License mapping is the only reliable permission chain.
7. Scratch org deletions do not reset metadata immediately.
Expect stale object IDs for up to 24 hours.
9. Why This Depth Matters for SaaS Engineering
SaaS platforms that integrate with Salesforce cannot assume:
- consistent metadata
- consistent objects
- consistent licenses
- consistent Connected App behavior
- consistent API access
- consistent user roles
Salesforce CLI is the only way to reveal the underlying state of each customer org, edition, and user context.
The terminal session shows the actual engineering work required to build a stable integration – far beyond standard documentation or UI-based setup.
Bojan