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.
How do edges determine order and values?
Section titled “How do edges determine order and values?”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.
| Status | Applies to | Meaning and next action |
|---|---|---|
pending, running | Workflow, flow, node | Work has not finished. |
completed | Workflow, flow, node | Compare actual values with the intended result. |
completed_with_error | Workflow, flow, node | Inspect handled failures and iteration statuses. |
failed, cancelled | Workflow, flow, node | Check effects before work stopped. |
paused | Workflow only | A 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. |
skipped | Node only | Read 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.
Does Return response stop execution?
Section titled “Does Return response stop execution?”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.
Which limits can end the work?
Section titled “Which limits can end the work?”| Limit | Value | Scope |
|---|---|---|
| Node executions | 25,000 | Shared across the run and child flows; scheduled skips count. |
| Subflow depth | Default 100; accepts 1–10000 | Root depth is zero; checked before the next child. |
| Loop items | Default 500; accepts 1–10000 | Over-cap lists fail before any iteration. |
| Wait duration | Default 1 second; 0–2,592,000 seconds | Up to 30 days on the durable timer. |
| Ordinary node action | Two hours; 60-second worker heartbeat timeout | One 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.
Where can I see what happened?
Section titled “Where can I see what happened?”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.
cai workflow list --limit 100 --jsonSelect 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:
cai use <workflowId> --jsoncai exec list --limit 100 --jsonexec 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:
cai exec get <workflowExecutionId> --jsoncai exec tree <workflowExecutionId> --limit 200 --jsonThe tree defaults to 50 rows, maximum 200, and omits payloads. While data.nextCursor exists, use it as <opaque> and request the next page:
cai exec tree <workflowExecutionId> --limit 200 --cursor '<opaque>' --jsonTake the desired data.items[].id as <nodeExecutionId>. CLI JSON includes the raw output even without --full, which controls human rendering:
cai exec node <nodeExecutionId> --full --jsonMCP 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.