Skip to main content

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 success the execution is Cancelled (an intentional stop); on failure it is Failed with your message.
  • A node returning a failure, or throwing (after retries), fails the whole execution.