ByteChef LogoByteChef
AutomationWorkflows

Flow Controls

Branch, loop, parallelize, and error-handle your workflows with the built-in task dispatchers.

Flow controls are the structural building blocks of a workflow — they decide which steps run, how many times, and what happens when something fails. In the editor they appear alongside components in the step picker; under the hood each one is a task dispatcher that orchestrates its child tasks.

Flow controlWhat it does
ConditionRun one of two branches depending on a boolean expression.
BranchRoute to one of several cases by matching a value (switch-style).
LoopRepeat its child steps over a list or a fixed number of iterations, with access to the current item and index.
Loop BreakEnd the enclosing Loop early from inside one of its iterations.
EachExecute child steps once per item of a list.
MapTransform a list by running child steps for every item and collecting the results.
ParallelRun several child steps at the same time and continue when all finish.
Fork/JoinSplit into multiple named branches that run concurrently, then join their results.
SubflowInvoke another workflow as a step and use its output — see Subflows.
ApprovalPause the workflow until a human approves or rejects — see Human-in-the-Loop.
On ErrorCatch a failing step and run a recovery branch instead of failing the run.
TerminateEnd the workflow immediately from inside a branch.
GraphRoute through named nodes and expression-based transitions, with cycles allowed — a state machine (coming soon).

Every flow control's inputs and outputs are documented in the flow-controls reference. Flow controls nest freely — a Loop inside a Condition inside a Fork branch is a normal workflow.

Outputs

Map returns a value today: a list of each iteration's last-task output, in source-list order — reference it downstream as ${mapNodeName}.

Coming soon. Condition, Branch, and Fork/Join return values are on the upcoming release track and are not yet available in the latest released version of ByteChef.

Once released, Condition and Branch will return the last task's output from whichever case actually ran — an empty case returns nothing (null), and a Branch case with a literal value instead of tasks returns that value directly. Fork/Join will return an object keyed branch_0, branch_1, … in branch declaration order, each value being that branch's last task's output.

Graph

Coming soon. The Graph flow control is on the upcoming release track and is not yet available in the latest released version of ByteChef.

Where Loop repeats a fixed body and Condition/Branch pick one of a small, fixed set of paths, Graph is a state machine: a set of named nodes, each with its own task list, wired together by expressions that decide which node runs next. A node's transition can point forward, sideways, or back to a node that already ran — the shape you'd otherwise hand-build out of nested Loop and Condition steps for a "retry until it passes" or "send it back for another pass" flow.

Nodes and transitions

A graph's nodes list is its state list. Each node has:

  • name — unique within the graph. This is the vocabulary every node's next expression targets, including its own.
  • tasks — an ordinary task list, run in order once the node is entered. A node can contain any other flow control, and a graph can itself be nested inside a Condition, Loop, Fork/Join, or another graph, exactly like every other flow control.
  • next — a formula-mode expression, evaluated against the accumulated run context once the node's tasks finish, that resolves to the name of the node to run next.
- name: triage
  type: graph/v1
  parameters:
    startNode: classify          # optional; defaults to the first declared node
    maxTransitions: 100          # optional; default 100
    nodes:
      - name: classify
        tasks: [ ... ]
        next: "=${score.value} > 0.8 ? 'approve' : 'review'"
      - name: review
        tasks: [ ... ]
        next: "=${review.verdict} == 'REDO' ? 'classify' : 'approve'"   # jumps back
      - name: approve
        tasks: [ ... ]
        # no `next` — this node is terminal, the graph completes here

A run starts at startNode (or the first declared node, if omitted) and keeps following next until it lands on a node with no next expression — that omission is what marks a node terminal; there's no separate "end" step to add. An expression that evaluates to a blank or null value has the same effect. An expression that resolves to a name that isn't one of the graph's nodes fails the run with an error naming the offending node and the value it produced, so a typo in a node name is caught at run time instead of silently dead-ending the graph.

A node can also declare an empty task list — a pure router. Its next is evaluated immediately on entry, with no work of its own in between; it still counts as one transition against the budget below.

Cycles and the transition budget

