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.
The model
Section titled “The model”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 to0; an emptymin,maxoravgis maintained asnulland readsnull. 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).
Authoring
Section titled “Authoring”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 · greaterOrEqualcontains · notContain · startsWith · includes · excludes · withinThe 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.
Semantics & evaluation
Section titled “Semantics & evaluation”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.
Limits, and the reasons behind them
Section titled “Limits, and the reasons behind them”| 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. |
How Salesforce does it
Section titled “How Salesforce does it”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 assum, count) andcount_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)formin/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 apending-recomputefreshness 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.
Metadata & deploy representation
Section titled “Metadata & deploy representation”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—SummaryOperationsenum:Count,Min,Max,Sum.summarizedField— the child field being summarized,ChildObject.Field(must be non-null unless the operation isCount).summaryForeignKey— the master-detail field on the child,ChildObject.RelationshipField.summaryFilterItems— a list ofFilterItem(field,operation,value,valueField), whereoperationis theFilterOperationenum (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).
Sources
Section titled “Sources”- Salesforce Metadata API Developer Guide —
CustomField - Salesforce Help — Roll-Up Summary Field
- Salesforce Help — Increase the Maximum Limit of Roll-Up Summary Fields (000334137)
- Trailhead — Optimize Roll-Up Summary Fields
- Gearset — How to create and deploy roll-up summary fields
- Ksolves — A Guide to Roll-Up Summary Field
- Traction Complete — Rollup Summary Field: 3 Limitations
- Salesforce Ben — Guide to DLRS
- DLRS — Declarative Lookup Rollup Summaries (SFDO-Community)