Humans as a first-class actor
A human decision in Swarm is a typed, durable object — not a chat message and not a webhook callback. You choose per transition where a human belongs; everything else stays autonomous — autonomy is a dial, not a switch.Decision gates: pause a stage for a verdict
The primary surface is the stage gate — a stage that waits for a typed decision, with every outcome declared:schema.yaml
advances_to and emit are verified statically,
so “what happens if they reject?” has an answer at boot, not at 2am. Two rules to know before
your first gate:
- Gate emits carry only literals and
decision.<field>values — they cannot read the entity. To emit a business event enriched with entity data after an approval, advance to a small relay stage and let its handler build the event (gate → awarding → handler enriches from entity → terminal). Budget one relay stage per outcome that needs entity data. - Declare the platform as the producer of any event only a gate emits:
swarm: {producer: mailbox_human}on the event declaration — otherwise verify reportsevent_producer_exists(gate emits do not count as ordinary producers). The platform itself never decides — no timeout auto-approves; an expired card escalates or defers, loudly.
The mailbox
Gates pause a stage until someone answers; the mailbox is a queue of work items that do not block a stage. Gates produce decision cards; mailbox items carry decision sheets. Alongside gates, the mailbox is the persisted queue for human tasks — work items that are not a stage verdict (review this draft, investigate this alert). A handler writes an item with themailbox_write action, declaring the item type, severity (normal, urgent,
critical), a summary, and an explicit payload. If the same event is delivered twice, you
still get one item, not two.
Each item carries a server-built decision sheet: the entity context and a preview of
what happens downstream, so an operator can decide without reconstructing the situation by
hand.
nodes.yaml
action on a rules: branch. The matched rule owns its action; the unmatched branch does
not fire. Both branches run as part of the same atomic handler transaction:
nodes.yaml
Decisions
An operator acts on a pending item in one of three ways:- Approve: emits the configured downstream event, resuming the flow.
- Reject: records a reason; no downstream event.
- Defer: postpones the item until a given time.
mailbox.item_decided, which the flow
subscribes to in order to continue.
mailbox.item_decided is platform-emitted and auto-registered: subscribe to it
directly. Don’t declare it in your own events.yaml — the platform owns that name. The
payload carries mailbox_id, decision (approved | rejected | deferred | expired),
decision_payload, and source_entity_id, plus provenance fields; the full payload is in
the API reference.
nodes.yaml
Branch on
payload.decision in a rules: block (or in a guard) to handle approvals,
rejections, and deferrals differently. The handler above shows the approve path only.Budget escalations
Cost control surfaces through the same channel. Token usage is tracked per entity and per actor; when spend crosses a declared threshold the platform emitsplatform.budget_threshold_crossed with a level (warning, throttle, emergency), and
an emergency raises a mailbox item for a human.
Operate the mailbox
The CLI and API for acting on pending decisions.

