The note said Built. It had never been deployed.
Five deploy errors, one cause. A foundational custom field and its global value set were written, committed, and never deployed — and every later piece of work assumed they existed. A dry-run found in seconds what reading the source never could.
Somebody opens the project notes to check whether a thing exists before building on it. There is a heading that says Built:, and the thing is listed under it. Good. Carry on.
It had never been deployed. It had been written months earlier, committed, reviewed, and then left in a branch that reached no org. Five separate deployment errors later, all of them tracing back to that one fact, is how it was found.
The dependency
The field is Account.Customer_Type__c — Residential, Builder, Commercial, Dealer — plus the
GlobalValueSet it is built on. It is about as foundational as a field gets on this org.
Lead and Account both point at the same value set so that
lead conversion field mapping does not drift. Validation
rules key off it. The household naming logic branches on it. Half the automation written since
reads it.
None of it was in the live org. The org had four custom fields on that object. The source expected five.
Five errors, one cause
The deployment that surfaced it was not about the field. It was about the work sitting on top of the field, and it came back with five errors in five different places, none of which said “this field does not exist here.”
Each of those reads like its own problem. Debugged individually, each one has its own plausible local fix, and all five of those fixes are wrong.
The message that actually gave it away was the one about the value set.
sf project deploy start --dry-run
Error Account.Customer_Type__c The global picklist cannot be resolved. That error is precise once you read it as a statement about the org rather than about the
source. The field references a GlobalValueSet. The org does not have one by that name. Not
malformed, not mismatched — absent. And a GlobalValueSet that is absent from the org while
being present in source has exactly one explanation.
Why reading the code could never have found it
This is the part worth sitting with, because it generalises past this one project.
The source was correct. Every file was well-formed, the field definition was right, the value set was right, and the references between them were right. A reviewer reading the repository would have found nothing, because there was nothing wrong with the repository. The defect was not in the code; it was in the relationship between the code and an org, and that relationship is not visible from inside the code.
SFDX source format makes this easier to miss rather than harder. The repository is a complete and self-consistent description of a Salesforce org. It looks like the org. It is arranged like the org. What it is not is evidence about any particular org, and nothing in the checkout distinguishes a component that has been deployed everywhere from one that has been deployed nowhere.
The cheap thing that answers it
A validation-only deployment. Seconds, no side effects, authoritative.
verify the dependency before building on it
# does the org actually have the thing the plan says is built?
sf project deploy start \
--source-dir force-app/main/default/objects/Account/fields/Customer_Type__c.field-meta.xml \
--dry-run
# and what is actually on the object right now
sf sobject describe --sobject Account \
| jq '[.fields[] | select(.custom) | .name]' The describe is the one that settles arguments. Four names came back where the source expected five, and at that point there is nothing left to discuss — the conversation stops being about whether the work was done and becomes a two-minute deployment.
The note got amended, which is the real output
It would have been easy to deploy the field, close the five errors, and move on. The more useful change was to the notes.
“Built” now means “written down.” That is what it had always meant on this project; it just had not been said out loud, and the heading implied something stronger than anyone had intended to claim. Deployment is a separate claim, about a named org, requiring separate evidence — a describe, a retrieve, a dry run, something that asked the org.
The vocabulary is doing real work here. Written, committed and merged are all claims about a repository and are all cheap to verify. Deployed is a claim about an org and is the only one that matters when you are about to build on top of something. A project note that says a thing is built, without naming where, is ambiguous in the most expensive direction available, because the reader will resolve the ambiguity in the optimistic direction every time.
What I do differently now
Verify the dependency, not the plan. When starting work whose first line assumes something exists, spend the ten seconds asking the org. The cost of the check is negligible and the cost of skipping it is a debugging session that begins in entirely the wrong place — reading your own new code for a mistake that is not in it.
Keep the deploy loop short. A component written months before it is deployed is a component with months of opportunity to become untrue. This gap existed because the two events were far apart, and nothing in the process noticed the distance.
Prefer statements that name an org. “Deployed to the sandbox on Tuesday, verified by describe” survives contact with a future reader. “Built” does not.
The failure mode here is not carelessness. Every individual step was done properly: the field was designed correctly, written correctly, and recorded honestly by someone who believed “built” meant what they meant by it. The gap was in a handoff that nothing in the tooling supervises, which is exactly the kind of gap that does not close by trying harder — only by changing what the words are allowed to mean.