Stages: the lifecycle
A flow declares its lifecycle inschema.yaml as a stages: map:
schema.yaml
- One stage at a time. An entity is always in exactly one stage, and only a handler’s
advances_tomoves it — to a single named stage, never a list. - Stages belong to the flow. There is no global stage enum;
assignedin your ticket flow andassignedin someone else’s are unrelated. - You never declare a state field. The entity’s current stage is platform-tracked;
expressions read it as
_entity.current_state(_entity.*is platform metadata,entity.*is your declared fields).
advances_to enters the stage (it runs in the same transaction as the transition), and
time-based follow-ups belong to the stage’s timers.
A flow that declares no stages: is stateless: it processes events without a lifecycle,
and advances_to is invalid there. Stateless still allows entities — a handler that writes
entity fields automatically creates one, with no stage attached.
The stage is also where the lifecycle’s machinery attaches, declared on the stage itself
rather than scattered through handlers:
- Timers (
stages.<stage>.timers) fire when an entity has sat in the stage too long — see Timers below. - Decision gates (
stages.<stage>.gate) pause the stage for a typed human verdict with every outcome declared — see Human in the loop. - Joins wait in a stage for a declared set of arrivals, with dedup and timeout owned by the platform — see Parallel work.
Terminal stages
Terminal is a one-way door. Once an entity is terminal:- New events targeting it are rejected before any handler runs (recorded as
terminal_reject). - Its timers are cancelled and its agent sessions terminated.
- All of its state is preserved: queryable, never deleted.
Gates
A gate is a named boolean on the entity, set bysets_gate and checked by guards. Gates
model “which milestones are complete” in validation-style flows. They default to unset and
are not auto-reset; clear_gates: true resets every gate on the entity at once (it is
all-or-nothing). Gate names are scoped to the flow, so they stay stable as instances come
and go.
Timers
Time is durable and declarative, in two forms with one owner each. Stage timers are the staged-lifecycle form — declared on the stage, armed when an entity enters it, cancelled when it leaves:schema.yaml
timers: list is one timer. An entry with advances_to is part of the
state machine — it shows up as a real edge when you inspect the flow
(swarm describe --graph). This is the canonical form for “an entity sat here too long.”
Node timers cover what stages cannot anchor: event-triggered delays and boot-anchored
periodic work (a nightly report timer with start_on: boot, recurring: true). They are
declared on a system node with prefixed triggers (state:/event:/boot); see
nodes.yaml for the full form.
Both forms are persisted: a timer survives restart, and one that came due while the process
was down fires on recovery. When any timer fires, its event enters the normal event loop like
any other — timers are just delayed event producers.
