Most useful pipelines need control flow - branch on this, loop over that, fan out to three things in parallel. The canvas gives you four node types for this: condition, switch, loop, parallel. This guide walks each, plus the patterns we've seen go wrong on tenant pipelines and how to avoid them.
Condition (if/else)
The simplest branch. Pick a field on the run's state (like an invoice amount), pick a comparison (greater than, equal to, matches), and connect the two outgoing edges to what should happen next. Use for binary decisions: "under threshold?", "new customer?", "requires legal review?".
Switch (multi-way)
When you have more than 2-3 branches, switch beats chained conditions. Express the cases as a list of (match, target_node) pairs; the platform picks the first match. A default branch catches anything unmatched.
Loop (for-each + while)
- for-each: iterate over a list in the run state; the child sub-pipeline runs once per item with $.item bound
- while: re-run a sub-pipeline until a condition flips false (rare - usually a code-smell that you should restructure)
- Both require a max_iterations guardrail - the platform refuses to publish a loop without one
Parallel (fan-out fan-in)
Run N child nodes simultaneously, gather their outputs, continue when all complete. Useful when child nodes are independent (querying three different systems for the same record). Aggregation strategy is configurable: first-success, all-success, majority, or custom merge function.
A real example combining all four
Inbound vendor invoice -> condition (is this a known vendor?) -> if yes -> parallel (PO lookup, GRN lookup, tax-code lookup) -> rule (compose the line-by-line match decision) -> switch (status = matched/disputed/needs_review) -> matched -> http_call (post to ERP). Real Invoice Reviewer pipelines roughly match this shape; tenants customise the leaves.
Patterns that go wrong
- Deeply nested conditions - past 3 levels deep, the diagram is unreadable. Refactor to switch or sub-pipeline
- Loop with side effects - state mutations inside a loop are hard to reason about; prefer building up a list in a rule node then writing once after the loop
- Parallel with shared writes - two parallel branches writing to the same record race. Use the platform's optimistic-lock helper (db.update with expected_version) or serialise the writes