Skip to content

The execution model

Predicting what a workflow will do requires the real execution model — not a plausible one. This page states it precisely.

The engine runs one flow frame as a single queue: it takes the next ready node, runs it to completion, then takes the next. Two branches with no dependency between them are interleaved, never concurrent. Loop iterations run one after another.

Consequences:

  • Node order within independent branches is not guaranteed — never build logic that depends on “branch A finishes before branch B”.
  • Side effects happen one at a time, which makes execution traces exact and replayable.
  • A slow node delays everything after it in the queue. For long waits, that is fine: Wait is a durable timer (up to 30 days) that survives restarts and doesn’t burn compute.

A node becomes ready when every incoming edge’s source has finished. Then:

  • If all inputs arrived normally, it runs.
  • If any incoming edge comes from a branch that was not taken, the node is blocked — it is skipped, and the block propagates to everything downstream of it.
  • Join Paths is the one exemption. It runs when any incoming branch arrives, which makes it the required primitive for converging conditional branches — not an optional tidiness node.

Each input handle accepts exactly one incoming edge; a second connection to the same handle is refused.

A skipped node records why — read it instead of guessing:

ReasonMeaning
only_when_falseThe node’s own run-condition evaluated false.
upstream_skippedSomething it depends on was skipped.
all_branches_blockedEvery path leading to it was an untaken branch.
no_active_pathNo active route reached it this run.

Node and flow executions: pending, running, completed, completed with error, failed, skipped, cancelled. Workflow executions can additionally be paused — a running execution can be paused, resumed, or cancelled from run details or the API.

Every node execution persists three inspectable layers — the foundation of all debugging:

  • Inputs — the actual values that arrived.
  • Resolved configuration — every prop after expression evaluation. A null or empty resolved value on a green node is a failure in disguise (0 and false are valid; empty is not).
  • Outputs — the values downstream nodes will see, on named output ports.
Terminal window
cai exec tree <workflowExecutionId>
cai exec ports <nodeExecutionId> --json

Node errors have four types: input errors (a wired value didn’t satisfy the input — a data-type mismatch fails loudly here), process errors (the action itself failed: missing parameter, bad value, API error), output errors, and unknown errors.

The failure mode that does not announce itself: expressions null-propagate. A missing field doesn’t throw — it flows through as empty, and comparisons against empty values fail closed (match nothing). The symptom is a green run with wrong or empty results; the diagnosis is reading resolved configs, not error messages. A completed run with empty output should be treated as a failure until the values are read and explained.

Off (default): a node failure aborts the flow frame immediately. On: the failed node’s output becomes null, the main path continues, and the run ends completed with error. Enabling it also offers an optional Error output port carrying the error details — wire it to build a recovery branch. Toggling continue-on-error off deletes that port and every edge from it.

A flow’s output comes from Return response nodes. When several complete in one run, their result objects merge in execution order — a later node silently overwrites an earlier value for the same field. One Return response per branch end is the predictable pattern.

LimitValue
Node executions per run25,000 — the run fails when exceeded.
Subflow nesting depthWorkflow setting, default 100 (configurable 1–10,000).
Wait node durationUp to 30 days, durable.

The per-run budget is the one loops hit: a flow over 5,000 items with 6 nodes inside the iteration exhausts it.

This run is green but the output is empty. Walk the execution tree, find the first node whose resolved config or output is empty, and explain why.
These two branches need to converge on one Slack message. Wire it so the message sends when either branch runs — use Join Paths, don't wire both edges into the Slack node.