Control flow & branching
The engine walks the node graph starting from the from: 'trigger' edge. After
each node runs, it follows the edge whose branch matches the branch the node's
result selected, and continues from there.
Branches
A plain node has one outgoing branch, main. Branching nodes declare more, and
their result picks one:
Condition
Declares true and false. Evaluates field <operator> value and takes the
matching branch. Only the matched branch is walked.
['id' => 'gate', 'type' => ConditionNode::type(), 'config' => [
'field' => '{{ trigger.role }}', 'operator' => '==', 'value' => 'admin',
]]
// edges:
['from' => 'gate', 'to' => 'admin-path', 'branch' => 'true'],
['from' => 'gate', 'to' => 'member-path', 'branch' => 'false'],
Operators: == === != > >= < <= contains not_contains starts_with ends_with in not_in is_null is_not_null is_empty is_not_empty.
Switch
Declares default plus one branch per configured case. Takes the first case
whose value matches field, or default if none do.
['id' => 'route', 'type' => SwitchNode::type(), 'config' => [
'field' => '{{ trigger.status }}',
'cases' => [
['value' => 'paid', 'branch' => 'paid'],
['value' => 'pending', 'branch' => 'pending'],
],
]]
Loop (For each)
Declares body and main. Each visit to the node either emits the next item on
the body branch or, once the list is exhausted, emits on main. To iterate,
wire the end of the body path back to the loop node so it's re-entered:
['from' => 'trigger', 'to' => 'loop', 'branch' => 'main'],
['from' => 'loop', 'to' => 'work', 'branch' => 'body'], // per item
['from' => 'work', 'to' => 'loop', 'branch' => 'main'], // back to the loop
['from' => 'loop', 'to' => 'done', 'branch' => 'main'], // after the last item
Inside the body, the current element and index are on the loop node's output:
{{ nodes.loop.item }} and {{ nodes.loop.index }}. The main output after the
last item is { "iterations": <n> }. The loop's cursor is persisted per
execution, so it survives a suspend/resume mid-iteration.
Scope
Declares body. Runs its body branch once as a grouped sub-run; when the body
has no more nodes, traversal unwinds back to the Scope node's main edge and
continues. Use it to group a run of steps logically. Any data you configure is
the node's output, readable as {{ nodes.<scopeId>.* }}.
The continuation stack
When traversal enters a nested branch (body of a Loop or Scope, and any
non-main branch of a Condition/Switch that is itself a grouping node), the
engine pushes a continuation frame. When that branch runs out of nodes, it pops
the frame and resumes from the parent's next edge. This is what lets a Scope's
body "fall through" to its main edge, and what lets a Loop re-enter itself
cleanly.
You don't manage this — it's automatic — but it explains why a node at the end of
a body branch with no outgoing edge of its own still lets the workflow continue.
Ending a workflow early
- Stop — ends the workflow. On
successthe execution isCancelled(an intentional stop); onfailureit isFailedwith your message. - A node returning a failure, or throwing (after retries), fails the whole execution.