Skip to content

Flow control

Flow control starts with one decision table. Pick by what you want to happen, not by what sounds familiar:

You want to…UseSemantics
Skip one action when a condition is falseOnly when (node condition)The node is skipped; execution continues past it.
Choose exactly one pathIf/ElseOrdered branches, first match wins, mandatory Else.
Run every matching pathRouterEach route’s condition evaluated independently — multiple routes may run.
Converge conditional branchesJoin PathsThe one node that runs when any incoming branch arrives.
Repeat work per itemRun flow on listSequential iterations of a child flow.
Reuse logicRun flowCall another flow, block until it returns.
Name a computed constantSet valueNamed values from expressions.
DelayWaitDurable timer, up to 30 days.
Return flow outputReturn responseWrites the flow’s output fields.

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.

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 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.

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.

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 item yourself — 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 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.

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.

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.