Skip to content

How Flows Execute

What determines which nodes run? The engine follows a flow’s edges and executes one ready node at a time. A false node condition skips that action. An inactive branch blocks its path. Completed Return response nodes supply the caller’s result.

For example, Invoice intake receives INV-105 with Amount 1500. Its review condition selects the review branch. The alternative skips. Join Paths lets a shared action proceed, and Return response supplies the invoice result.

Nodes with no incoming edges start in their saved node order. The engine awaits each ready node, including independent branches. An ordinary node waits for all of its incoming sources to finish before it can run. To expose a producer’s value in expressions, wire it directly into the consumer.

A node-only test supplies the selected node’s input values without executing upstream nodes. Testing a Run flow node still executes its child flow. Referenced flow inputs need explicit IDs and values, because saved flow test values are not a fallback for this test.

Why does a skipped action differ from a blocked branch?

Section titled “Why does a skipped action differ from a blocked branch?”

A false Only when records only_when_false and null outputs, and downstream nodes remain eligible. An inactive branch blocks its edges. A single blocked incoming edge prevents an ordinary shared action from running, with upstream_skipped.

Join Paths waits for every incoming source to finish or skip. Choose active path requires one active input. With none, it records no_active_path and blocks downstream work, unless continuation is enabled. With more than one active input, it fails. Pass through supports overlapping paths and retains nulls for blocked inputs. See Choose Execution Paths.

What happens during loops, waits, and failures?

Section titled “What happens during loops, waits, and failures?”

Loop iterations run one after another. For a failed child, iteration continuation records {index, status: "failed", outputs: null, error} and proceeds. A failed list expression or pre-child mapping still fails the loop. A child that completes with handled errors keeps its outputs, and the loop still reports errors.

Wait uses a durable timer that survives worker restarts. Execution waits for that timer, separately from the ordinary action timeout. Its output is true, and it does not pass an upstream payload through.

An unhandled node failure ends the flow. Continue on error stores {data: null, error: <message>}, marks the node completed_with_error, and allows downstream work. The containing flow also finishes completed_with_error unless it later fails.

Failure does not undo earlier writes. An invoice record created before a later failure remains. Read back the destination before rerunning.

StatusApplies toMeaning and next action
pending, runningWorkflow, flow, nodeWork has not finished.
completedWorkflow, flow, nodeCompare actual values with the intended result.
completed_with_errorWorkflow, flow, nodeInspect handled failures and iteration statuses.
failed, cancelledWorkflow, flow, nodeCheck effects before work stopped.
pausedWorkflow onlyA pause request takes effect at the next node or iteration boundary; it does not interrupt the current action or Wait. CLI polling stops with WORKFLOW_EXECUTION_PAUSED; its error detail contains the execution ID.
skippedNode onlyRead the skip reason; flows have no skipped status.

A completed_with_error run is terminal, but CLI run node and run flow exit with WORKFLOW_EXECUTION_FAILED. MCP run_node and run_flow return the execution result instead, so inspect its status.

Return leaves the scheduler running. Every completed Return writes every declared field. Untouched text writes "", and untouched non-text writes null. Later completed Returns overwrite earlier values, so use one complete Return per execution path. A successful flow with no completed Return returns {}.

Even after a completed Return, a later unhandled failure makes the flow’s top-level output null. Earlier external effects remain. Otherwise the child’s named outputs become its Run flow caller’s result, so inspect both sides of that contract.

LimitValueScope
Node executions25,000Shared across the run and child flows; scheduled skips count.
Subflow depthDefault 100; accepts 1–10000Root depth is zero; checked before the next child.
Loop itemsDefault 500; accepts 1–10000Over-cap lists fail before any iteration.
Wait durationDefault 1 second; 0–2,592,000 secondsUp to 30 days on the durable timer.
Ordinary node actionTwo hours; 60-second worker heartbeat timeoutOne attempt; the engine does not automatically retry the action.

Raising the item cap does not raise the node budget. A run of 5000 items with six body nodes each exceeds 25,000 before counting parent nodes.

By default, CLI run node waits 120,000 ms and run flow waits 300,000 ms. --timeout takes a positive integer in milliseconds. MCP waitSeconds defaults to 60 and caps at 120. A polling timeout does not cancel execution, so inspect its returned ID.

In the app: open the workflow’s Run history, select a run, then inspect Run details, node Configurations, inputs, and outputs.

With an assistant: these examples pin the workflow once, and MCP execution calls always require workflowId. Start by listing the workflows.

Terminal window
cai workflow list --limit 100 --json

Select the intended data.items[].id as <workflowId>. If several match, ask which one. If data.hasMore is true, increase the CLI limit or obtain the ID from the app. MCP caps the list at 100.

Pin it, then list the recent runs:

Terminal window
cai use <workflowId> --json
cai exec list --limit 100 --json

exec list defaults to 10 runs. Select data[].id as <workflowExecutionId>. A paused or timed-out run command supplies it in error.detail.workflowExecutionId instead.

Read that execution, then its node tree:

Terminal window
cai exec get <workflowExecutionId> --json
cai exec tree <workflowExecutionId> --limit 200 --json

The tree defaults to 50 rows, maximum 200, and omits payloads. While data.nextCursor exists, use it as <opaque> and request the next page:

Terminal window
cai exec tree <workflowExecutionId> --limit 200 --cursor '<opaque>' --json

Take the desired data.items[].id as <nodeExecutionId>. CLI JSON includes the raw output even without --full, which controls human rendering:

Terminal window
cai exec node <nodeExecutionId> --full --json

MCP uses execution_get/execution_tree with workflowExecutionId, and execution_node_get with nodeExecutionId and full: true. Continue with Debug a Run when values or paths differ from the intended result.