The workflow document
A workflow is a versioned JSON document in the v0-draft schema, plus a set of trigger records that live outside it. This page is what that document contains and what the canvas in Cockpit is editing when you drag a node.
You do not need Execution modes first, but it explains why the save-then-build step exists.
The graph
A workflow is a directed graph.
Nodes are the steps. Each node on the canvas is an instance of a node type from the palette: it has an id that is unique within the workflow, a pinned node version, and its own configuration. The id matters beyond the canvas — it is what expressions use to read that node's output, so a node named fetch is referenced as ${{ nodes.fetch.outputs.response }}.
Edges connect one node's output port to the next node's input. A connection must leave an output and arrive at an input; the canvas refuses output-to-output and input-to-input drags rather than making a connection that could never run.
Ports carry meaning. A node declares its ports, and there are three kinds:
- A default port is the normal continuation — the HTTP node's
response, a transform'sout. - Branch ports are alternatives, and exactly one of them fires per execution:
true/falseon a condition, one named case on a switch,itemanddoneon an iterator. A branch port that fires and has two connections still runs both of them (see Branches that split and join). - An error port is the exception path. It carries a node's expected failures, and — when the node sets
onError: "route"— the failures that survived its retries too.
A node that always ends the run, like Stop and Error, declares an output that is never emitted, and cannot be dropped into the middle of an existing connection.
The node instance in the document names the node type by namespace:slug, pins its version, and carries its own config alongside the engine's per-step settings:
{
"id": "fetch-orders",
"nodeSlug": "revenexx:http-request",
"nodeVersion": "1.1.0",
"config": {
"method": "GET",
"url": "${{ vars.erpBaseUrl }}/orders"
},
"retry": { "maxAttempts": 5, "backoffSeconds": 2, "strategy": "exponential" },
"timeoutSeconds": 120,
"onError": "route"
}
Pinning the version is what stops a new release of a node silently changing a running workflow; a step can instead follow the release channel and take new releases on purpose. See Node configuration for the config half and Failure handling for retry, onError and timeoutSeconds.
Following the release channel
Instead of a version number, nodeVersion can be the literal channel. A version number is an exact major.minor.patch, optionally with a pre-release suffix such as 1.2.0-beta.1; any other value, latest included, is refused when you save.
Pick a version number when a node's new release must not reach the workflow before you have tested it. Pick channel when the workflow should take each version the tenant's release channel brings, without an edit: unlike a pinned step, a channel step changes when the channel moves.
A tenant is on the stable or the candidate release channel. A channel step runs the version of its node that the tenant's channel resolves to, which on the candidate channel can be a version still being tested. Where the channel names no version for the node, one that is not available yet, or one the tenant is not entitled to, the step runs the newest approved version the tenant may use, skipping retired versions; it never falls back to a version still being tested.
- The version is settled when the bundle is built (see Revisions: what a save does), per tenant, and the compiled step names it. The saved document is never rewritten: it keeps saying
channel, and a step that names a version number keeps that version. - A channel move rebuilds the workflow. When the platform announces that the tenant's channel moved, every workflow of that tenant with a
channelstep is rebuilt, active or not. Workflows whose steps all name a version are left alone. A move that is not announced reaches a workflow only at its next build, which the next save starts. - A step with no version to run fails the build, which names the node, rather than producing a bundle without that node's code. That happens when the channel names no version the step can run and the tenant may use no approved version of the node that is not retired.
- A channel move can switch an active workflow off. If the version the channel moves to makes the workflow break the join rules, it is switched off, as active workflows were when the rules went live (see Active workflows that break the rules).
Branches that split and join
A split is an output port with more than one connection leaving it, and every branch leaving that port runs. It belongs to the port, not the node: if true has two connections and false has one, true splits and false does not. A trigger with several connections is a split too, and so is a branch port with two connections: when that port fires, both of its branches run. The branches of one split can come back together at a single step, where they join.
These rules have applied since 2026-10-19.
- Branches run one after another. Each branch runs to its end, or to the step where it joins, before the next branch starts. Which branch goes first is the order the connections were saved in, which the canvas does not show; do not rely on one branch's side effects in another.
- A joining step waits for every branch. It reads each branch's result under the id of that branch's last step, as
in.<step id>. A step that did not run has no key. A branch that did not run is not passed on as the trigger's data. - Alternatives do not wait. Where the branches of one condition or switch meet (its
trueandfalseports, say), or the branches of several triggers, the step runs on whichever branch fired and reads that branch's result. - A branch that ends without delivering skips the join. The joining step and everything after it are skipped, and the run's steps show them as skipped, naming the split step and its port, and the branch's position counted from 1 in the order its connections were saved. The run still completes, unless a Stop and Error step ends it.
What a save refuses
Saving refuses a workflow whose joins break the rules. The refusal names the step and the rule. It applies whether the workflow is active or not, whether it is saved in the Studio, through MCP or through the API, and also to workflows saved earlier, to restoring an earlier version and to creating a workflow from a template. The rules refuse:
- branches that come back together outside their own split, or a step reached both through a split's branches and by a path that goes around that split;
- a step that reads a step in a sibling branch;
- a joining step that reads its input by a name that is not one of its branches;
- a connection that crosses into or out of an Iterator's body (the steps on its
itemport); - an Iterator body that does not end in exactly one step;
- a step where split branches and alternatives meet in a shape that cannot be read as one join inside another;
- connections that run in a circle.
When one branch of a split crosses into another, the message also names the connection where they cross: the output of which step, into which step. That is the connection to change, and it is rarely the step the save refuses.
Loops follow the same idea. A loop's done output carries, for each item in order, the output of the one step its body ends in (an item whose end step did not run adds nothing), and a stop step such as Stop and Error does not count as an end of the body. A loop can therefore send the error of each of its steps to a stop step of its own and still end in one step.
Active workflows that break the rules
When the rules went live on 2026-10-19, every active workflow whose joins broke them was switched off, so it no longer runs on a schedule, on an event or on a webhook call: a webhook call to an inactive workflow is answered with 404 and starts no run (as of 2026-10; see Triggers). The audit log records the switch-off and the rules that were broken. The workflow itself is left as it was. It opens in the Studio with the broken rules listed as warnings. Correct the shape and switch the workflow on again. A workflow that is already off is left alone. A workflow with a node that cannot be resolved is not judged and stays on. Templates whose branches break the rules can no longer be published.
Variables
Variables are workflow-level values, declared once and referenced from node configuration as ${{ vars.<key> }}. Each one has a type (string, number, boolean, object, array), a value, and an optional description. Keys are identifiers — letters, digits and underscores, not starting with a digit.
Use them for everything that differs between installations of the same graph: the SFTP folder, the base URL, the column a supplier's product id lands in. The BMECAT template is the model — nine variables, set once after install, and no node configuration to touch.
Open them from Variables on the canvas toolbar.
/orders instead of https://erp.example.com/orders looks like a data problem rather than a configuration one. Give every variable a value.Canvas organisation the engine ignores
Four things in the document exist for the people reading the workflow, not for the engine:
- Node groups — a labelled frame around a set of nodes, for the stage of the flow they belong to.
- Sticky notes — free text on the canvas.
- Tags — labels on the workflow itself, for finding it in a long list.
- An icon — how the workflow is shown in that list.
They are saved with the workflow and they bump the revision like any other edit, but they change nothing about execution. A per-node note behaves the same way: it shows as a small marker on the card, never reaches the engine, and is the cheapest way to explain to the next person why a mapping is the way it is.
Validation and the activation gate
You can save a workflow in any state, except a branch shape that breaks the join rules in Branches that split and join (see What a save refuses), which is refused even for an inactive workflow. You cannot activate or run one whose nodes have unfilled required settings.
An incomplete workflow shows a banner above the canvas naming how many settings are missing in how many nodes, one line per node, using the node's canvas name rather than an identifier. Following a line opens that node's settings and centres its card. The affected cards are outlined and badged with their own count. The Activate control is disabled and says how many settings remain, and the keyboard shortcut for activation is refused the same way.
Two details are easy to trip over:
- The markers reflect the last save, not what you are typing. Fixing a field clears the marker when you save, not as you type.
- Saving an already active workflow into an incomplete state is allowed, but it asks first and saves the workflow paused so it cannot fire in that state. Cancelling saves nothing and leaves your edit in the editor. That is about unfinished settings only: a branch shape that breaks What a save refuses is refused, not paused.
Revisions: what a save does
Saving does three things: it validates the graph, it bumps the workflow's revision, and it queues the build that turns the graph into a runnable bundle.
Revisions are plain increasing numbers on the workflow. Revision 1 is the first save; each subsequent save of the graph is the next number, and the previous revision is archived rather than overwritten. There is no branching and no naming — a revision is a snapshot of the graph at one save, and the one that runs now is the current one.
What a revision covers is the graph document: nodes, edges, canvas organisation and workflow variables. Changes that sit beside it — a workflow's own metadata, and its triggers — do not write a revision, so an edit to one of those leaves nothing in the history to go back to. The audit log records them regardless.
Until the queued build reports ready, runs are rejected — see Execution modes.
A newly created workflow — including one installed from a template — is paused. Nothing fires until you activate it deliberately, so you can read and adjust the whole flow first.
Reading the history and restoring an earlier revision is covered in Revisions and audit. Deleting a workflow is permanent and takes its revision history and all of its runs with it, along with its triggers, its missed runs and any state stored for that workflow alone.
Where to go next
- Node catalog — every node you can place, and what it produces.
- Node configuration — the inspector, the field types, and testing a single node.
- Expressions — wiring one node's output into the next node's config.
- Triggers — what makes the workflow you just built actually start.