06. Flow Control¶
For when an automation that runs in a straight line is not enough.
| What you want | Node to use |
|---|---|
| Take a different path depending on a value | CONDITIONAL |
| Bring split paths back together | JOINT |
| Process an array one item at a time | LOOP_START / LOOP_END |
Branching — CONDITIONAL¶
Looks at a value and picks which way to go.
- id: route
name: Branch by amount
type: CONDITIONAL
execution-info:
conditions:
- label: vip
expression: "amount >= 100000"
- label: normal
expression: "amount >= 10000"
- label: small
otherwise: true # if none of the above match, come here
edges:
- from: route
to: vip-handler
label: vip # ← label says which branch this is
- from: route
to: normal-handler
label: normal
- from: route
to: small-handler
label: small
Three key rules.
- Conditions are checked from the top. Only the first match runs.
- Every
labelmust have exactly one outgoing edge. Without it the save is rejected. otherwise: trueis the default branch taken "when nothing above matches". Including one is recommended.
The values a condition sees must come over an edge¶
⚠️ Condition expressions are evaluated in the ${input.*} scope. If the incoming edge has no request.data, the condition can see no values at all.
edges:
- from: fetch-order
to: route
request:
data:
amount: "${nodes.fetch-order.response.body.totalAmount}" # ← this has to be here
status: "${nodes.fetch-order.response.body.status}"
Once handed over this way, the condition expression uses just the name.
expression: "amount >= 100000" # ✅ the name alone, no ${}
expression: "${input.amount} >= 100000" # ❌
Available operators¶
| Operator | Meaning | Example |
|---|---|---|
== |
equals | status == 'paid' |
!= |
not equal | status != 'cancelled' |
> >= < <= |
comparison | amount >= 50000 |
contains |
contains | message contains 'refund' |
startsWith |
starts with | orderId startsWith 'ORD-' |
endsWith |
ends with | email endsWith '@company.com' |
- Wrap strings in single quotes:
'paid' - If both sides convert to numbers it is a numeric comparison; otherwise it is a lexicographic string comparison.
Branching on an AI answer¶
contains is especially useful for splitting on what the AI replied.
- id: classify
name: Classify the inquiry
type: CALL
integration: llm_chat
input:
apiContract: OPENAI_CHAT
model: gpt-4o-mini
apiKey: "${secrets.OPENAI_API_KEY}"
systemPrompt: "Classify the customer inquiry with exactly one word: refund / delivery / product / other. Say nothing else."
userPrompt: "${nodes.trigger.response.body.message}"
- id: route
name: Branch by category
type: CONDITIONAL
execution-info:
conditions:
- label: refund
expression: "category contains 'refund'"
- label: delivery
expression: "category contains 'delivery'"
- label: other
otherwise: true
edges:
- from: classify
to: route
request:
data:
category: "${nodes.classify.response.body.content}"
- from: route
to: refund-flow
label: refund
- from: route
to: delivery-flow
label: delivery
- from: route
to: default-flow
label: other
The trick is to nail the AI down in
systemPromptso that it answers with exactly one word. It still will not be perfect, so always keep anotherwisebranch.
Joining — JOINT¶
The spot where split paths meet again. It is the only node that may take several incoming edges.
There is nothing to configure. Writing type is the whole thing.
What it does¶
It waits until every incoming branch has finished. Once they all have, it moves on to the next node. Use it when you want to call two APIs at the same time and process the results only after both have arrived.
edges:
- from: trigger
to: fetch-orders
- from: trigger
to: fetch-stock
- from: fetch-orders
to: join
- from: fetch-stock
to: join
- from: join
to: summarize
⚠️ A JOINT has no output of its own¶
A JOINT only waits. It does not produce a value. In the node after it, reference the node before the JOINT directly.
# ❌
userPrompt: "${nodes.join.response.body.data}"
# ✅
userPrompt: |
Orders: ${nodes.fetch-orders.response.body.data}
Stock: ${nodes.fetch-stock.response.body.data}
Looping — LOOP_START / LOOP_END¶
Takes an array and repeats the same processing for each element.
- id: each-order
name: Start per-order loop
type: LOOP_START
execution-info:
items: "${nodes.fetch-orders.response.body.data | raw}" # ← the array to iterate
# ─── the loop body starts here ───
- id: notify-each
name: Notify per order
type: CALL
integration: http_request
input:
uri: "https://api.example.com/notify"
method: POST
body:
orderId: "${item.orderId}" # ← the current element
sequence: "${index}" # ← the iteration number, starting at 0
# ─── end of the loop body ───
- id: collect
name: Collect loop results
type: LOOP_END
execution-info:
loop-start: each-order # ← the id of the matching LOOP_START
Rules¶
LOOP_STARTandLOOP_ENDalways come in pairs. With only one of them, the save is rejected.- Always attach
| rawtoitems. Without it the array becomes a string and nothing iterates. - Every path in the loop body has to converge on
LOOP_END. - You cannot put a
CONDITIONALinside a loop. (See below.)
Values available inside a loop¶
| Expression | Meaning |
|---|---|
${item} |
The whole current element |
${item.field} |
A specific field of the current element |
${index} |
Which iteration this is (starting at 0) |
These values are valid only between LOOP_START and LOOP_END.
If you want to branch inside a loop¶
You cannot put a CONDITIONAL inside a loop. The moment one branch is cut off, there is no path left for it to converge on LOOP_END.
Two alternatives.
Alternative 1 — filter before the loop (this is enough most of the time)
- id: filter
name: Keep only what needs processing
type: CALL
integration: transform_jmespath
input:
expression: "{targets: orders[?status == `paid`]}"
data: "${nodes.fetch-orders.response.body | raw}"
- id: each
type: LOOP_START
execution-info:
items: "${nodes.filter.response.body.targets | raw}"
Alternative 2 — branch outside the loop
Judge the aggregated result with a CONDITIONAL placed after LOOP_END.
The graph rules, collected¶
These are checked when you save. Break one and the save is rejected, with a message about what went wrong.
| Rule | Detail |
|---|---|
| Start | There is exactly one ENTRYPOINT, with 0 incoming edges |
| Multiple inputs | Only JOINT may take two or more incoming edges |
| No cycles | Following the arrows must never return you to where you started |
| Node ids | Must be unique within the workflow |
| Edge references | from / to must point at nodes that actually exist |
| Branching | Every label must have an outgoing edge |
| Loops | LOOP_START and LOOP_END must pair up, and the body must converge on LOOP_END |
| Branching in loops | No CONDITIONAL in a loop body |
| Size | At most 200 nodes, at most 256,000 characters of YAML |
Next → 07. Managing Connections