Sometimes the most interesting engineering work comes from problems you’ve been avoiding for years.
Recently I found myself dealing with one of those problems.
The Salesforce org I work with contains a number of Custom Labels that hold values which differ between environments. It’s not an approach I would recommend today, but it’s something that was inherited as part of an older implementation of the platform.
Because of that, Custom Labels had always been treated carefully during deployments.
Whenever a change involved labels, there was always a concern that deploying one update might unintentionally overwrite values that were meant to remain different in another environment.
Over time, the team developed an understanding of the risk and generally worked around it. In some cases that meant avoiding changes where possible. In other cases it meant making updates manually rather than pushing them through the normal deployment pipeline.
The real issue wasn’t that we didn’t know about the problem.
The issue was that we never had a compelling enough reason to invest time in solving it properly.
Until recently.
A new requirement came along that required updates to Custom Labels, and I didn’t want to fall back to manual deployment again. If we were going to maintain a CI/CD pipeline, then it needed to handle this scenario properly.
That kicked off an investigation into how Salesforce handles Custom Label deployments and whether there was a safer way to deploy label changes without affecting unrelated values.
Why Custom Labels Became a Deployment Problem
Like many Salesforce DX projects, our deployment pipeline uses delta deployments.
The idea is simple: only deploy metadata that has changed.
For Apex classes, Flows, Lightning Web Components, and most metadata types, this works extremely well.
Custom Labels are different.
By default, every label in a Salesforce DX project is stored inside a single file:
force-app/main/default/labels/CustomLabels.labels-meta.xml
This means that changing a single label causes the entire file to be modified.
From Git’s perspective, the whole metadata component has changed.
From the deployment pipeline’s perspective, the whole metadata component gets deployed.
Normally that’s fine.
But when some labels contain environment-specific values, deploying the entire label set introduces risk.
A change intended for one label can potentially overwrite values that should remain unique in the target environment.
This was exactly the scenario I wanted to avoid.
Looking for a Better Approach
While researching options, I came across Salesforce’s Custom Label decomposition feature.
Instead of storing all labels inside a single XML file, Salesforce can split them into individual source files.
Conceptually, the structure changes from:
labels/
|-- CustomLabels.labels-meta.xml
to:
labels/
|-- API_Endpoint.label-meta.xml
|-- Feature_Flag.label-meta.xml
|-- Error_Message.label-meta.xml
|-- Success_Message.label-meta.xml
Immediately, this looked promising.
Individual labels could now be tracked independently by Git.
Delta deployments could potentially include only the labels that actually changed.
Most importantly, it reduced the likelihood of unrelated labels being included in a deployment package.
It seemed like exactly what I needed.
The Pipeline Breaks
As is often the case, the first attempt didn’t go smoothly.
sf project convert source-behavior --behavior decomposeCustomLabelsBeta2
After enabling Custom Label decomposition and committing the new source files, the deployment pipeline started failing.
The error looked something like this:
TypeInferenceError:
Could not infer a metadata type

Salesforce CLI was unable to determine what metadata type the decomposed label files represented.
At first I assumed the issue was related to the deployment command itself.
The pipeline was performing source-directory deployments, so perhaps Salesforce couldn’t correctly resolve the decomposed metadata during deployment.
Chasing the Wrong Problem
My first instinct was to switch from source-directory deployments to manifest-based deployments.
The thinking was straightforward:
If Salesforce was struggling to infer metadata types from source files, perhaps explicitly defining the metadata in a package.xml would solve the problem.
sf project deploy validate --manifest delta/package/package.xml --test-level=RunLocalTests
So I modified the deployment process to use the generated manifest instead.
The result?
Exactly the same error.

At least that narrowed down the problem space.
The deployment method wasn’t the issue.
Something else was missing.
Finding the Real Root Cause
After spending more time comparing what actually changed when decomposition was enabled, I noticed something easy to miss.
The decomposition process wasn’t just creating new label files.
It was also modifying the project’s sfdx-project.json.
Specifically, Salesforce added a source behavior configuration:
{
"sourceBehaviorOptions": [
"decomposeCustomLabelsBeta2"
]
}
This turned out to be the missing piece.
Without that configuration, Salesforce CLI sees files like:
API_Endpoint.label-meta.xml
and has no idea how to interpret them.
The metadata itself was valid. The deployment process was valid. The CLI simply wasn’t aware that the project had been converted to use decomposed Custom Labels.
The moment that configuration was committed to source control and made available to the pipeline environment, everything started working.
The Real Fix
After comparing the project before and after decomposition, Because there were tons of new files, it was easy to miss that cli had updated the project’s sfdx-project.json.
Alongside the new decomposed label files, it had added a source behavior configuration:
{
"sourceBehaviorOptions": [
"decomposeCustomLabelsBeta2"
]
}
That configuration tells Salesforce CLI how to interpret decomposed Custom Label metadata. The issue wasn’t with the deployment pipeline at all. The issue was that the CI environment didn’t know the project had been converted to use decomposed labels.
Once the updated sfdx-project.json was committed to source control and picked up by the pipeline, everything started working.
Even better, the existing source-driven deployment process continued to work without modification. No special deployment logic was required.
No manifest deployment fallback was required. The pipeline itself remained largely unchanged.
Final Thoughts
Looking back, the actual fix was relatively small.
The investigation took significantly longer than the implementation.
What started as a simple requirement to deploy some Custom Label changes ended up uncovering a useful capability in Salesforce DX that I hadn’t previously explored in depth.
More importantly, it allowed us to remove a piece of deployment friction that had existed in the background for years.
Is decomposing Custom Labels the ideal long-term solution?
Probably not.
The better long-term outcome is to move environment-specific values into configuration models that are designed for that purpose. But engineering is often about making incremental improvements rather than waiting for perfect architecture.
For now, decomposed Custom Labels and a small enhancement to the deployment pipeline have given us a safer, more reliable way to manage a part of the platform that had previously required extra caution.
And perhaps more importantly, it’s one less manual deployment to worry about.