Overridable values
An overridable value is not a field type. It is a composition: a computed value (a formula or a roll-up total), a nullable override slot, and a derived effective = coalesce(override, computed) that every consumer reads. All three parts live in the pure tier — the override is stored data, and computed and effective are read/save-time derivations that are deterministic and statically analyzable. There is no trigger, no dirty-check, and no automation anywhere in the construct.
The model
Section titled “The model”A single logical overridable value is backed by three parts on the same record:
| Part | Storage | Written by | Tier / when |
|---|---|---|---|
computed |
stored (generated column) or virtual (view expression) or a storage-enforced roll-up | the engine only | pure — recomputed from sources at write (stored / roll-up) or at read (view) |
override |
a real, nullable stored column of the same type/unit as computed |
a human (or an explicit API write) | data — persists untouched across recomputes |
effective |
virtual, COALESCE(override, computed) |
nobody — derived | pure — resolved at read time |
The load-bearing rule is the sentinel:
override IS NULL→ not overridden.effectivefalls through tocomputed.override IS NOT NULL→ overridden.effectivereturns the pinned value. This holds for any stored value — including0, an empty-but-present string, or a value that happens to equalcomputed. Override-ness is keyed on presence (NULL vs not-NULL), never on magnitude.
Because computed and override are different slots, recomputing computed on every save is safe and desirable: it can never touch override. The two never contend for the same cell, so there is no clobber to defend against and nothing to serialize.
The construct is identical for a leaf value and for a roll-up total. Overriding a parent aggregate sets override on the roll-up node; the roll-up’s own storage-enforced recomputation continues to maintain computed underneath it. There is no separate “override a roll-up” mechanism — it is the same three parts applied one level up.
Authoring
Section titled “Authoring”An overridable value is declared as one field whose type is overridable, wrapping a computed expression. The author supplies the computation (a formula body or a roll-up spec) and the return type; the platform provisions the override column and the effective resolver.
Config vocabulary:
| Key | Required | Meaning |
|---|---|---|
computed |
yes | The derivation. Either an inline pure formula (same grammar as a formula field) or a rollup spec ({ child, aggregate, field, filter? }). |
type |
yes | Return type of computed; override is provisioned with the identical type. One of the scalar types (number, currency, percent, date, datetime, text, boolean, …). |
unit |
when applicable | Currency/measure unit. override inherits it; a mismatched unit is a compile error, not a silent coercion. |
overridable_by |
no | Permission expression gating who may write override. Defaults to the field’s edit permission. |
drift |
no | Drift-surfacing policy — see Semantics. { warn_at: <fraction>, block_at?: <fraction> }. Absent this key, override-ness is still tracked but no proactive drift signal fires. When declared, it defaults to warn-only; block_at is always explicit opt-in. No field class blocks by default — a hard stop turns a drifted pin into a save-blocking error with approval implications, a policy that belongs to the field, not the platform. |
on_source_change |
no | For roll-up overrides: keep (default — a pinned parent stays pinned when a child changes; drift is surfaced) or clear (a lower-level change nulls the parent override, returning the parent to the live roll-up). This is a per-field authoring choice; the platform applies no object-wide auto-clear, because whether a child edit should invalidate a deliberate parent pin depends on the field’s meaning, not on the mechanism. |
The resolver effective = coalesce(override, computed) is generated, not authored. Consumers reference the field by name and receive effective; computed and override are addressable as <field>.computed and <field>.override for provenance and drift surfaces, but are never the default read.
Worked example — a line-item extended cost that a billing specialist may pin, and a parent total that rolls the effective line costs and is itself overridable:
// Field on LineItem: computed extended cost, overridable by a billing specialist{ "key": "extended_cost", "label": "Extended Cost", "type": "overridable", "body": { "type": "currency", "unit": "USD", "computed": { "formula": "quantity * unit_cost" }, "overridable_by": "can(\"dgn__pricing.override_material_total\")", "drift": { "warn_at": 0.05 } // flag when a pin is >5% off recompute }}
// Field on Invoice: roll-up of the EFFECTIVE line costs, itself overridable{ "key": "material_total", "label": "Material Total", "type": "overridable", "body": { "type": "currency", "unit": "USD", "computed": { "rollup": { "child": "line_items", "aggregate": "sum", "field": "extended_cost" // reads each child's EFFECTIVE value } }, "on_source_change": "keep", // a pinned total survives a child edit; drift is surfaced "drift": { "warn_at": 0.05 } }}Reading material_total yields coalesce(override, sum(effective extended_cost)). Pinning it writes material_total.override; clearing it writes NULL.
Semantics & evaluation
Section titled “Semantics & evaluation”Where it sits in the save order. The override is ordinary stored data, so it is shaped and validated before the write like any field. computed (as a generated column or storage-enforced roll-up) is maintained by the transactional write itself; effective is resolved at read time from the two persisted slots. No author-defined effect participates — an overridable value never requires after-save automation. This keeps the whole construct inside the single-pass, non-re-entrant save order: there is no phase in which a recompute could race a manual write, because they target different columns.
NULL / blank handling. NULL on override is reserved as the not-overridden sentinel and is therefore not a legal override value. Pinning “the answer is genuinely nothing here” is expressed with the type’s own empty-but-present value (e.g., 0 for a currency, '' for text) — which is a present value and reads as overridden. This is the exact ambiguity that a blank-substitution idiom cannot express, resolved by making presence the signal rather than blankness.
Precision & determinism. effective is a pure derivation over two persisted, typed slots; it is deterministic and reproducible. When computed calls a calc function, reproducibility follows from that function’s purity and pinned (content-addressed) version — the override slot does not affect it. override must match computed’s type and unit exactly; the compiler enforces this at author time so coalesce can never return a mismatched value.
Drift — the honest bound. The composition guarantees the platform always knows a value is overridden. It does not guarantee the pin is still wanted. When computed moves (a source changed) while override stays pinned, effective is a human number frozen against a moved baseline. This is a real, named cost, not something the model eliminates. The obligation it creates:
Read routing. Every read path resolves effective by default: the report engine, the REST and GraphQL APIs, PDF export, and any downstream pure formula that references the field all receive coalesce(override, computed) without opting in. computed and override are reachable only through the explicit <field>.computed / <field>.override accessors, which provenance and drift surfaces use deliberately. Because the resolver is generated from type: overridable — one read expression compiled into the field, not reconstructed per consumer — no read path can silently default to the raw computed slot. The one-resolver guarantee is structural, not a convention each consumer must remember to uphold; the only way to read past the resolver is to name a .computed/.override accessor on purpose.
Provenance and retention. An override is an explicit write to a dedicated column, so field history captures who set it, when, and the prior value — and because computed is a separate slot that keeps recomputing, the machine’s number at the moment of the override is recoverable by reading <field>.computed alongside the history entry. Provenance is intrinsic to the composition, not a bolted-on audit feature with a field budget. History for both the override writes and the superseded computed snapshots lands in the append-only field-history table, which is an ordinary table rather than a tracked-field allocation: there is no per-object cap on how many overridable values may be audited and no fixed retention cliff. How long that history is kept is a tenant-level operational policy (a data-retention or pruning decision on a normal table), unbounded by construction rather than by a platform ceiling.
Limits, and the reasons behind them
Section titled “Limits, and the reasons behind them”| Concern | CAOS approach | Salesforce’s exact limit / behavior (cited) | The WHY behind the SF limit | Does the CAOS stack still need an equivalent? |
|---|---|---|---|---|
| Editing a computed field | override is a real nullable column; effective coalesces |
Formula fields are always read-only; they never appear on an edit/create screen | Source fields “are not finalized until a record is saved,” so a formula has no writable moment in the edit lifecycle | No — the writable part (override) is a distinct stored slot, so nothing about the derivation blocks editing |
| Overriding an aggregate | Same composition on the roll-up node; a pin sets override on the parent |
Roll-up summary fields are read-only and not shown on edit pages | Same derived-at-save-time rationale as formulas | No — the parent’s override is a plain column; the roll-up still maintains computed beneath it |
| “Overridden to blank/zero” | Presence (NULL vs not-NULL) is the signal; 0 and empty-present read as overridden |
The BLANKVALUE/ISBLANK idiom conflates blank with not overridden — an override equal to blank is indistinguishable from no override |
Emulation stores the value but not the fact of overriding; blank is overloaded as the sentinel | No — NULL-as-sentinel is independent of magnitude, so the fact of override is stored separately from its value |
| Recompute clobbering a manual edit | Impossible — computed and override are different slots |
An auto-populating before-save flow / trigger runs on every update and overwrites the manual value unless guarded | Automation infers intent from state because the platform stores no provenance | No — there is no shared cell to clobber, so no dirty-check, lock flag, or ISCHANGED heuristic is needed |
| Provenance / audit | Intrinsic: override writes are history-tracked; computed slot preserves the superseded machine value |
Field History Tracking: up to 20 fields/object, 18-month default retention; 60 fields via the paid Field Audit Trail add-on | History is a bolt-on with a hard per-object field budget and a retention cliff | No — history is an append-only table, not a tracked-field budget, so there is no per-object cap and no fixed retention cliff; retention is a tenant-level operational policy on a normal table |
| Field-count sprawl | One declared overridable field |
Pattern B needs three fields (computed, manual, effective) per value; custom fields per object are capped (edition-dependent) | No single “computed-with-override” component exists, so the behavior is hand-assembled from separate fields | No — the three slots are one declared field; only override is a stored column, computed/effective are derivations |
How Salesforce does it
Section titled “How Salesforce does it”Salesforce has no first-class overridable-computed field type. Its field-type catalog splits cleanly into computed-and-read-only (Formula, Roll-Up Summary, Auto Number) and editable-and-not-computed (Number, Currency, Text, …); no type is both. Formula fields are read-only by construction — the platform states the value cannot be shown during create/inline-edit because “the values of source fields are not finalized until a record is saved.” Roll-up summary fields are likewise read-only and absent from edit pages. The capability has been a standing IdeaExchange request for years (ideas titled “Editable Formula Fields” and variants) without being delivered — corroborating the absence.
Admins therefore emulate it, in three families:
- Pattern A — one editable field populated by automation. A before-save flow or Apex before-trigger computes and assigns the value in-memory before commit (no extra DML; runs on create and update). Because the target is a real column, a user can type into it. The leak: the same automation runs on every subsequent save and overwrites the manual edit. Admins patch this with dirty-check hacks — an “is-overridden” lock checkbox, an
ISCHANGED/Trigger.oldMapcomparison, or a shadow “last computed value” field — every one of which infers intent from state because no provenance is stored. - Pattern B — three fields. A read-only formula
Computed__c, an editableManual_Override__c, and a formulaEffective__cdefined asBLANKVALUE( Manual_Override__c , Computed__c ). This is the closest native analog to the CAOS composition —coalesce(override, computed)spelled out in three fields plus consumer discipline. The leaks: three fields per value (field-count pressure, layout/report sprawl); every consumer must remember to readEffective__c, neverComputed__c; and the blank-substitution test cannot express “overridden to blank/zero” — plus a number formula’s Blank Field Handling (“treat blanks as zeroes” vs “as blanks”) can itself change results. - Pattern C — a default value (computed by a formula). Reached for often; not a solution. Default values are applied once, at record creation, and never reapplied on edit, and a formula field cannot have a default at all. It yields a seed, not a living computed value — no stickiness, no recompute, no override semantics.
What CAOS does better, by named mechanism.
- Genuinely better (mechanism): the override lives in its own nullable column with NULL as the not-overridden sentinel, so override-ness is independent of the value — Pattern B’s
BLANKVALUEfundamentally cannot express “overridden to blank,” and CAOS can. Becausecomputedis a generated column / storage-enforced roll-up in a separate slot, Pattern A’s whole clobber-and-dirty-check class is eliminated by construction — no trigger to write.effective = coalesce(override, computed)is enforced in one read resolver rather than relying on every report/API/PDF consumer to reference the right field. The same composition applies to a roll-up total, which Salesforce cannot override in place at all. - Mere parity: the idea of
coalesce(override, computed)is exactly what a careful Pattern B admin builds, and what any engineer writes as a coalesce. CAOS does not invent an algorithm — it promotes the pattern to a single first-class field semantic. - Costs / risks (named): staleness is not solved — a pinned override drifting from a moved
computedis the same problem Salesforce has, in a different shape; CAOS makes it visible (drift signal) and recoverable (clear-override), which is strictly better but is not a fix. Type/unit discipline is not eliminated, only centralized —overridemust matchcomputedexactly. The one-resolver advantage, by contrast, is not a residual risk: becauseeffectiveis a single generated resolver every read path inherits, there is no per-consumer “read the wrong field” leak to guard against — the SF failure mode where a report or PDF references the raw computed field cannot arise unless a consumer names a.computed/.overrideaccessor deliberately.
Metadata & deploy representation
Section titled “Metadata & deploy representation”An overridable value is one canonical component. Contrast with Salesforce, where the behavior is emergent across 2–3 CustomField elements plus imperative automation and there is no single component that declares “computed-with-override.”
{ "key": "material_total", "label": "Material Total", "type": "overridable", "body": { "type": "currency", "unit": "USD", "computed": { "rollup": { "child": "line_items", "aggregate": "sum", "field": "extended_cost" } }, "on_source_change": "keep", "drift": { "warn_at": 0.05 }, "overridable_by": "can(\"dgn__pricing.override_material_total\")" }}The component fully declares the semantic: the override column, the computed derivation, and the effective resolver are all implied by type: "overridable" — none is a separate hand-wired field the deploy must keep in sync. Retrieve → diff → deploy operates on this single object; a diff that changes computed re-provisions the generated column / roll-up, a diff that changes drift or overridable_by is metadata-only, and the override data is never part of the component (it is record data, not schema).
Salesforce Metadata API analog (for migration/parity mapping): the three pieces deploy as separate CustomField elements — the computed field carries <formula> and <formulaTreatBlanksAs> (Zeroes|Blanks); the override field is an ordinary editable CustomField; the effective field is another formula CustomField whose <formula> is the BLANKVALUE(...) expression. Pattern A’s recompute/skip logic ships as a record-triggered Flow or an ApexTrigger, and provenance as object-level FieldHistoryTracking (enableHistory + per-field trackHistory).
Sources
Section titled “Sources”- Formula Fields Not Visible on Edit Screen (formula fields are read-only; source fields not finalized until save)
- Create a Roll-Up Summary Field (roll-up summary fields are read-only, not on edit pages)
- Default Field Value Considerations (defaults applied once at creation, not reapplied on edit)
- How Blank Field Handling Works for Salesforce Formula Fields (zeroes vs blanks)
- Before-Save Flow vs. After-Save Flow (before-save runs create+update, no extra DML)
- Salesforce Field History Tracking (20 fields/object, 18-month retention, 60 with Field Audit Trail)
- Track Object Field History (developer guide)
- Salesforce IdeaExchange — standing requests “Editable Formula Fields” (a0B8W00000Gdlp5UAB), “…in Managed Package” (a0B8W00000GddOEUAZ), “Allow changes in formula field value to trigger workflow” (a0B8W00000GdW63UAF). Existence confirmed via search index; verbatim status labels not captured.