Calc functions
A calc function is a registered, named computation unit — typed signature, pure body, immutable content-addressed revision — that a formula, roll-up, or validation may call as if it were built in. It belongs to the pure tier: evaluated at read/save time, deterministic, statically analyzable, and safe to push down. It is the platform’s procedural escape hatch, and it is deliberately narrower than an unrestricted procedural language — a calc function is what a formula is allowed to reach, not a place to run effects.
The differentiator is structural, not cosmetic. Salesforce has no user-defined formula functions of any kind, and its formula layer cannot reach custom code at all (see How Salesforce does it). A CAOS formula that calls markup(cost, pct) is exercising a capability with no Salesforce analogue.
The model
Section titled “The model”A calc function has three fixed properties, all enforced at registration and preserved through dispatch:
- Typed. A declared signature — parameter types and return type — checked by the compiler at every call site. A call whose argument types do not unify with the signature fails compilation, the same as a malformed built-in call.
- Pure. The body may not read the wall clock, generate randomness, read ambient session/user state, perform I/O (network, email, enqueue), or read mutable database rows. Same inputs → same output, by construction. Purity is enforced (see Semantics & evaluation), not documented as a convention.
- Content-version-pinned. Each revision of a function body is identified by an immutable content hash. A formula that calls a function records the exact revision it compiled against; dispatch resolves to that revision, not to “whatever the body is now.”
Where the value lives and when it computes are inherited from the calling context — a calc function has no storage of its own. Called from a virtual formula field, it evaluates at read time; called from a stored/generated column’s definition or a validation expression, it evaluates at save time inside the pre-write shape-and-validate phase of the save order. It never fires on an event and never runs after commit, because it is pure — there is nothing for it to do post-write.
Reproducibility is not a feature bolted on; it falls out of purity plus pinned revision. A price computed by fn@<hash-A> over a fixed set of inputs recomputes byte-identically as long as the formula stays pinned to hash-A, with no reliance on external version control. Upgrading a formula to fn@<hash-B> is an explicit, visible change to the formula’s compiled form.
Authoring
Section titled “Authoring”A calc function is authored as a registered component with a typed signature and a body written in the pure subset of the one typed language — the same TypeScript grammar, operators, and standard library available to formula fields, with the effectful surface removed by the compiler.
Signature grammar
Section titled “Signature grammar”A calc function is a typed TypeScript function declaration marked with the @calc decorator — the same decorator convention that binds a controller to an automation slot. There is no separate signature DSL to learn:
@calcexport function name(param: Type, …): ReturnType { return /* expression */;}- The name is the identifier used at call sites; it is namespaced so the standard library and org-defined functions cannot collide.
- Parameters are typed. Types are the platform scalar/temporal set —
Decimal,Currency,Percent,string,boolean,DateOnly,DateTime,Time— plus declared picklist types. TypeScript primitives are used where the semantics match exactly (string,boolean); numerics are platform types because they are exact decimal, not IEEE. - The return type is single and declared, and is enforced against the call-site context — a
Decimal-returning function cannot be dropped into astringslot without an explicit conversion. - Purity is not declared, it is verified. There is no
where pureclause to write: the tier comes from the surface (@calcmeans pure), and the compiler rejects any body that violates it. A declaration that could be a lie is worse than no declaration. - The body is one
returnof a pure expression. Composition is expression-level: a calc function may call other pure calc functions and standard-library functions, and nothing else.
Body vocabulary
Section titled “Body vocabulary”The body draws from the pure subset, identical to the formula grammar — the same operators, the same standard library, the same methods on typed values. Two restrictions are specific to a calc function, and both follow from it having no record in scope:
- No
record, noprior, nouser, noorg. A calc function receives everything it needs through its parameters. That is what makes it callable from any surface and memoizable on its arguments alone. - No clock.
today()andnow()are excluded — they read the current instant and would make the result non-deterministic. A function that needs the date takes it as a parameter.
Also rejected at registration: random generators, any I/O, and any read of a mutable database row. A body that names one of these does not compile.
Worked example
Section titled “Worked example”A reusable markup rule, duplicated across many formula fields in a Salesforce org, becomes one registered function in CAOS:
@calcexport function markup(cost: Decimal, pct: Percent): Decimal { return round(cost * (1 + pct / 100), 2);}A formula field then calls it exactly like a standard-library function:
// field: invoice.line_price → Decimal (virtual)markup(record.base_cost, record.margin_pct)Every formula that needs the markup rule calls markup(…) rather than re-typing round(cost * (1 + pct / 100), 2). Changing the rule is a single registered edit that produces a new content hash; formulas migrate to the new revision explicitly, and any formula still pinned to the prior hash keeps computing the prior rule until migrated.
Semantics & evaluation
Section titled “Semantics & evaluation”Timing. A calc function has no independent evaluation moment; it evaluates wherever its caller does. In the save order of execution, formula and validation evaluation sit in the pre-write shape-and-validate phase, before the transactional write. Calc functions invoked there complete before any row is persisted. There is no re-entrancy and no second pass.
Determinism enforcement. Purity is checked structurally at registration, not trusted:
- The body is parsed and every referenced symbol is resolved. Any non-pure built-in (clock, RNG, ambient-state, I/O) is a compile error.
- Every called calc function must itself be
pure. The pure-may-only-call-pure rule is a type/effect constraint enforced at registration, so purity cannot be laundered through a transitive call. If any dependency is effectful, registration fails. - The resulting body is content-hashed to produce the revision identity.
The determinism guarantee is only as strong as this enforcement and the runtime that backs it — a sandbox escape (a body that reaches the clock, a table, or the network) silently breaks reproducibility, which is the single capability the construct exists to provide. That risk is covered in Limits.
Null / blank handling. Nulls propagate through arithmetic per the formula layer’s rules; a function that must treat a null argument as a value uses COALESCE/BLANKVALUE explicitly. A null argument is a valid input — it does not throw — and the function’s output for a null input is as deterministic as any other input.
Precision & numeric model. The pure tier is exact-decimal end to end. Every number/decimal value is a Postgres numeric — arbitrary-precision, exact for addition, subtraction, and multiplication — never IEEE-754 binary floating point, because floating-point non-associativity across hardware, build, and parallel-evaluation changes can defeat “same inputs → same output.” numeric carries no such drift, at a known cost: numeric arithmetic is materially slower than the binary float and integer types (the price of exactness). decimal types declare precision and scale (decimal(p, s)); the return of a division or ROUND is rounded to the declared scale. Rounding is half away from zero (“half-up”) at every tie: 1.45 → 1.5, -1.45 → -1.5. That choice is not arbitrary — it is simultaneously the Postgres numeric native rounding mode and the Salesforce ROUND tie rule, so the substrate implements it without emulation and cost math matches a Salesforce org digit-for-digit [S11][S12]. A function needing statistical neutrality across large aggregates uses an explicit banker’s-rounding built-in rather than changing the default.
Type contracts. The declared signature is the contract. Argument types must unify with parameter types at compile time; the return type must satisfy the call-site slot. There is no runtime coercion that a static check would not have already accepted.
Runtime & syntax. The body is authored in the TypeScript-flavored pure expression language shared with formula fields — the same surface syntax across the whole pure tier, with the effectful surface removed by the compiler — and executed by a bespoke expression interpreter over the exact-decimal value model, not by Postgres plpgsql/plpython and not by WASM. The interpreter is the right substrate for three reasons the alternatives cannot match: it evaluates a small typed AST, so per-call overhead is arithmetic-scale rather than the process/module cold-start a WASM or subprocess sandbox pays on every invocation; it enforces purity structurally (there is no opcode in it that can reach a clock, a socket, or a table, so a “sandbox escape” is a missing capability rather than a permission to revoke); and it makes exactness native (its numeric primitive is numeric, with no float path to accidentally take). An in-Postgres plpgsql body under a locked-down role would reintroduce the very hazards the pure tier exists to exclude — ambient now()/random(), locale- and timezone-dependent formatting, and a float8 arithmetic path — and would depend on role hardening to stay pure rather than on the language having no way to be impure. The surface syntax is specified together with this runtime, as one artifact, so a body’s meaning is fixed by the interpreter that evaluates it.
Limits, and the reasons behind them
Section titled “Limits, and the reasons behind them”Salesforce’s constraints on custom logic exist because Apex is arbitrary effectful code the platform cannot reason about statically. CAOS’s pure, typed, pinned model removes some of those constraints and still requires an equivalent for others.
| Concern | Salesforce’s exact limit | The WHY (platform constraint it guards) | CAOS equivalent still needed? |
|---|---|---|---|
| Formula → custom code | Formula fields cannot call Apex or any user-defined function; grammar is built-in-only [S2][S5][S6] |
The formula engine is a sealed expression evaluator; there is no dispatch-to-code opcode in the grammar | No — a calc function is the formula→code path. This is the differentiator, not a limit to re-impose. |
| Deploy quality gate | ≥75% org-wide Apex test coverage, every trigger ≥1 line, all tests must pass [S1][S3][S4] |
Apex is effectful and unprovable statically; coverage is a procedural safety net, not a correctness proof | Yes, but reshaped — the gate is mandatory static checks (type-soundness, purity, pure-only-calls-pure) plus declared characterization tests (golden input → output assertions) pinned to the content hash, not a coverage percentage. A pure function’s soundness is proven statically, so a line-coverage floor proves nothing extra. |
| Runtime containment | 100 SOQL / 150 DML / 10,000 ms CPU (sync) / 6 MB heap per transaction [S9] |
Multi-tenant fairness: one tenant’s runaway code must not starve others | Yes — a pure UDF sandbox still needs CPU/memory/timeout ceilings. Different implementation, parity in intent. DML/SOQL ceilings are moot in the pure lane (no writes, no mutable reads). |
| Reuse of logic | No user-defined formula functions; $CustomMetadata shares values, not functions; logic must be duplicated across formulas [S6][S7][S8] |
No authoring surface for a callable function exists in the formula layer | No — registered calc functions are the reuse surface SF lacks. |
| Versioning of the callable unit | ApexClass has apiVersion + mutable in-place body; status is only Active/Deleted; no content-hash revision identity [S4] |
Metadata stores source-in-a-blob; the platform has no immutable-revision primitive | No re-import needed — content-hash pinning is a CAOS primitive. The cost moves elsewhere: the kernel must store and dispatch old revisions (see below). |
| Invocable shape | One @InvocableMethod per class; single List-in / List-out [S2] |
Bulkification contract for the automation runtime | No — the pure lane calls scalar-signature functions directly; the bulk-List shape is an automation-tier concern. |
How Salesforce does it
Section titled “How Salesforce does it”Salesforce’s declarative layer reaches custom code through exactly one language — Apex — and only through specific doors, none of which is a formula field.
- The formula layer is sealed. Formula fields, validation rules, and default-value formulas evaluate a fixed library of built-in operators and functions inline. There is no grammar construct that dispatches to custom code.
[S5][S6] - No user-defined formula functions exist. Through Summer ’26 (API v67.0) there is no GA, beta, or pilot feature to author a named function and call it from a formula. The
FormulaFunctionsObject (API v47.0+) is a read-only catalog of the built-in functions — documentation about built-ins — not an authoring surface for custom ones.[S6][S7]The only cross-formula reuse is$CustomMetadata, which centralizes values, not logic.[S7][S8] - Apex is the escape hatch, reached only from automation/triggers.
@InvocableMethodbridges declarative logic to Apex; its documented invocation sources are Flow, REST API, Apex, Agentforce, and external API sources — formula fields are absent by design.[S2] - Apex has no purity or determinism contract. A method reached via Flow may do DML, callouts, send email, and read
System.now()/UserInfo. Nothing in the type system marks a method pure or deterministic; the platform’s only levers are governor limits (runtime) and 75% coverage (deploy-time). Consequently an invoice priced today may not reprice identically tomorrow, and static analysis cannot certify otherwise.[S1][S9] - The reverse direction is not the same thing. “Evaluate Formulas in Apex” (
System.Formula/FormulaEval, GA Spring ’25) lets Apex evaluate a formula string and enumerate its referenced fields — Apex calling the formula engine, the opposite of a formula calling code. It creates no formula→code path.[S10]
What CAOS does genuinely better (mechanism named; [BETTER] = no Salesforce analogue):
[BETTER]Formulas can call registered functions. SF has no formula→code path of any kind; a callable pure function from the formula layer is a new capability, not a parity feature.[BETTER]Enforced purity + determinism as a contract. Registration proves the body reads no clock, RNG, ambient state, I/O, or mutable row, so the kernel guarantees same-inputs→same-output. SF offers no such contract on Apex. This is what makes a priced invoice reproducible by construction.[BETTER]Content-version pinning. Each revision has an immutable content hash a formula pins; historical recomputation is deterministic without external VCS archaeology. SF’sApexClasshas onlyapiVersionand a mutable in-place body.[BETTER]Static analyzability. Typed pure signatures let the kernel build a static dependency graph (which fields/functions a formula reaches) for impact analysis and safe memoization. SF’sgetReferencedFieldsgives field refs for formulas only; Apex downstream is an opaque effectful box.[BETTER]Open-but-locked standard library. A readable, inspectable standard library that consumers can read and pin but not silently fork — clone-to-customize produces a new, separately-pinned function. SF’s built-ins are a closed, unversioned catalog; Apex is fully open but unconstrained.
Parity, not a win (do not oversell): having an escape hatch at all (SF has Apex); a typed invocable action for the effectful lane (SF’s @InvocableMethod); a deploy quality gate (SF’s 75% + all-pass); runtime containment (SF’s governor limits). CAOS needs each of these too.
Costs / risks CAOS must budget (the hard parts):
- Enforced purity carries an implementation burden. “Pure” is enforced, not trusted, by the expression interpreter — the substrate has no opcode that reaches a clock, socket, or table. That closes the sandbox-escape class the alternatives leave open, but the interpreter is a component the kernel must build, harden, and keep exact; determinism is only as strong as that implementation.
- Determinism has sharp edges the substrate must trap. Clock reads, RNG, locale/timezone-dependent formatting, unordered iteration, and mutable-DB reads are all excluded by construction; floating point is excluded by using exact-decimal (
numeric) throughout, which costs measurable arithmetic speed relative to a binary-float path. - Versioned dispatch adds real complexity. Content-hash pinning means the kernel stores and executes old revisions indefinitely, resolves which revision each formula pinned, garbage-collects unpinned revisions safely, and presents a coherent
fn@v1 → fn@v2migration story — a version-resolution problem SF sidesteps by not versioning at all. - The impure lane must not contaminate the pure lane. If a pure function could transitively call an effectful one, purity collapses. The pure-may-only-call-pure rule is enforced at registration.
- Live data must enter as pinned snapshots, not direct reads. A reproducible priced output cannot read a mutable price row inside a pure body. Current GP/inventory data enters the pure tier as a content-addressed data snapshot resolved to a specific version — a data-versioning primitive parallel to function-revision pinning — so a recomputation over
fn@<hash>andsnapshot@<hash>is byte-identical. The cost is a second versioned store and a snapshot-refresh workflow; the reward is that “reprice this invoice exactly as it was priced” needs no external archaeology.
Metadata & deploy representation
Section titled “Metadata & deploy representation”A calc function is a first-class metadata component (unlike Salesforce, where the only deployable unit for custom logic is an ApexClass/ApexTrigger, and no formula-function metadata type exists [S4][S6]). Canonical component shape:
{ "key": "calc.markup", "label": "Markup", "type": "calc_function", "body": { "signature": { "params": [ { "name": "cost", "type": "Decimal" }, { "name": "pct", "type": "Percent" } ], "returns": "Decimal" }, "tier": "pure", "expression": "round(cost * (1 + pct / 100), 2)", "content_hash": "sha256:…" }}key— stable namespaced identifier used at call sites and for cross-component references.label— human-facing name.type—calc_function.body.signature/body.tier— the typed, tier-declared contract the compiler enforces.body.expression— the pure body.body.content_hash— the immutable revision identity a calling formula pins. Editingexpressionyields a new hash; the old revision remains resolvable for formulas still pinned to it.
Retrieve → diff → deploy. Retrieve emits the component above. A diff is a body/signature diff; because the hash is derived from the body, a changed body is a changed hash, so the version delta is visible in the diff rather than hidden inside a mutated in-place blob. Deploy registers the new revision, runs the registration-time checks (type-soundness, purity, pure-only-calls-pure), and migrates only the formulas the deploy explicitly re-pins.
Publish authority and the quality gate. Registering a calc function — and, in particular, publishing a new revision of a shared standard-library function — is a permissioned action, gated by a publish capability granted through an additive permission set assigned to a group, consistent with the platform’s permission model (there are no profiles; capability flows set → group). The gate that replaces Salesforce’s 75% coverage floor [S1] is not a coverage percentage — a pure function’s soundness is established statically, and a line-coverage number proves nothing a type/effect check does not. Two gates apply instead, both blocking at deploy: the mandatory static checks (type-soundness, purity, pure-only-calls-pure), which cannot be waived; and a set of declared characterization tests — golden input → expected output assertions stored with the revision — that must pass against the exact content-hashed body being published. Characterization tests pin observable behavior to the revision, so a later fn@v2 that changes an output is caught as a failing prior assertion rather than shipping silently. A standard-library revision additionally requires review by a holder of the publish capability; org-defined functions require whatever review the org’s permission sets encode.
Salesforce Metadata API analog. The nearest SF elements are ApexClass (fields: apiVersion, content as base64 source, fullName, status = Active/Deleted, packageVersions) and ApexTrigger. [S4] Both store source-in-a-blob with no content-hash revision identity; there is no formula-function metadata type to compare against, because none exists.
Sources
Section titled “Sources”The inline citation tags [S1]–[S10] used above resolve to the correspondingly numbered sources below:
- [S1] — Salesforce Help — Apex Unit Testing Best Practices and Code Coverage Requirements — 75% coverage; all triggers ≥1 line
- [S2] — Salesforce Developers — InvocableMethod Annotation (Apex Developer Guide)
- [S3] — Salesforce Help — Code coverage steps before deployment
- [S4] — Salesforce Developers — ApexClass (Metadata API Developer Guide)
- [S5] — Salesforce Developers — Formula Operators and Functions by Context
- [S6] — Salesforce Developers — FormulaFunction (Object Reference, Summer ’26 / API v67.0)
- [S7] — Salesforce Help — Custom Metadata Types and Advanced Formula Fields
- [S8] — Salesforce Summer ’26 Release Notes — searched; no user-defined formula-function feature found
- [S9] — Salesforce Developers — Execution Governors and Limits (Apex Developer Guide)
- [S10] — Bob Buzzard Blog — Evaluate Dynamic Formulas in Apex GA (Spring ’25) — secondary; corroborates the
System.Formula/FormulaEvalreverse-direction feature - [S11] — PostgreSQL — Numeric Types (§8.1, current) —
numericis exact arbitrary-precision; rounds half away from zero; slower than integer/float types - [S12] — Salesforce Developers — Formula Operators and Functions (
ROUND) —ROUNDbreaks ties by rounding half away from zero