Skip to content

The logic model

Business logic in Cloud Atlantis OS is not a formula dialect beside a real programming language beside a workflow tool. It is one typed language, and the only real distinctions among the constructs built on it are what type a piece of logic returns and whether it may cause effects. This page is the frame the rest of the Business logic section hangs on: formula fields, roll-up summary fields, validation rules, record-triggered automation, calc functions, and overridable values are the same language wearing different contracts.

Every piece of logic belongs to one of two capability tiers.

  • Pure logic reads a record and returns a value with no side effects. It is evaluated at read or save time, is deterministic, is statically analyzable, and can be pushed down into the query. The pure constructs are formula fields, the input expression of a roll-up, validation rules, a field type’s validators, and calc functions.
  • Effectful logic is record-triggered automation. An event fires it, it runs sandboxed, and it may write other records or reach outward. Automation runs in a bounded before-write window (adjusting only the triggering row), an after-write window (wider effects), or post-commit — the sequence is fixed by the save order of execution.

The tier is not a syntax choice. It is a property the compiler enforces against the contract of the surface the logic is authored on.

The authoring surface declares two things the compiler checks: the return type it will accept and the tier it permits. A validation slot demands a boolean, pure. A formula slot demands the field’s declared type, pure. An automation body permits effects. The editor is the same in every case; only the contract differs, and it is checked as the logic is written rather than discovered at run time.

The pure tier is a subset, not a separate language

Section titled “The pure tier is a subset, not a separate language”

The analyzable tier is not a second formula language sitting beside a real one. It is the same syntax under a compiler-enforced contract: no writes, no I/O, only calls to other pure units. Two independent boundaries live inside the one language:

  • Expression versus procedure. An expression admits only collection-bounded iteration — map, filter, and reduce over a known, finite collection already in scope, such as a related list. Its iteration domain is statically known, so the dependency graph and cost are computable before anything runs, and it lowers to a set operation the query planner runs at once rather than a row-by-row loop. There is no unbounded or open loop form (while, or a for with a data-dependent bound) anywhere in the expression surface. When an expression outgrows operators, or an algorithm needs genuine step-by-step iteration, it is extracted into a calc function — procedural, but still pure, its termination bounded operationally by the runtime rather than by the grammar. Algorithmic power is gained without leaving the analyzable tier.
  • Pure versus effectful. Writes and outbound effects are reached only by moving to automation, whose contract permits them.

The language never changes across either boundary; only the contract does.

Why constrain the pure tier — four guarantees

Section titled “Why constrain the pure tier — four guarantees”

The restriction is the feature. A pure, analyzable subset buys four things an unrestricted language cannot:

Guarantee What it enables
Purity Safe to evaluate on every read — no hidden effect to roll back, no per-call sandbox.
Analyzability Exact dependency extraction, where-used across the org, and cycle detection before anything runs.
Push-down A pure expression compiles into the data query and evaluates over a whole set at once, instead of row-by-row in application code.
Small safe surface The evaluator that runs on every read is small and closed — no file system, no network, no unbounded loops (only collection-bounded map/filter/reduce). The large surface stays confined to the effectful tier, which runs far less often and inside a sandbox.

A field’s type carries its own validators, so a formula that targets a Phone field must return a value that satisfies the Phone contract — the compiler rejects one that could yield a non-phone. Return type and tier are the only real distinctions between the constructs; the type system carries everything else. That is why one expression editor serves all of them, and why a value produced by a formula, an override, or a roll-up is interchangeable wherever its type is expected.

The one language is TypeScript — its syntax, its type system, its tooling — not a formula dialect that resembles it. The pure tier is the compiler-enforced subset of that grammar; the effectful tier is the same grammar under a contract that permits effects. A single notation means logic reads the same in a formula, a validation rule, a calc function, and an automation body, and one editor, type checker, formatter, and autocomplete serves all of them.

There is no IF(a, b, c), no AND(x, y), no & for concatenation, and no uppercase built-in vocabulary anywhere in the platform. Those belong to a spreadsheet-descended formula language, and adopting one would make “one language” false by the second page of documentation. The equivalents are the operators the grammar already has:

// a formula field: invoice.margin_pct → Percent
record.sell_price > 0
? (record.sell_price - record.total_cost) / record.sell_price
: 0
// a validation rule: pure, must return boolean, true = valid
record.status != "Confirmed" || record.unit_price != null
// crossing relationships is typed property access
record.account?.owner?.region ?? "Unassigned"

Three properties fall out of that choice rather than being designed in. Several incumbent built-ins cease to exist as functions: IF is ? :, BLANKVALUE is ??, ISCHANGED(f) is record.f != prior.f, ISNEW() is prior == null. Migration from a formula dialect is a mechanical transpile over the AST, not a hand rewrite. And the analyzable contract is expressed in the grammar itself — the pure subset has no assignment, no statements, no new, no await, so an expression can only evaluate to a value.

The subset departs from stock TypeScript in exactly three places, each forced by a platform guarantee: === does not exist (nothing to distinguish without implicit coercion), numbers are exact decimal rather than IEEE (below), and the pure tier admits only collection-bounded iteration. Everything else a TypeScript developer knows transfers unchanged. The grammar is fixed together with the calc-function runtime, which shares it.

Numbers are exact decimal, never IEEE binary floating point. Each numeric field is a Postgres numeric(p,s) column with a declared precision and scale, and all pure-tier arithmetic evaluates in that exact decimal domain. The standard rounding rule is round half away from zero — the native behavior of Postgres numeric (datatype-numeric), which also coincides with the round-half-up rule Salesforce documents for its Number type. Scale is enforced by the column itself, so a stored value can never carry more fractional digits than its field declares.

Exact decimal is a determinism requirement, not a preference. Binary floating point is non-associative across parallel evaluation and across hardware, so it cannot guarantee identical results from identical inputs — the property the pure tier exists to provide. Exact decimal arithmetic over a declared scale can.

There is a single typed editor: the record’s fields and the calc-function library are in scope with autocomplete, the context’s return-type-and-tier contract is shown and enforced live, and what is written is versioned, diffable, and testable like any other component. The builder-versus-code split dissolves into on-ramps onto the same surface — a field picker, a saved snippet, or a natural-language request each generate the same readable code a builder could have typed by hand, and what they produce can always be opened and edited directly.

Salesforce splits business logic across genuinely separate languages with a hard wall between them: a closed formula-function grammar (no user-defined functions, and a formula cannot call Apex), Apex as an unrestricted imperative language with no purity contract, and a flow-authoring surface distinct from both. The restricted tier itself is not the mistake — a small analyzable expression language is the right instinct. The mistake is that the tiers are different languages a builder must abandon at each boundary, and that the escape hatch (Apex) drops all the guarantees at once: purity, analyzability, push-down, and reproducibility. One language in two compiler-enforced tiers keeps the guarantees on the pure side of a boundary drawn by capability, not by vocabulary.