This page is the specification — every field and constraint. For the guide with worked
shapes, see Accumulation.
accumulate step gathers arrivals for one entity and
fires only when a completion condition is met. materialize_from then copies the gathered
items into a typed entity field so the rest of the flow can read them.
This page covers the runtime behavior of accumulation. For the field list see
Handler fields; for how accumulation sits in the handler pipeline
see Handler execution model.
The accumulate step
Dedup defaults to the sender
dedup_by decides what counts as a distinct arrival. It defaults to sender (the sending
session’s id). That default is wrong whenever one sender legitimately contributes several
items, for example an agent emitting five dimension scores: the platform treats them as
duplicates and keeps only the first.
Completion modes
The expected count is not inferred. It is read from entity state through
expected_from,
and some earlier handler must have written it, typically the handler that fanned the work out
(write fan_out.count to an entity field with data_accumulation, then point expected_from
at it).
on_timeout is the branch the platform runs if the timer fires before completion. The partial
accumulated data is available to it.
Accumulation is idempotent
Duplicate arrivals (samededup_by key, same entity) are ignored. The received set is a set,
not a list, so crash recovery can replay events through the same tracking and reach the same
result: already-received items are skipped and the accumulator stays consistent.
Accumulated items are not entity fields
The gathered items live only in the accumulator. They are exposed asaccumulated.*, and only
inside on_complete conditions and filter expressions (both run after accumulation
completes). They are never promoted to entity fields automatically, so entity.* cannot
see them, and neither can a guard or an agent reading entity state.
To keep a typed copy on the entity, declare materialize_from.
Projecting to an entity field with materialize_from
materialize_from is declared on an entity field (in entities.yaml), naming the
accumulator to copy from:
- Same flow, matching accumulator. The source is
<node_id>.<accumulator_name>, the node must be in the same flow, and that node’s handler must declareaccumulate.intowith that name. - Both sides are named-type lists. Target and source must be
list<NamedType>(or[NamedType]).jsonb,[text], and untyped targets are rejected. projectonly when the item types differ. If the source and target item types are identical, omitproject(identity copy). If they differ,projectis required and must name every field of the target item type.projectreads onlysource.*,policy.*, and literals. Bindings toentity.*,payload.*,event.*,fan_out.*, oraccumulated.*are not allowed inproject, andsource.*resolves only against declared fields of the source item type (accumulator metadata such asevent_idorreceived_atis not addressable).materialize_fromis the sole writer of that field. After the create-timeinitial:value, nothing else may write it, notdata_accumulation, not astore_as, notclear, not an agent save tool. Any other authored write to the field is a boot error.
When the projection runs
The projection writes a full replacement list after the matchedon_complete branch’s
data_accumulation, and before emit.fields, so an emit in that handler sees the projected
value. It does not run for incomplete accumulation, for the on_timeout branch, or when
completion is reached with no matching on_complete rule.
Handler execution model
Where accumulation, projection, and emit sit in the dependency graph, and what overrides what.