Because next can point at a node that already ran — including the node it's declared on — a graph can express review loops and retry-until-done flows directly: review jumping back to classify above is a normal transition, not an error. To keep a routing expression that never resolves to a terminal node from running forever, every graph carries a transition budget, maxTransitions (default 100). Every node-to-node hop, including a router node's immediate next, counts against it; once the budget is used up the run fails cleanly instead of looping indefinitely.

Context accumulates across the whole run the normal way: a node that runs a second time (after a cycle back to it) sees everything produced so far, including its own earlier run, and a data pill referencing a step inside that node resolves to that step's most recent execution.

Outputs

${graphName} resolves to the terminal node's last-task output — the same convention as Condition and Branch above. If the terminal node's task list is empty, or its last task produces no output, ${graphName} is null.

In the editor

  • Each node gets its own lane on the canvas, similar to Fork/Join's branch columns — including empty (router) nodes, which stay addressable rather than collapsing away.
  • A node's name is an inline-editable chip above its lane; names must stay unique and non-empty within the graph.
  • Selecting the graph container shows every node's next expression as a field in the properties panel, so transitions can be reviewed and edited without opening each node individually.
  • Where a next expression contains plain quoted node names (e.g. 'approve', 'review'), the editor draws routed transition edges between the corresponding lanes, plus a small badge on the node listing its possible next steps — available under the experimental layout engine. An expression whose target can't be read directly off the text — like the LLM-routed example below — renders as a dynamic badge instead of a fixed edge, since the actual destination is only known once the run reaches it. Under the standard layout engine, transitions always show as badges only; routed edges appear only with the experimental engine.

An LLM-routed example: ticket triage

A common use of Graph is letting an AI Agent step choose the next node itself instead of hand-writing the routing logic. Give the agent a structured output schema with an enum of the possible next states, and point next straight at the field the model fills in.

Take a support-ticket triage flow: an AI Agent node classifies an incoming ticket, and the graph routes to whichever node handles that outcome.

- name: triage
  type: graph/v1
  parameters:
    startNode: classify
    maxTransitions: 20
    nodes:
      - name: classify
        tasks:
          - name: classifyTicket
            type: aiAgent/v1/chat
            parameters:
              format: SIMPLE
              systemPrompt: >-
                You triage incoming support tickets. Choose exactly one category and say
                how confident you are in that choice.
              userPrompt: "Subject: ${trigger_1.subject}\n\n${trigger_1.body}"
              response:
                responseFormat: JSON
                responseSchema: |
                  {
                    "type": "object",
                    "properties": {
                      "decision": {
                        "type": "string",
                        "enum": ["refund", "technical", "escalate", "resolved"]
                      },
                      "confidence": { "type": "number" }
                    },
                    "required": ["decision", "confidence"]
                  }
        next: "=${classifyTicket.decision} ?: 'escalate'"
      - name: refund
        tasks: [ ... ]
      - name: technical
        tasks: [ ... ]
      - name: escalate
        tasks: [ ... ]
      - name: resolved
        tasks: [ ... ]

Setting response.responseFormat to JSON (with a response.responseSchema describing the shape) is what turns the AI Agent's reply from free text into a structured object — the model's answer is parsed against the schema, and its properties become data pills exactly like any other step's output: ${classifyTicket.decision}, ${classifyTicket.confidence}. The next expression then does nothing more than forward the model's own decision as the target node name, falling back to 'escalate' with the Elvis operator (?:) if the model produced no usable answer at all — a parsing failure or an empty response — so a bad turn from the model routes to a safe node instead of failing the whole run.

Because the schema's enum only ever produces one of refund, technical, escalate, or resolved, every value the model can return already names a real node in the graph — the fallback exists for "the model produced nothing," not for an out-of-vocabulary answer.

Gating on confidence. A router doesn't have to trust the model outright. Combine the ternary and comparison operators to defer to a human whenever confidence is low, regardless of what the model decided:

next: "=${classifyTicket.confidence} > 0.7 ? ${classifyTicket.decision} : 'human_review'"

Here the model's decision is used as the target only above the confidence threshold; anything less certain always lands on a human_review node instead — worth adding to the graph with tasks that notify a person and let them pick a category before the graph continues.

Both variants read the model's own output to pick the next state, so neither one has a node name the editor can read directly off the expression text — both render as a dynamic badge on the classify node rather than fixed edges to refund/technical/escalate/resolved/human_review.

How is this guide?

Last updated on

On this page