Node configuration
Every node has anodeId, a type (agent or human), a config block, and an optional slaMs deadline.
Agent nodes
Human nodes
Exactly one ofreviewers[] (preferred) or reviewerIds[] (legacy) must be provided.
Per-human-node onReject shorthand
Authors can express the rejection path directly on a human node instead of declaring a top-level loops[] region or hand-writing an edge with when: decision == 'reject'. Two mutually-exclusive forms:
Form A — route on reject (synthesizes an edge):
Strict-mode validation
Everyhuman node MUST satisfy ONE of:
config.onRejectis set (any form), OR- The node is a body member of a top-level
loops[]entry.
INVALID_ARGUMENT with message Human nodes missing a reject path: <list>. This forces explicit rejection routing — silent dead-ends on rejection are a bug.
What desugaring rejects
TheonReject shorthand is converted to canonical edges[] + loops[] at definition write time. The pass returns INVALID_ARGUMENT for:
onReject.routeToNodeId on a joinOnQuorum group member is dead code
The shorthand satisfies the strict-mode reject-path rule, but at runtime joinOnQuorum suppresses per-member fan-out — so the synthesized reject-gated edge can never fire. If you want per-rejecter routing on parallel reviewers, use cancelOnQuorum or waitAll (per-member fan-out preserved), or wrap the group in a top-level loops[] body so the loop interceptor handles rejections.
Canonical form after desugaring
The stored definition hasonReject STRIPPED from human node configs and the synthesized edges/loops appended to the top-level arrays. GET /definitions/get returns the canonical form (auto-synthesized loops carry a loopId prefix auto_<nodeId>_loopback). If you want to preserve your original input, store it on your side.
Edge gating expressions
Edges can carry an optionalwhen expression evaluated against the source step’s output. Expressions compile at write time (pure AST, no eval) and walk at runtime. No untrusted code ever runs.
includes, startsWith, endsWith, length, isEmpty.
Path roots:
If
when is omitted, the edge always fires.
SLA and breach handling
SetslaMs on any node to give the step a deadline. If the step doesn’t complete within the window, it transitions to breached and emits a step.breached event.
To handle breaches, declare an outgoing edge that routes on the breached status. Otherwise the engine emits the missing-breach-edge linter rule and rejects the definition. Silent dead-ends are a bug.
Parallel groups and quorum policies
A parallel group declares a set of member nodes that conceptually run in parallel and share an approval threshold.Quorum is approval count, not completion count
A member counts as an approval when its terminal status iscompleted AND its output.decision === 'approve'. Rejections, failures, breaches, and cancellations contribute to the completion counter only, never the approval counter.
Two consequences:
- Non-blocking agent members never satisfy quorum. They have no decision concept. Only place human or blocking agent nodes inside groups whose policy is
cancelOnQuorumorjoinOnQuorum. - A
rejectdoes not block the group from rolling up to “complete”. It just keepsapprovedShardsfrom advancing. Group-completion (expectedStepsmet) and group-quorum (approval threshold) are tracked separately.
onQuorumMet policies
Specific-must-approve quorum
By default, quorum is anonymous: any N approvals out of M members trigger the policy. To express “these specific members must approve”, declare them inrequiredNodeIds:
- Every
nodeIdinrequiredNodeIdsis among the approvers, AND - Total approval count reaches the numeric
quorum.
brand alone approving doesn’t satisfy quorum even if quorum: 2 is reached numerically. legal AND finance must both also approve. If requiredNodeIds is omitted or empty, behavior collapses back to anonymous quorum.
Loop regions
A loop region lets a workflow re-enter an earlier node when a reviewer rejects, instead of failing outright. Declare loops at the top level of a definition, peer togroups[]:
Body-shape constraint
The body must be one of:- Single-terminal sequential. Exactly one body node has outgoing edges that leave the body — that node is the iteration-terminal.
- Group-bounded. The set of exit-bearing body nodes equals the
memberNodeIdsof one parallel group withonQuorumMet: 'joinOnQuorum', every member lies inside the body, and the group hasquorum === expectedSteps.
loop-body-must-have-single-terminal.
When to use a loop vs. the onReject shorthand
- Use a loop region when multiple parallel reviewers should share a single retry counter, when the retry zone needs to contain a
joinOnQuorumgroup as a unit, or when you want explicit control over theloopId. - Use the
onRejectshorthand on a single human node for simple per-node reject routing (route to a different node on reject, or kick back to a single earlier node).
previousAttempts payload threaded into iteration N+1
The entry step of iteration N+1 receives:
Iteration-scoped parallel groups
When a parallel group lives inside a loop body, each iteration gets fresh quorum state — the container path becomesparallelGroups/<groupId>_iter_<N> internally. You don’t address this directly, but it explains why per-iteration quorum starts from zero.
Loop events
Two new event types fire alongside the standardstep.* and execution.* events. Both are returned from Get Execution Events.
Linter rules
Definitions are linted at create and update time. Any rule violation is rejected withINVALID_ARGUMENT and an explicit code in the error message.
Events
For receiver setup (signature verification, security rules, delivery basics), see Setup, Configure your webhook receiver.Event reference
Externally-visible events delivered via webhook and returned from Get Execution Events:Internal-only events (
step.scheduled, step.started, step.retried, step.resumed, step.response-recorded, step.overridden, parallel-group.completed, idempotency.suppressed) fill seq gaps but are filtered from external delivery. Your stream may have non-contiguous seq values.Cancellation reasons
step.cancelled events carry a data.reason string. This is an open string set. Consumers should switch on event.type for control flow, not on data.reason.
Webhook retry policy
After 5 failed retries, the payload is written to a dead-letter queue. Recover missed events via Get Execution Events with
sinceSeq.
At-least-once delivery. The same eventId and seq appear on retries. Make your receiver idempotent on (executionId, seq).
Errors
All errors follow the standard envelope:Canonical codes
Schema-level validation errors
Rate limiting
Rate limits are applied per API key, with additional per-endpoint tiers on high-volume routes. ARESOURCE_EXHAUSTED error indicates you should back off with exponential retry. Dispatch retries are safe to replay with an idempotencyKey.
Object reference
output (after resume) includes the aggregator rollup:
joinOnQuorum group successor steps, input includes:

