Skip to main content
This is the deep-dive reference. Setup covers the happy path. This page covers everything you reach for when you need precise control over node behavior, edge routing, quorum policies, SLA breaches, and event consumption.

Node configuration

Every node has a nodeId, a type (agent or human), a config block, and an optional slaMs deadline.

Agent nodes

Human nodes

Exactly one of reviewers[] (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):
Form B — loop back on reject (synthesizes a top-level loop region):

Strict-mode validation

Every human node MUST satisfy ONE of:
  1. config.onReject is set (any form), OR
  2. The node is a body member of a top-level loops[] entry.
Otherwise the API returns 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

The onReject 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 has onReject 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 optional when 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.
Supported operators: equality, comparison, boolean, regex, includes, startsWith, endsWith, length, isEmpty. Path roots: If when is omitted, the edge always fires.

SLA and breach handling

Set slaMs 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 is completed AND its output.decision === 'approve'. Rejections, failures, breaches, and cancellations contribute to the completion counter only, never the approval counter. Two consequences:
  1. Non-blocking agent members never satisfy quorum. They have no decision concept. Only place human or blocking agent nodes inside groups whose policy is cancelOnQuorum or joinOnQuorum.
  2. A reject does not block the group from rolling up to “complete”. It just keeps approvedShards from advancing. Group-completion (expectedSteps met) 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 in requiredNodeIds:
Quorum-met now requires both:
  1. Every nodeId in requiredNodeIds is among the approvers, AND
  2. Total approval count reaches the numeric quorum.
In the example, 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 to groups[]:

Body-shape constraint

The body must be one of:
  1. Single-terminal sequential. Exactly one body node has outgoing edges that leave the body — that node is the iteration-terminal.
  2. Group-bounded. The set of exit-bearing body nodes equals the memberNodeIds of one parallel group with onQuorumMet: 'joinOnQuorum', every member lies inside the body, and the group has quorum === expectedSteps.
The linter rejects other shapes with 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 joinOnQuorum group as a unit, or when you want explicit control over the loopId.
  • Use the onReject shorthand 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 becomes parallelGroups/<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 standard step.* 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 with INVALID_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. A RESOURCE_EXHAUSTED error indicates you should back off with exponential retry. Dispatch retries are safe to replay with an idempotencyKey.

Object reference

For human steps, output (after resume) includes the aggregator rollup:
For joinOnQuorum group successor steps, input includes: