Skip to content
Build

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.

  1. Create the workflow
    From 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.
  2. Add nodes from the palette
    Click 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.
  3. Connect the nodes
    Drag from a node's output handle to the next node. Each output port of a branching node (for example the if port 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).
  4. Configure each node in the inspector
    Click 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).
  5. Fix validation issues
    Validation 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.
  6. Save
    Click Save in the page header. Every save creates a new version of the workflow.
  7. Run it
    Click 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.
  8. Put a version in production
    When 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:

Node key prefixes
PrefixCategoryExamples
trigger:Entry pointswebhook, schedule, chat, form, manual, table, workflow, error
mcp:Integration stepsan API call from the catalog
table:Built-in table operationsfind, create, update, delete rows
agent:AI nodesAgent, Classify, Guardrail, Browser Agent, Generate
core:Control flow and utilitiesIf / else, While, Split, Transform, Wait, HTTP Request
interface:Web pagesa page rendered in an iframe
note:Canvas annotationsnever 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:

Output ports of branching nodes
NodeConnectable output ports
If / else (Decision)if, elseif_0, elseif_1, ..., else
Switchcase_0, case_1, ..., default
Optionchoice_0, choice_1, ...
Forkbranch_0, branch_1, ...
While (Loop)body, exit
User Approvalapproved, rejected, timeout
Classifycategory_0, category_1, ...
Guardrailpass, fail
Split, Merge, Transform, Wait, Aggregatenone

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.

Reference examples
{{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 field

A 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.

Branching nodes
NodeSelects onRuleMain outputs
If / else (Decision)boolean conditionstop to bottom, the first true condition winsselected_branch, selected_branch_index, skipped_branches, evaluations
Switcha value compared with each casethe first matching case wins, else defaultselected_branches (a string, the matched label), selected_case_index, skipped_branches, evaluations
Optionone expression per choice portthe first true choice winsselected_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.

Split references
{{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
Split output fields
FieldMeaning
itemsthe evaluated list
item_countnumber of items spawned
split_ididentifier of this split
spawn_reasonitems_spawned or empty_list
terminatedalways 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

Loop termination reasons
reasonWhenResult
condition_falsethe condition said stopthe run continues through exit
iterations_exhausteda While with no condition ran its maxIterations passes ("repeat N times")the run continues through exit
max_iterations_reachedthe limit was reached while the condition still asked for another passthe run fails and exit is not taken
While output fields
FieldMeaning
iteration0 on the first pass, then incremented; readable inside the body
maxIterationsthe configured limit
terminatedwhether the loop has finished
enter_bodywhether another pass is starting
selected_paththe port the loop routes to
reasonpresent 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.

User Approval output fields
FieldMeaning
approver_rolesthe roles allowed to respond
required_approvalshow many approvals were required
expires_atwhen the approval times out
approval_contextthe rendered context shown to the approver
selected_portapproved, 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.

Reliability policy fields
FieldDefaultWhat it does
retryCount0additional attempts after a failure; total attempts = retryCount + 1
retryBackoffMs0delay between attempts; blocks only the executing branch or item
continueOnFailurefalseon final failure the node is still marked FAILED, but its successors run instead of being skipped
timeoutMs0 (no limit)a limit per attempt; on expiry the attempt fails and retries apply
executeOncefalseinside 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.

Node statuses
StatusMeaning
PENDINGpredecessors have not all resolved yet
READYall predecessors resolved, about to execute
RUNNINGcurrently executing
COMPLETEDfinished successfully (terminal)
FAILEDfinished with an error (terminal)
SKIPPEDnot taken, for example the other branch of an If / else (terminal)
AWAITING_SIGNALpaused on a timer, approval, webhook wait, or blocking interface
WAITING_TRIGGERwaiting for its trigger to fire
COLLECTINGan 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.

  1. Choose the version
    Use 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.
  2. Confirm
    Click 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.

Related pages