Node configuration
Every node has anodeId, a type (agent, human, or webhook), a config block, and an optional slaMs deadline. webhook nodes validate in a definition but their runtime handler isn’t enabled in v1, so this section covers the two runnable types. (The deferred webhook node is distinct from the inbound webhook handler and from per-execution webhook delivery, both of which are live in v1.)
Every node also accepts optional cosmetic name (1–200 chars) and description (≤ 2000 chars) fields, echoed verbatim in NodeView. They’re labels for visual graph editors and don’t affect runtime behavior.
Agent nodes
Human nodes
Exactly one ofreviewers[] (preferred) or reviewerIds[] (legacy) must be provided.
Rejection paths are edges
Ahuman node carries no rejection config. Its reject path is an outgoing on:"reject" edge — see the edge model below. Every human node must have one, or the definition is rejected with APPROVAL_HUMAN_NODE_REQUIRES_REJECT_PATH.
Edge model
All workflow transitions — approve routing, reject routing, group fan-out/in, and loop-backs — are expressed as a single unifiededges[] array. Each edge is { from, to, on?, when?, loop? }.
Reject, loop-back, and exhausted
-
Reject path: an outgoing
on:"reject"edge from the rejecting node. -
Loop-back: an
on:"reject"edge whosetois an ancestor offrom, marked withloop. The server derives the loop region (entry node, body, cap) from the marked edge. -
Exhausted handling: a sibling
on:"exhausted"edge from the samefrom, fired when the loop’smaxIterationscap is reached. Without it, an exhausted loop rolls the execution up tofailed.
Custom predicates
Useon:"custom" with a when expression to gate an edge on the source step’s output. when is a JSON-AST string — the engine parses it as JSON and evaluates it with a safe walker, never as JavaScript. No untrusted code ever runs. when is invalid on any non-custom edge.
includes, startsWith, endsWith, length, isEmpty.
Path roots:
An
on:"always" edge (the default when on is omitted) always fires.
Groups as edge sources
A group can be an edge source (from: { kind: "group", groupId }), giving it one collective branch instead of N per-member fan-outs:
waitAlledge-source: all members must terminate, then the group takes one collective branch by unanimity —approveif every member approved, elsereject. Provide both anon:"approve"and anon:"reject"branch; exactly one fires. Successor step ID:group_<groupId>__to__<childNodeId>. A routed collective reject is not a failed run — when the reject branch’s successor completes, the execution rolls up tocompleted, and a member whose rejection is handled by the group edge is surfaced ascompletedwithoutput.decision="reject", neverfailed.cancelOnQuorumedge-source: fires one collective approve-successor on approval-quorum (same asjoinOnQuorum). A forwardon:"reject"from ajoinOnQuorumorcancelOnQuorumgroup is rejected as a dead edge — those policies fan out only on approval-quorum.- Edge-to-group fan-out (
to: { kind: "group", groupId }) expands to one compiled edge per group member for all quorum policies.
The compiled view
EveryDefinitionView returns a read-only compiled block alongside the authored edges (which echo sourceEdges byte-for-byte): compiled.forwardEdges is the runtime forward-edge list (group endpoints expanded, on roles compiled to predicate ASTs) and compiled.loops is the server-derived loop region list. See Get Definition for the field schema.
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. You don’t declare loops directly — you mark anon:"reject" edge with loop (its to must be an ancestor of from), and the server derives the loop region (entry node, body, cap) on the compiled graph. Add a sibling on:"exhausted" edge to route when the cap is reached.
compiled.loops[]:
The default iteration predicate is
decision == 'reject' && rejectorMandatory == true. The unified contract does not support custom loop predicates on loop-back edges: loop is valid only on an on:"reject" back-edge.
Body-shape constraint
The derived loop 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.
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 validated at create and update time. Authored-edge provenance errors come fromcompileGraph; graph-shape errors come from the linter. Any violation is rejected with INVALID_ARGUMENT and an explicit code in the error message.
Edge model validation errors
The unifiededges[] contract is validated at create and update time. Violations are rejected with INVALID_ARGUMENT and one of these keys 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:

