Skip to content

Roll-up summary fields

A roll-up summary is a stored aggregate on a parent record, derived from a set of child records (count, sum, min, max, and — on the CAOS kernel — more). It belongs to the pure tier: its value is a deterministic function of the child set and a filter predicate, with no effectful behavior. Unlike a formula field, which recomputes at read time from a single row, a roll-up is materialized on the parent row and maintained incrementally as children change, so reads are O(1) and the write pays the cost.

A roll-up summary has three parts: a target (the parent object and the column that holds the aggregate), a source (the child object, the relationship that links child to parent, and — for anything other than a count — the child field being aggregated), and an operation with an optional filter predicate over the child set.

The value is stored on the parent row, not virtual. It is maintained inside the transactional write phase of the save order (see Save order of execution): when a child is inserted, updated, deleted, or reparented, the affected parent’s aggregate is recomputed and persisted in the same transaction that commits the child change, before any author-defined effects run. This is storage-enforced — the maintenance is performed by the kernel’s write path (a trigger-maintained column against the parent), not by user automation — so the aggregate cannot drift from the committed child set the way a manually-maintained counter can.

Maintenance mode. The default is strict synchronous-in-transaction maintenance, matching Salesforce’s consistency guarantee: every read of the aggregate reflects the committed child set exactly, with no window of staleness. Mode is a per-roll-up policy, not a global switch. A roll-up over a hot, high-fan-in parent may declare debounced/async maintenance instead, trading strict real-time consistency for write throughput; that roll-up carries a pending-recompute freshness marker on the parent while an async worker converges, and its declared contract states the eventual-consistency semantics. A synchronous roll-up never exposes that marker because it is never stale.

Read/write semantics:

  • Read — every read of the parent returns the maintained value: opening the record, a list, and a document generated from it. A parent that no child save has reached yet reads null, which is not the same as a count or sum maintained to 0; an empty min, max or avg is maintained as null and reads null. A list can be sorted by a roll-up. Parents with no value sort after the rest ascending and before them descending, as empty values do on any field. A list cannot be narrowed by a roll-up yet.
  • Write — the aggregate column is not directly writable as a base value; it is computed. A roll-up may, however, be the computed input to an overridable value: effective = coalesce(override, rolled_up_aggregate). In that composition the base aggregate stays storage-maintained and the override is a separate nullable column, so provenance is explicit and the two never entangle.

Because the aggregate is pure, it is statically analyzable: the compiler records, for each roll-up, the parent, the child relationship, the summarized field, and the filter columns it reads. That dependency record is what lets the kernel propagate invalidations along true data edges across multiple levels (see Semantics & evaluation).

A roll-up is declared as a field on the parent object. The authoring surface is a metadata component (see Metadata & deploy representation); the vocabulary is fixed and enumerable.

Operations. The kernel exposes the four Salesforce-parity operations plus the additional Postgres-native aggregates:

Operation Meaning Allowed summarized field types Notes
count Number of children in the filtered set (no field) Incrementally maintained.
sum Total of a child field number, currency, percent Incrementally maintained (running total).
min Lowest value number, currency, percent, date, datetime Delete of the current extreme forces a re-scan (see costs).
max Highest value number, currency, percent, date, datetime Same re-scan cost as min.
avg Mean of a child field number, currency, percent Maintained as (sum, count); not a stored average recomputed per child.
count_distinct Distinct non-null values any comparable type Maintained exactly via a per-parent value-frequency structure (distinct = number of keys with frequency > 0).
min/max extensions — — Broader type support than a running sum, because ordering is defined on more types than addition.

Relationship. A roll-up is declared against any relationship from child to parent — a nullable, reparentable lookup is sufficient. There is no master-detail requirement (see How Salesforce does it). Null-FK children (children that point at no parent) contribute to no aggregate, or to an explicit sentinel bucket if one is declared.

Filter predicate. The child set may be restricted to rows matching a predicate. The predicate is a pure boolean expression over child columns using the standard comparison and set operators:

equals · notEqual · lessThan · greaterThan · lessOrEqual · greaterOrEqual
contains · notContain · startsWith · includes · excludes · within

The predicate compiles to a WHERE clause on the child table. Filter columns are added to the roll-up’s dependency record, so a child update that crosses the predicate boundary (entering or leaving the filtered set) correctly triggers maintenance.

Worked example. An invoice parent totals the price of only its included line items:

{
"key": "invoice.total_included_amount",
"label": "Total Included Amount",
"type": "rollup",
"body": {
"parentObject": "invoice",
"childObject": "invoice_line_item",
"relationship": "invoice_line_item.invoice_id",
"operation": "sum",
"summarizedField": "invoice_line_item.total_price",
"filter": {
"op": "and",
"conditions": [
{ "field": "invoice_line_item.status", "operation": "equals", "value": "Included" }
]
}
}
}

At save time, inserting, editing, deleting, or reparenting any invoice_line_item, or changing its status across the Included boundary, updates invoice.total_included_amount for the affected invoice(s) in the same transaction.

Where it sits in the save order. Roll-up maintenance runs in the transactional write phase, after shape+validate and as part of persisting the record, before author-defined after-save effects (see Save order of execution). One pass, no re-entrancy: a child write maintains the parent aggregate, and if that parent aggregate feeds a higher-level roll-up or formula, the invalidation propagates along the recorded dependency edges in topological order within the same transaction. Cycles are rejected at compile time by the dependency graph.

Multi-level propagation. Because every roll-up carries a static record of the fields it reads, the kernel propagates changes along the true data dependency, not along a proxy signal. This closes the class of staleness where an aggregate depends transitively on a value that changed without the immediate child being written. Grandparent-and-higher totals are expressed as roll-ups over lower-level roll-ups, and the graph orders their maintenance deterministically.

Null/blank handling. Nulls in the summarized field are excluded from sum, avg, min, max, and count_distinct, matching SQL aggregate semantics. count counts rows in the filtered set regardless of the summarized field (there is none). An empty child set yields 0 for count/sum and NULL for min/max/avg (no rows to order or average).

Precision & determinism. Aggregation runs at the numeric precision of the summarized column (currency and number columns are exact-typed in Postgres; avg over exact types is computed from the maintained exact (sum, count)). Given the same committed child set and filter, the aggregate is deterministic — it is a pure function of storage. Reproducibility follows from purity; there is no read-time nondeterminism (no now(), no current-user) permitted in a roll-up filter, because those are effectful inputs excluded from the pure tier.

Type contract. The declared roll-up column’s type must be compatible with the operation: numeric for sum/avg, order-comparable for min/max, integer for count/count_distinct. The compiler enforces the return type of the authoring context, rejecting, for example, a sum over a non-numeric child field.

Concern CAOS approach Salesforce exact limit (cited) Why SF’s limit exists Does the CAOS stack still need an equivalent?
Relationship type Any relationship, incl. nullable/reparentable lookups Master-detail only; lookups cannot be natively rolled up Master-detail guarantees a required, fixed parent (no orphan), cascade delete, inherited ownership/sharing, and a lockable single parent row — the contract that makes synchronous in-transaction maintenance safe No. On Postgres the parent-exists/locking guarantee is a foreign key plus SELECT … FOR UPDATE on the parent during maintenance, independent of ownership/cascade. The orphan case is handled explicitly via nullable FK.
Operations 4 SF-parity + avg, count_distinct, and percentile/median/string-agg under a declared exact-or-sketch contract Exactly 4: Count, Sum, Min, Max (no AVG/DISTINCT/median natively) Each extra operation is extra synchronous write work and a broader maintenance-correctness surface Partially — extra operations are first-class Postgres aggregates, but each carries its own maintenance cost (see below).
Roll-ups per object No fixed count cap; governed by a per-object write-cost budget that warns at a soft threshold and blocks at a hard one Default 25, hard max 40, raised 25→40 via a Support case (Help article 000334137) The cap bounds write amplification: every roll-up adds synchronous maintenance to every child DML Yes, in substance. Removing the count does not remove the physics; the cost budget accounts each roll-up’s maintenance cost per DML path — a truer control surface than a fixed number — but a control is still required.
Depth (rollup-on-rollup) Static dependency graph with a configurable depth ceiling computed and enforced at deploy time Master-detail chains up to ~3 custom-detail levels of rollup-on-rollup (documented across Salesforce guidance) Deep synchronous recompute chains multiply lock scope and transaction time Yes. One leaf write can cascade up an arbitrarily deep chain holding many parent locks; the compile-time graph measures chain depth and rejects a deploy that exceeds the configured ceiling.
Single-field span One roll-up = one level; higher levels chain roll-up-on-roll-up One level — a single RSF aggregates immediate children only Each hop is its own materialization Same structural shape; the improvement is graph-ordered propagation, not multi-level in one field.
Backfill / recompute Set-based SELECT … GROUP BY parent_fk, one planned aggregate “Force a mass recalculation,” up to 30 minutes Mass recompute is batched row-by-row over all parents/children Yes — a bounded backfill job is still needed as the trust-but-verify reconciliation, but it is a single planned statement, typically far faster for comparable volumes.

Salesforce mechanism. A roll-up summary field (RSF) is a stored aggregate on the master side of a master-detail relationship. It supports exactly four operations — Count, Sum, Min, Max — with Sum numeric-only (number/currency/percent) and Min/Max additionally over date and datetime. Maintenance is synchronous in the child’s save transaction: an insert/update/delete/undelete of a child in the aggregated set updates the parent’s stored value in the same DML. Drift is repaired by “Force a mass recalculation of this field,” which the Help documents as taking up to 30 minutes. Per-object count is default 25, hard max 40 (raised via a Support case). An invalid result freezes — the field stops recalculating and shows an invalid-value icon until the source data changes.

