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, pausing, 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 opens with a chat prompt (“How can I help you?”) and a Trigger button to pick a first trigger.
- Add nodes from the paletteClick Add node (the + button at the top right of the 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 Edittab 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 nodewith 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. Every save creates a new version of the workflow.
- 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 datatab 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.payload.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 the request body under payload; its other outputs are headers, query, method, triggered_at, and triggered_by.
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.
- You don't need an explicit Fork or Merge node to get that behavior: the edges alone decide it.
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 node with several incoming edges) is always AND: it waits for every predecessor. 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).
Pausing the run
Wait pauses 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 pauses the run until someone responds, 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; total attempts = retryCount + 1 |
| retryBackoffMs | 0 | delay between attempts; blocks only the executing branch or item |
| continueOnFailure | false | on final failure the node is still marked FAILED, but its successors run instead of being skipped |
| timeoutMs | 0 (no limit) | a limit per attempt; on expiry the attempt fails and retries apply |
| executeOnce | false | inside a Split, run only for the first item and skip the rest; no effect outside a split |
Some combinations are rejected up front instead of failing at run time: continueOnFailure on If / else, Switch, or Option; executeOnce on Split, Aggregate, Merge, or While; 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 pause the run on a signal (a long wait, a user approval, a webhook wait, or a blocking interface) and resume once the signal resolves. 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
Every save creates a new version. Open Version History with the arrow next to Save to see each version with its date, node count, and number of runs. From there you can click an earlier version to restore it on the canvas, rename a version, or use the pin icon to Set as production.
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.
- Choose the versionUse the pin button under a trigger node or in the canvas toolbar (it targets the version on the canvas), or 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 Sub-Workflownode reports success as soon as the child workflow's run cycle has finished, even if a node 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.