Skip to main content
nodes.yaml declares the deterministic system nodes that orchestrate a flow. Each node is a map entry keyed by its id.

Node fields

object
required
A map of event name to handler declaration — the only required field. See Handler fields for every field a handler can declare.
string
Derived from the map key; an explicit id is accepted only when it equals the map key.
string
Derived (always system_node); an explicit value is accepted for compatibility and carries no information.
list
Derived from the event_handlers keys; an explicit list is accepted but adds nothing. Subscriptions never cross a flow boundary — qualified forms (<flow>/<event>) are retired and fail verify; cross-flow delivery is owned by the package connect block.
list
Derived from the handler emit sites; an explicit list is accepted but the analyzer warns on drift (produces_drift).
object
Per-node state and accumulator fields, keyed by entity.
string
The state table name for this node.
object
Gate field definitions.
list
Durable timers attached to this node (see below).
list
Node permissions, for example create_flow_instance.
One event is handled by at most one system node (single_node_per_event); two nodes on the same event is a boot error.

Timers

A timer is a durable time-based trigger declared on a system node. When the delay elapses the platform emits the timer’s event, which routes through the normal event loop. Timers are persisted, so they survive restarts: at boot the platform checks for any whose fire_at has passed and fires them.

Fields

string
required
Timer identifier, unique within the node.
string
required
Event name emitted when the timer fires. A handler somewhere in the flow must subscribe to it, or the timer fires into a dead end.
string
required
Duration string (72h, 30m, 7d) or a policy reference ({{policy.sla_timeout_hours}}h). How long after start_on the timer waits before emitting.
string
required
When the timer begins counting. Must be one of three canonical forms; see below.
string
Optional. When the timer is cancelled before firing. Canonical forms: state:<name> or event:<name> (boot is invalid here). If omitted, the timer can only stop by firing or by entity-terminal cleanup.
boolean
default:"false"
When true, the timer re-fires at every delay interval until cancelled or until the entity reaches a terminal state. When false (default), the timer is one-shot.

start_on / cancel_on canonical forms

Trigger values must be prefixed. Bare strings are boot errors: the platform does not infer state-vs-event intent. Unknown referenced states are boot errors; unknown referenced events are boot warnings.

Lifecycle

  1. Start. Timer begins when its start_on condition fires (entity enters the state, the event is delivered, or the platform boots). Platform persists (entity_id, timer_id, fire_at) to the timer store.
  2. Cancel. If cancel_on fires before fire_at is reached, the timer is cancelled and removed from the store. The entity reaching a terminal state also cancels active timers.
  3. Fire. When wall-clock reaches fire_at, the platform emits the configured event into the normal event loop. The event is routed by its declared subscribers like any other event.
  4. Recurring. If recurring: true, the timer re-arms for the next interval after firing.
  5. Crash recovery. Timers are durable. On restart, the platform checks the store and fires any whose fire_at is past, then resumes counting for the rest.
Timer events carry the standard runtime fields (event_id, run_id, entity_id) but no business payload; handlers read entity for context.

Examples

A one-shot SLA breach timer:
A recurring boot-anchored periodic timer (one fires for the whole runtime, not per entity):
See Stages, gates, and timers for the conceptual treatment alongside gates.

Example

nodes.yaml