check reads entity, payload, and policy. on_fail says what happens when the check
fails; the values are covered below. A guard reads the entity before this
handler’s own writes, so it reflects what earlier handlers left, not what this one is about to
change.
Multiple checks
When a guard has several conditions, write them as a list underchecks instead of one long
&& expression. Each entry carries its own id:
id of the check that failed,
so you can see which condition blocked
the handler instead of just learning that some combined expression came back false. In
production this matters: a callback handler usually needs two separate checks — one that the
callback belongs to this entity, and one that it has not already been processed — and naming
them separately tells you which one fired.
The list is evaluated top to bottom and stops at the first failure, so every check must pass for
the guard to pass. There is one on_fail for the whole guard; an individual check cannot set its
own. If you supply checks, the singular check is ignored, so use one form or the other, not
both.
on_fail
on_fail chooses what happens when a check fails. It defaults to reject. The chosen value
becomes the handler’s trace outcome, visible in swarm trace and recorded on the event
delivery row, so picking the right one is what makes guard failures legible in operations.
Choosing between them
reject: the default. The event tried, the guard said no, it’s recorded as a failure. Operationally noisy on purpose; a rejection rate worth alerting on.discard: the expected-failure value. A handler that subscribes topayment.receivedbut only acts oncurrency == "USD"usesdiscardfor everything else: not interesting, not an error.kill: terminal cliff. The entity has hit a hard policy boundary (rate limit, fraud threshold, blocked region) and shouldn’t continue, ever. Advances to terminal; no recovery path from the guard alone.escalate:{event}: routes the failure somewhere useful instead of just stopping. The escalation event is a normal event with a handler somewhere downstream.
Escalate emits with no implicit passthrough
Payload construction for an escalate event is covered in detail on Emitting events. Whenescalate:{event} fires, the emitted event follows the same payload-construction rules as
a declarative system-node emit: block. Nothing is auto-copied from the triggering event: the
payload stays empty unless the event declares fields and a matching escalate_payload:
supplies them. The runtime-owned envelope (event_id, run_id, entity_id, etc.) is still
attached; only business fields need authoring. Schema validation still runs fail-closed, so an
escalate event that declares required fields without authored values fails at the emit step
rather than silently shipping a half-filled payload.
The practical consequence: if rate_limit.exceeded declares reason: text, you must author
reason somewhere; the runtime won’t fill it from payload.reason on the triggering event.
What a failed guard leaves behind
When a guard fails, the handler stops: no state change, no data writes, no emit (other than theescalate one if configured). Guard failures are business logic, not transient errors,
so they are not retried. See the execution model for where the
guard sits in the pipeline.
