Flow control
Flow control starts with one decision table. Pick by what you want to happen, not by what sounds familiar:
| You want to… | Use | Semantics |
|---|---|---|
| Skip one action when a condition is false | Only when (node condition) | The node is skipped; execution continues past it. |
| Choose exactly one path | If/Else | Ordered branches, first match wins, mandatory Else. |
| Run every matching path | Router | Each route’s condition evaluated independently — multiple routes may run. |
| Converge conditional branches | Join Paths | The one node that runs when any incoming branch arrives. |
| Repeat work per item | Run flow on list | Sequential iterations of a child flow. |
| Reuse logic | Run flow | Call another flow, block until it returns. |
| Name a computed constant | Set value | Named values from expressions. |
| Delay | Wait | Durable timer, up to 30 days. |
| Return flow output | Return response | Writes the flow’s output fields. |
Only when: skip, don’t block
Section titled “Only when: skip, don’t block”Any standalone action node can carry an Only when condition (it’s an opt-in toggle; triggers, Return response, Router, If/Else, and Join Paths don’t have it). When it evaluates false, the node is skipped — its outputs resolve null and its outgoing edges are still followed, so downstream nodes run and see nulls.
That is the opposite of branch blocking: a node on an untaken If/Else or Router route is blocked, and the block propagates downstream (skip reason upstream_skipped). Only-when skips don’t propagate; branch blocks do. Debugging a run tree requires this distinction — see the execution model.
If/Else: exclusive choice
Section titled “If/Else: exclusive choice”Branches are ordered and evaluated top-down; the first matching branch runs and the rest are blocked. The Else branch is mandatory and stays last — the node refuses to run without one. A branch with a blank condition is a runtime error, not a no-op. Branch conditions are authored with cai branch add/update; order matters, so cai branch reorder is behavior, not cosmetics.
Do not build exclusivity by hand with opposing Only-when conditions on sibling nodes — If/Else guarantees it structurally.
Router: deliberate fan-out
Section titled “Router: deliberate fan-out”Router conditions live on its own output routes and are evaluated independently — every true route runs. There is no first-match short-circuit. Use Router when multiple matching paths should all execute (notify AND log AND escalate); use If/Else when exactly one should. Picking Router for an either/or decision is how work runs twice.
Join Paths: the convergence primitive
Section titled “Join Paths: the convergence primitive”Wiring two conditional branches straight into one node does not work — the untaken branch blocks it. Join Paths is the exemption: it runs when any incoming branch arrives. Two modes:
- Pass-through emits a named object with one field per incoming path (null for blocked paths).
- Choose active path validates that exactly one incoming path is active and returns its value — it errors if more than one is active (the classic surprise when converging Router routes).
The “Continue execution when no path is active” setting decides whether the flow proceeds or stops when everything upstream was blocked.
Loops: Run flow on list
Section titled “Loops: Run flow on list”The loop node runs a child flow once per list item, sequentially. What the docs of every automation tool get wrong and Controller AI makes explicit:
- Selecting the target flow materializes one mapping prop per child-flow input. You map them from
itemyourself — nothing is injected into the child automatically, and an unmapped input arrives empty. - Max items defaults to 500, and exceeding it is all-or-nothing: the node throws and zero items iterate. A 600-row list fails entirely on a node you never configured.
- Continue on iteration error covers a failed child run only. A per-item expression or config error aborts the whole loop even with it enabled.
- The loop result records each iteration’s index, status, outputs, and error — per-iteration drill-down is in run details and
cai exec tree. - Remember the 25,000 node-executions-per-run budget: items × nodes-per-iteration.
A durable timer up to 30 days, backed by the engine — it survives restarts and costs nothing while sleeping. Do not design around a short-delay assumption: “wait 3 days then send the follow-up” is one node.
Return response
Section titled “Return response”Return response writes the flow’s typed output fields. If several Return response nodes complete in one run, their results merge in execution order and later values silently overwrite earlier ones for the same field. The engine does not enforce one-return-per-run — design exactly one Return per path unless merging is the intent.
Error handling
Section titled “Error handling”Continue on error (per node; not on triggers, Return response, or If/Else): the failed node’s output becomes null, the main path continues, the run ends completed with error. Enabling it offers an optional Error output port for a recovery branch — and disabling it later deletes that port plus every edge wired from it.
There is no “check execution status” expression source — recovery logic is built from the Error output and Only-when conditions on the error value, not by inspecting sibling statuses.
Example prompts
Section titled “Example prompts”High-priority tickets should page AND email AND log; normal ones just log. Wire this with a Router, and make the two paths converge on one summary step with Join Paths.This loop needs to survive individual item failures but stop if the list itself is malformed. Set continue-on-iteration-error and explain what it does and does not cover here.