Salesforce’s requirement is master-detail because that relationship encodes the guarantees synchronous maintenance depends on: the child cannot be orphaned (the relationship is required and the parent is fixed at creation), delete cascades from parent to children, ownership and sharing are inherited from the parent (so the parent can be locked and updated without cross-owner permission checks), and the known-required parent row can be locked while children recompute. A plain lookup provides none of these, so Salesforce refuses native roll-ups over lookups. The community package DLRS (Declarative Lookup Rollup Summaries) exists precisely to add lookup-relationship roll-ups, AVG, and uncapped counts via Apex triggers/batch — direct evidence the native feature is deliberately narrow.

Salesforce also carries a correctness gap, not merely lag: if a summarized child field is a formula that references a cross-object/grandparent value, changing that upstream value does not refresh the RSF, because no child DML fires. The invalidation is keyed on child DML, not on the true data dependency.

What CAOS does better — mechanism-named, parity vs. improvement separated.

  • [BETTER] Roll-ups over any relationship. The master-detail requirement is a coupling artifact (ownership inheritance + cascade + locking bundled together). On Postgres, “parent exists and is lockable” is a foreign key + row lock, decoupled from ownership, cascade, and sharing. Roll-ups run over ordinary nullable, reparentable lookups — obsoleting the reason DLRS exists.
  • [BETTER] Invalidation on true data edges. The compile-time dependency graph propagates changes along the actual columns a roll-up reads, transitively across levels, in topological order. This closes the cross-object-formula staleness hole by construction, rather than leaving it as a documented gap.
  • [BETTER] More operations. avg (maintained as sum, count) and count_distinct (per-parent value-frequency structure) are exact and incremental; percentile/median and string aggregation are first-class Postgres aggregates under a declared exact-or-sketch contract (exact recompute for bounded child sets, t-digest/HyperLogLog with an advertised error bound at high fan-in).
  • [BETTER] Planner and generated columns. For read-mostly or low-cardinality parents, a generated column or an incrementally-maintained materialized aggregate can serve a roll-up, and the planner can use a covering index on (parent_fk, summarized_field) for min/max/count. Salesforce exposes no planner and no index control.
  • [BETTER] No invalid-value freeze. Because maintenance is storage-enforced and in-transaction, a synchronous roll-up cannot enter Salesforce’s frozen/invalid state — it is always consistent with the committed child set, so there is no icon to surface. The only staleness the platform surfaces is explicit and named: override drift in the effective = coalesce(override, aggregate) composition (the recomputed aggregate differs from a pinned override) is shown through the standard non-blocking banner with provenance from field history, and a roll-up that opted into async maintenance carries a pending-recompute freshness marker until its worker converges. A documented precedence chain and two named freshness states replace one opaque freeze.
  • [PARITY] Stored aggregate maintained in-transaction; the four core operations with filters; a force-recalc/backfill path. These we match; the backfill is a single set-based statement rather than a 30-minute row-by-row batch.

Costs and risks.

A roll-up is a metadata component with the canonical shape used across CAOS logic components — key, label, type, body:

{
"key": "invoice.total_included_amount",
"label": "Total Included Amount",
"type": "rollup",
"body": {
"parentObject": "invoice",
"childObject": "invoice_line_item",
"relationship": "invoice_line_item.invoice_id",
"operation": "sum",
"summarizedField": "invoice_line_item.total_price",
"filter": {
"op": "and",
"conditions": [
{ "field": "invoice_line_item.status", "operation": "equals", "value": "Included" }
]
}
}
}

The lifecycle is retrieve → diff → deploy: the component is retrieved as canonical JSON, diffed against the target environment’s component of the same key, and deployed. Deploy is ordered — the relationship and the summarized child field must exist in the target before the roll-up that references them, and the kernel installs or updates the maintenance path (the trigger-maintained column and its dependency-graph edges) as part of applying the component. See Metadata & deploy.

Salesforce Metadata API analog. The corresponding element is a CustomField with type Summary, carrying:

  • summaryOperation — SummaryOperations enum: Count, Min, Max, Sum.
  • summarizedField — the child field being summarized, ChildObject.Field (must be non-null unless the operation is Count).
  • summaryForeignKey — the master-detail field on the child, ChildObject.RelationshipField.
  • summaryFilterItems — a list of FilterItem (field, operation, value, valueField), where operation is the FilterOperation enum (equals, notEqual, lessThan, greaterThan, lessOrEqual, greaterOrEqual, contains, notContain, startsWith, includes, excludes, within).

The CAOS body maps 1:1 onto these, with relationship generalizing summaryForeignKey to any relationship (not only a master-detail field).