I reported four missing standard fields. All four exist.
No such column 'BillToContactId' on entity 'Order' is a sentence you believe. Under API 67.0 user mode it also fires for a field that exists and that your profile simply cannot see — and sf sobject describe hides both cases identically.
Some errors you argue with. Some you believe immediately, write down, and build around.
No such column 'BillToContactId' on entity 'Order'
That is the second kind. It names the field, it names the object, and it says the thing does
not exist. I read it, accepted it, wrote it up in the build notes as a gap in the standard
Order object, and moved on to designing around the absence.
The field exists. So do three others I had written off the same way.
Where it came from
The context was the Work Order to Order billing bridge at Doorvana — a completed
WorkOrder becoming a Draft Order on a single revenue ledger. Orders need a payer, and a
payer is an Account and often a Contact, so Order.BillToContactId is the obvious place to
put one.
Under API version 67.0 Apex runs in user mode by default. Queries and DML resolve against
the running user’s field-level security rather than with full system access. So when the code
referenced BillToContactId, the platform tried to resolve that field as me, could not, and
said so in the only vocabulary it has for an unresolvable column.
Two different facts, one error message
Here is the part worth internalising. Under user mode these two situations are indistinguishable from the error text:
- The field does not exist on the object.
- The field exists and the running user has no field-level security on it.
And the first tool anyone reaches for does not separate them either:
describe reflects access, not schema
sf sobject describe --sobject Order --target-org <alias> \
| jq -r '.fields[].name' | grep -i billto
# (no output) No output. Which reads as proof, and is not. sf sobject describe returns the fields
accessible to the running user. A standard field with no field permission is not listed
as restricted; it is not listed at all. Describe is answering “can I use this field”, which is
a perfectly good question and not the one being asked.
The query that settles it
EntityParticle, through the Tooling API, describes the schema. It does not care what the
running user can access:
Tooling API — does this field exist at all?
SELECT QualifiedApiName, DataType, IsCreatable, IsUpdatable
FROM EntityParticle
WHERE EntityDefinition.QualifiedApiName = 'Order'
AND QualifiedApiName LIKE '%Contact%' Run with sf data query --use-tooling-api, that returns BillToContactId, with a data type,
on an object I had just finished documenting as not having it.
The same check across the two objects turned up three more standard fields I had quietly
treated as absent: Order.OpportunityId, Order.QuoteId and OrderItem.QuoteLineItemId —
which is to say, the entire standard trail from a Quote to the Order it produced. I had been
prepared to rebuild that with custom lookups.
The fields that genuinely are not there
The contrast matters, because the same build turned up a real absence and the difference in how it behaves is instructive.
OrderItem has no discount field. QuoteLineItem has one. OpportunityLineItem has one.
OrderItem carries UnitPrice, Quantity, ListPrice and TotalPrice, and nothing that
expresses “less ten percent”.
That is not a permissions problem and no Tooling query rescues it — EntityParticle agrees
the field is absent. It is a modelling constraint, and it has a consequence. Take an invented
line: one unit at a list price of 150, agreed at ten percent off. It has to arrive on the
Order as a unit price of 135, because there is nowhere else for the discount to live — or
the booked revenue drifts away from what the customer signed. The booking service divides the
discounted line total by the quantity, and a test asserts the Order total equals the quote
total.
A genuine absence forces a design decision. An invisible field produces a design decision that did not need to be made, against a constraint that was not real. The two are worth five minutes of telling apart.
What I changed about how I work
The original write-up said Order.BillToContactId does not exist. That claim is wrong and
the correction is the more useful record of the two, because the wrong version says something
true about the tooling: this is what an error message can make a competent person believe on
first reading.
The rule that came out of it is small:
- An error about a missing field is a claim about resolution, not about schema. Under user mode, those stopped being the same claim.
- Before building a custom field to replace a standard one, run one
EntityParticlequery. - If the field exists and you cannot see it, the work is a field permission on a permission set, not a custom field. Those two paths diverge permanently — the second one leaves a custom field shadowing a standard field forever, and every integration that reads the standard one gets nothing.
Practical summary
No such column 'X' on entity 'Y'under API 67.0 user mode fires for both a nonexistent field and an inaccessible one.sf sobject describeshows accessible fields only, so it confirms the wrong hypothesis silently.SELECT QualifiedApiName, DataType FROM EntityParticle WHERE EntityDefinition.QualifiedApiName = 'Order'through the Tooling API is the schema-level answer.Order.BillToContactId,Order.OpportunityId,Order.QuoteIdandOrderItem.QuoteLineItemIdall exist and commonly have no field-level security on any profile.OrderItemgenuinely has no discount field. Resolve discounts into the unit price before the line reaches the Order.
Four standard fields, written off on the strength of an error message that was telling the truth about something else entirely.