Workflows
A workflow is a directed graph of nodes joined by edges. You can build it by chatting or by editing the canvas directly. This page shows how to build one in the canvas, then explains the model underneath: how nodes are named and wired, how data flows, and how branching, parallelism, loops, joins, waiting steps, reliability, and versions behave.
Build a workflow in the canvas
The canvas is the visual editor of a workflow. You can also ask the assistant in chat to build or change a workflow for you; both edit the same plan.
- Create the workflowFrom your workflows list, click Create Workflow, enter a Name (the Description is optional), then click Create Workflow. The empty canvas asks “What should this workflow do?”: describe it to the assistant in the prompt, or start by hand with Add the first node or the Trigger button, which picks a first trigger.
- Add nodes from the paletteClick Add node at the top right of the canvas (or the + in the prompt of an empty canvas) to open the node palette. It has five categories: Triggers, Integrations, AI, Flow, and Core, plus a search box. Click a node to add it, or drag it onto the canvas. To insert a node between two connected nodes, hover the edge between them and click its Add node button.
- Connect the nodesDrag from a node's output handle to the next node. Each output port of a branching node (for example the
ifport of an If / else) leads to one node only; to run several nodes in parallel from it, add a Fork. Drawing an edge back to a node that already ran creates a loop edge (see Loops). - Configure each node in the inspectorClick a node to open its inspector. The Edit tab holds the node's settings; reference data from earlier nodes with
{{...}}expressions (see Expressions & variables). In the canvas Settings (gear button in the canvas toolbar) you choose where the inspector docks (Floating or Side panel) and what a node click opens (Settings only or Settings, input and output). - Fix validation issuesValidation runs as you edit. When something is missing, an info icon appears next to Add node with a count such as “1 error, 2 warnings”. Open it and click an entry to jump to the node concerned.
- SaveClick Save in the page header. A save that changes the workflow creates a new version of it (see Versions and production).
- Run itClick Run in the header to run the workflow automatically; its arrow offers Run (auto) and Step-by-step (debug). You can also start from a specific trigger with the play button under that trigger node. Starting a run from the editor saves your changes first. Switch the canvas between editing and runs with the toggle at the top of the canvas (tooltips Edition and Runs); in runs mode the inspector adds a Run data tab with the node's input and output.
- Put a version in productionWhen the workflow behaves as expected, set a version as production so that its triggers fire for real (see Versions and production).
Right-click a node or the empty canvas for more actions, such as Duplicate, Delete, Add node, Auto-layout, or Fit view.
Nodes, edges and ports
Every node has a normalized key of the form prefix:label. The prefix marks the category; there are seven:
| Prefix | Category | Examples |
|---|---|---|
trigger: | Entry points | webhook, schedule, chat, form, manual, table, workflow, error |
mcp: | Integration steps | an API call from the catalog |
table: | Built-in table operations | find, create, update, delete rows |
agent: | AI nodes | Agent, Classify, Guardrail, Browser Agent, Generate |
core: | Control flow and utilities | If / else, While, Split, Transform, Wait, HTTP Request |
interface: | Web pages | a page rendered in an iframe |
note: | Canvas annotations | never executes, never affects the graph |
An edge has the shape { from, to }, plus an optional backEdge marker for a loop edge. Edges set execution order only, never data. Branching nodes expose named output ports, written as a suffix on the reference (core:label:port), and each port leads to exactly one successor:
| Node | Connectable output ports |
|---|---|
| If / else (Decision) | if, elseif_0, elseif_1, ..., else |
| Switch | case_0, case_1, ..., default |
| Option | choice_0, choice_1, ... |
| Fork | branch_0, branch_1, ... |
| While (Loop) | body, exit |
| User Approval | approved, rejected, timeout |
| Classify | category_0, category_1, ... |
| Guardrail | pass, fail |
| Split, Merge, Transform, Wait, Aggregate | none |
Label normalization
Node keys are derived from your label by a fixed rule: accents are transliterated to ASCII, the text is lowercased, every non-alphanumeric character becomes _, repeated underscores collapse, and leading and trailing underscores are trimmed. So “My-API Call” becomes mcp:my_api_call. Write references with the normalized form: {{mcp:my_api_call.output.data}}.
How data flows
Connecting nodes sets execution order, not data. To pass a value, reference it with {{prefix:label.output.field}}, resolved at run time. A node can read only the outputs of nodes that ran before it on its path: triggers, earlier steps, and the current item inside a split body. It cannot read its descendants, a parallel sibling branch, itself, or an unconnected node.
{{trigger:webhook.output.userId}} → a field of the webhook request body
{{mcp:fetch_user.output.email}} → the "Fetch user" step's email field
{{mcp:fetch_user.output.data.user.id}} → nested field access
{{mcp:fetch_user.output.items[0]}} → array index access
{{core:summary.output.transformed.total}} → a Transform node's computed fieldA webhook trigger puts each request body field directly under output (there is no payload wrapper); it also outputs _webhookMethod, _webhookTimestamp, triggered_at, and triggered_by. Request headers are not passed to the workflow.
A value written as a single, whole {{...}} expression keeps its type (a number stays a number, an object stays an object); an expression embedded in surrounding text always yields a string. Expressions also offer built-in functions such as now(), formatdate(), coalesce(), and json(). See Expressions & variables for the syntax and the full function list.
Readiness rules
A node runs when all of its predecessors have resolved (completed or skipped):
- Triggers are ready at the start of a run. A trigger with several successors makes an implicit Fork.
- Any other node becomes ready once all of its predecessors are resolved. Several incoming edges make an implicit Merge (AND); several outgoing edges make an implicit Fork.
- Edges from triggers are the exception: each trigger run starts on its own, so a node fed by a form trigger and a schedule trigger runs once on each trigger run, without waiting for the other trigger.
- You don't need an explicit Fork or Merge node to get that behavior: the edges alone decide it.
- If / else, Switch, Option, Classify, Guardrail, User Approval, Split, Fork, and While accept a single incoming edge, an edge from a trigger included (a loop edge back into them does not count). To join branches, or several triggers, in front of one of them, add a Merge first.
- When the assistant builds through chat, validation is stricter: every
core:node except Merge (for example Transform, Wait, HTTP Request, or Code) must have exactly one incoming edge, or edges from triggers only, so branches are joined with a Merge there too.
Branching: exactly one path
If / else, Switch, and Option are mutually exclusive: each activates exactly one branch and skips the rest.
| Node | Selects on | Rule | Main outputs |
|---|---|---|---|
| If / else (Decision) | boolean conditions | top to bottom, the first true condition wins | selected_branch, selected_branch_index, skipped_branches, evaluations |
| Switch | a value compared with each case | the first matching case wins, else default | selected_branches (a string, the matched label), selected_case_index, skipped_branches, evaluations |
| Option | one expression per choice port | the first true choice wins | selected_choice, selected_label, selected_choice_index, skipped_branches, evaluations |
Put the most specific condition first. Because a branching node selects exactly one port, a failed branching node has no port to route through, so continue-on-failure cannot be enabled on If / else, Switch, or Option.
Parallelism: Fork and Split
Fork runs all of its branches in parallel, with no condition. An explicit Fork exposes branch_0, branch_1, and so on; an implicit Fork is just several edges leaving one node. Parallel branches cannot see each other's outputs while running.
Split fans a list into parallel item contexts on a single path: the same body runs once per item. It evaluates its list expression once. Inside the body, current_item and current_index (0-based) are available, also as the shorthands {{item}} and {{index}}. They are runtime-only: nothing downstream of the split can read them.
{{core:process_orders.output.current_item}} → the current item (inside the body only)
{{item}} → shorthand for current_item
{{core:process_orders.output.items}} → the persisted list, readable downstream
{{core:process_orders.output.item_count}} → how many items were spawned| Field | Meaning |
|---|---|
| items | the evaluated list |
| item_count | number of items spawned |
| split_id | identifier of this split |
| spawn_reason | items_spawned or empty_list |
| terminated | always true once the split has spawned its items |
Loops
There are two ways to repeat steps.
The While node
While repeats its body while its condition holds, re-evaluating it after every pass, then routes to exit. Connect the last step of the body into core:label:iterate to close the loop. Its safety limit, maxIterations, defaults to 10 (range 1 to 10,000) and can also be an expression that resolves to a positive whole number.
A loop edge
On the canvas, drawing an edge from a node back to a node that already ran (for example from the else port of an If / else back to a fetch step) creates a dashed loop edge, with no While node. Select it to open the Loop Edge panel: Condition (SpEL) is re-evaluated before each new pass (leave it empty to loop every time the edge is taken), and Max iterations defaults to the inherited limit of 10.
How a loop ends
| reason | When | Result |
|---|---|---|
condition_false | the condition said stop | the run continues through exit |
iterations_exhausted | a While with no condition ran its maxIterations passes ("repeat N times") | the run continues through exit |
max_iterations_reached | the limit was reached while the condition still asked for another pass | the run fails and exit is not taken |
| Field | Meaning |
|---|---|
| iteration | 0 on the first pass, then incremented; readable inside the body |
| maxIterations | the configured limit |
| terminated | whether the loop has finished |
| enter_body | whether another pass is starting |
| selected_path | the port the loop routes to |
| reason | present once terminated (see the table above) |
Joins: Merge and Aggregate
Merge (an explicit node, or any other node with several incoming edges) is always AND: it waits for every predecessor (when several triggers feed it, only for the trigger that started the run). There is no OR mode. Its outputs describe the join (merged_branches, sources, source_count, success_count); read each branch's data through that branch's own key, for example {{mcp:api_call.output.data}}.
Aggregate collects the item contexts spawned by a Split back into one output. It waits in a collecting state until every expected item has arrived. Its output is aggregated_count (alias count) plus one list per field you configure (each label → expression pair produces its own list).
Waiting inside a run
Wait holds back the nodes after it for a duration. In the canvas you set it in milliseconds, up to 10 minutes, with presets from 100 ms to 10 m. Up to 3000 ms it sleeps inline and a cancel still takes effect within about 100 ms; longer waits register a timer and resume automatically when it expires. Outputs: status, waited_ms, started_at, completed_at, plus duration_ms and expires_at on the timer path.
User Approval holds back the nodes after it until someone responds (branches that do not depend on it keep running), then routes to approved, rejected, or timeout. It supports several required approvals (requiredApprovals, at least 1) and an optional list of approver roles. It times out after 24 hours unless you set another timeout. An optional context template is rendered when the node pauses, shown to the approver, and kept as the output approval_context.
| Field | Meaning |
|---|---|
| approver_roles | the roles allowed to respond |
| required_approvals | how many approvals were required |
| expires_at | when the approval times out |
| approval_context | the rendered context shown to the approver |
| selected_port | approved, rejected, or timeout |
Exit ends only its own branch: parallel Fork or Split branches keep running, and the exited branch counts as successful. It takes an optional reason (default “Branch exited”) and outputs reason, status (exited), and exited_at.
Per-step reliability policy
Any executed node (mcp:, table:, agent:, core:, interface:; not triggers or notes) can carry an optional reliability policy. A node with no policy runs once.
| Field | Default | What it does |
|---|---|---|
| retryCount | 0 | additional attempts after a failure, at most 10; total attempts = retryCount + 1 |
| retryBackoffMs | 0 | delay between attempts, at most 60000 (60 seconds); blocks only the executing branch or item. When the provider's refusal carries a Retry-After, the longer of the two is used |
| retryOn | absent | integration steps only, with retryCount above 0. Absent: every failure is retried except a refusal no retry changes: a request the provider refused outright (a 4xx other than 408, 425 and 429 that is not a rate limit), or one the platform refused (missing credits, a budget reached, a plan that does not include the integration, a credential it cannot pick, a tool that no longer exists). 'rate_limit': only a rate limit is retried (429, 503, or a 4xx with a Retry-After or a message saying it is a rate limit, e.g. rate limit, too many requests, throttled, request limit reached; a Retry-After on a 5xx does not count) |
| continueOnFailure | false | when every attempt fails, the node stays FAILED and the run ends as it would without this setting, but the next nodes run instead of being skipped, whatever started the run (execute, a trigger, a schedule or the Run button), unless the failure is a refusal for missing credits or a budget; inside a Split, a failed item goes on to the next nodes |
| timeoutMs | 0 (no limit) | a limit per attempt; on expiry the attempt fails and retries apply. On an agent node it bounds the wait for the answer, a wait in the queue or for a person the agent asked included |
| executeOnce | false | inside a Split, run only for the first item and skip the rest; no effect outside a split |
The platform never re-sends a refused call on its own: retrying is this policy's job. For a provider that limits requests per minute, use a retryBackoffMs that outlasts the window (60000). When the provider says how long to wait (Retry-After), the step waits that long if it is longer than the backoff; a provider asking for longer than 60 seconds ends the retries, because every wait holds a worker: a window of minutes or hours belongs to a schedule. retryOn: 'rate_limit' is safer on a step that publishes or writes: it does not re-send after a timeout or most server errors, which the provider may already have applied, although a 503 can still arrive after a write went through. An HTTP Request node does not fail on an HTTP error (it completes with the status in its output), so its HTTP errors are never retried: branch on its status instead. When retries stop early, the result says why in policy_retry_stopped.
Some combinations are rejected up front instead of failing at run time: continueOnFailure on If / else, Switch, Option, While, Classify, or Guardrail; executeOnce on Split, Aggregate, Merge, or While; retryOn on anything but an integration step or without retries; a value above a cap; and any negative value.
Execution modes and node statuses
In automatic mode every node runs as soon as its predecessors are resolved, and independent ready nodes run concurrently. In step-by-step mode you advance one node at a time, which is useful for debugging.
Some nodes wait on a signal (a long wait, a user approval, a webhook wait, or a blocking interface): the nodes after them resume once the signal resolves, and the other branches keep running meanwhile. See Runs & execution for runs, epochs, and re-running steps.
| Status | Meaning |
|---|---|
| PENDING | predecessors have not all resolved yet |
| READY | all predecessors resolved, about to execute |
| RUNNING | currently executing |
| COMPLETED | finished successfully (terminal) |
| FAILED | finished with an error (terminal) |
| SKIPPED | not taken, for example the other branch of an If / else (terminal) |
| AWAITING_SIGNAL | paused on a timer, approval, webhook wait, or blocking interface |
| WAITING_TRIGGER | waiting for its trigger to fire |
| COLLECTING | an Aggregate waiting for more Split items |
Versions and production
A save that changes the workflow creates a new version. Saving with no change, or after only moving nodes on the canvas, keeps the current version (moving nodes of the production version does create one, because that version is never changed). Open Version History with the arrow next to Save to see each version with its date, who made it (a person, or Orbi or an agent with a link to its conversation), its node count and its number of runs. From there you can click an earlier version your plan opens to restore it on the canvas, rename a version, or use the pin icon to Set as production.
How many versions you can open, and how many are kept, depends on your plan: Free opens the current version and keeps the 20 most recent, Pro opens the 20 most recent and keeps 100, Team and Enterprise open and keep 100 (a self-hosted install opens and keeps 100). Older versions are deleted beyond what your plan keeps, except the production version. A version your plan keeps but does not open is listed as Locked; the production version never is, until production moves off it. See Version history for the history of agents, skills, interfaces, tables and projects, which follows the same plans.
The production version is the one real traffic runs on. Production triggers (Webhook, Scheduler, Tables, Chat, Form, and Workflows triggers) fire only into the production version; while no version is in production, they do not fire. Runs you start from the editor do not need a production version.
The Live status at the start of the canvas toolbar always says where the workflow stands, in the editor and on a run: Live v3 with a green dot when the version on screen is the production one, Live v3 with a hollow amber ring when you are looking at another version or have unsaved changes (production still runs v3), Paused v3 with a small amber square when v3 is in production but the workflow was disabled (from the notification bell or the board's Paused column: nothing fires until you reactivate it), and Not live when no version is in production. Click it for a one-line explanation and, when the version on screen is not the production one, to set it as production. On a narrow canvas (a phone, a narrow side panel) the toolbar stays on one line: the status shows its dot and the version only, and the zoom buttons and the cursor mode move to the canvas Settings (pinch, or Ctrl and the mouse wheel, still zoom too).
- Choose the versionClick the Live status at the start of the canvas toolbar, or the pin button under a trigger node (both target the version on the canvas), or use the pin icon of a version in Version History.
- ConfirmClick Set as production. If the canvas has unsaved changes, the button offers Save & set as production, which saves a new version first. If another version is already in production, the dialog confirms the move from one version to the other.
Remove from production (the same pin control on the production version) stops the production triggers until you set a version again. See Triggers for how each trigger type fires.
Troubleshooting
An HTTP Request returned an error but the node is green
A non-2xx response (404, 500, 502...) does not fail an HTTP Request node. The node completes with output.success set to false, output.status set to the status code, and output.data holding the error body, and the rest of the workflow runs. Only transport errors (DNS, connection, timeout) fail the node. Check the response explicitly, for example with an If / else on {{core:call_api.output.success}}.
A file collected by Aggregate cannot be used downstream
Values collected by an Aggregate after a Split are converted to text: numbers become strings, and objects, including files, become text that later nodes cannot use as a file. To pass files or typed objects onward, collect them in a Code node instead, which keeps real JSON types.
A sub-workflow step succeeded but produced nothing
A failed step inside the child fails the Sub-Workflow node only when the child's cycle runs straight through. When the child pauses on the way (an agent running in the background, a wait longer than 3 seconds) and the node has to wait for it to finish, the node reports success even if a step inside the child failed. Check the child's actual output rather than the node's status. The child's outputs are keyed by the child node's key without its prefix: {{core:call_child.output.result.step_result.output.transformed.url}}, not result.core:step_result.
A loop failed with max_iterations_reached
The loop hit its limit while its condition still asked for another pass. Raise Max iterations (or maxIterations on the While node), or check that the condition eventually becomes false.
A reference resolves to nothing
Check the node key (the normalized label), the .output. segment, and that the referenced node runs before this one on the same path. An expression that fails to evaluate resolves to empty without an error; see Expressions & variables.