Skip to content

Workflows ​

A workflow is a graph you draw in the editor: one or more triggers start it, steps do the work, and links between their ports decide which way the data goes. This page explains the model behind the canvas: the document a workflow is stored as, how a run walks through it, what the publish check refuses, and how the flow-control nodes (loop, sub-workflow, wait, approval) behave.

For what starts a workflow, see Triggers and conditions. For how values are read inside a node ({{ email.subject }}, {{ data.sort.category }}), see Data and expressions.

The three kinds of nodes ​

Every box on the canvas is a node, and a node is one of three kinds.

KindWhat it doesExamples
TriggerStarts runs. It has no input port and never runs as a step.trigger.email, trigger.webhook, trigger.schedule, trigger.called, trigger.manual, integration triggers
StepRuns once per run when its inputs are ready, and publishes data for the nodes after it.ai.categorize, mail.send, flow.if, http.request
Service providerSupplies a service to other nodes instead of running. It never becomes a step, has no data input or output, and appears in no run plan.llm.anthropic, llm.openai, llm.mistral, llm.openrouter, llm.ollama, llm.openai_compatible

A model provider is linked to the model service port of an AI node (ai.categorize, ai.extract, ai.summarize, ai.compose, ai.prompt). That node then uses this provider's model. An AI node with nothing on its model port uses the instance defaults. A provider linked to nothing is only a warning (unused_service_provider).

The workflow document ​

A workflow version is stored as one JSON document. This is the shape the editor saves and the API returns:

json
{
  "id": "wfv_…",
  "workflowId": "wf_…",
  "nodes": [
    { "id": "__trigger__", "type": "trigger.email", "version": 1, "name": "Email received", "params": {} },
    { "id": "sort", "type": "ai.categorize", "version": 1, "name": "Sort", "params": { "…": "…" } },
    { "id": "claude", "type": "llm.anthropic", "version": 1, "name": "Claude", "params": { "…": "…" } }
  ],
  "connections": [
    { "from": "__trigger__", "output": "main", "to": "sort" },
    { "from": "claude", "output": "model", "to": "sort", "input": "model", "kind": "service" }
  ]
}

Nodes carry these fields:

FieldMeaning
idUnique inside the version. 1 to 64 characters among A-Z a-z 0-9 _ -. Connections point at it.
typeThe catalog type, for example ai.categorize.
versionThe node version the workflow was built with.
nameA display label (1 to 200 characters). Nothing references it, but it names the node's data (see Data that flows).
paramsThe settings, raw: {{ }} expressions are stored as typed and resolved at run time.
onErrorfail (default), continue or errorPort. See When a step fails.
positionCanvas coordinates. Purely visual: it never changes the run order.
disabledtrue to disable the node. See Disabled nodes.
notesA free note shown on the canvas (up to 2,000 characters).

Connections carry from and to (node ids), output (the port on the source), input (the port on the target, main when omitted) and kind (data when omitted, or service for a model-provider link). There is a single kind of data link: branching is expressed by port names, never by special link types.

Two optional fields sit next to the graph and are ignored by the publish check and by real runs: notes (sticky notes on the canvas) and pins (outputs pinned on a node for test runs). Any other top-level key is refused.

Ports and branches ​

A port is a named entry or exit on a node. Most nodes have one input, main, and one output, main. Nodes that decide something have several outputs, and each output starts its own branch.

Port(s)NodeTaken when
mainmost nodesthe node succeeded
true / falseflow.if (Condition)the conditions hold / do not hold
cat:<category> and otherai.categorize (Categorize)one port per category, named cat: plus the category name exactly as typed (cat:Invoices); other exists only when the fallback is "Exit through Other"
case:<rule> and otherwiseflow.switch (Switch)one port per rule, named case: plus the rule name (case:Urgent); the first rule that matches wins; otherwise when none does
item and doneflow.loop (Loop)item opens the loop body; done fires once at the end
approved / rejectedflow.approval (Approval)the approver's decision, or the timeout action
main, event, pastflow.wait (Wait)main when the deadline is reached; event exists only when an early wake-up is set; past exists only in "Until a date" mode with the "already passed" branch chosen
errorany node with onError: "errorPort"the node failed
noneflow.stop (Stop)the branch ends here

Port names derived from your input (categories, switch rules) are kept exactly as typed, accents and spaces included. A port name must be non-empty, have no leading or trailing space, contain no control character, and be at most 200 characters. Renaming a category or a rule renames its port, and the link that used the old name becomes invalid (unknown_output_port) until you reconnect it.

A step always leaves through one port per run. ai.categorize with "Multiple categories" ticked still exits through the first true category; the full list is in its data.

How a run walks the graph ​

A run follows the data links from the trigger that fired. The rules are fixed and do not depend on where boxes sit on the canvas.

  • Only the firing trigger's branches run. A workflow with an email trigger and a webhook trigger runs only the webhook half when a webhook call arrives, and vice versa.
  • A node is ready when all its inputs are settled and at least one of them is live. Settled means the node upstream has finished (success, failure handled by onError, or skipped). Live means it succeeded and left through the port this link starts from. This is an OR join: a node linked to both true of a condition and cat:Invoices of a categorizer runs as soon as either path arrives.
  • Branches that are not taken are skipped, and the skip propagates to everything that only depends on them. The run detail shows them as skipped with the reason "branch not taken".
  • Independent branches can run in parallel. When an order between two nodes matters, link one after the other.
  • The order is deterministic. Ties are broken by node id, so the same graph always runs in the same order.
  • The graph has no cycle. A link that would bring the data back upstream is refused at publication (cycle_detected). Repetition goes through flow.loop, which runs its body in separate runs.

Data that flows between nodes ​

Each run carries:

  • email — the triggering email, when the trigger brings one (see the triggering email).
  • data.<node> — what each finished step published. The key is the node's name turned into an identifier: accents removed, other characters replaced by _, lower case. A node named Sort publishes under data.sort. The node id is always available as an alias (data.n3 for a node with id n3, with - replaced by _).
  • The trigger's own data, under a key named after it: data.webhook (webhook body), data.input (sub-workflow input), data.schedule (schedule times), data.airtable, data.notion, data.mynotary, data.yousign (integration events).

Renaming a node changes the key of its data, so expressions that quoted the old name must be updated. The full syntax is on Data and expressions.

Node effects ​

Every node declares what it does outside the product. This declaration drives test runs, the send kill switch and the organisation's node policy.

EffectMeaningExamples
noneReads, computes or decides; can be replayed without consequence.AI nodes, flow.if, flow.switch, flow.loop, flow.wait, flow.approval, workflow.call, mail.compose, triggers, model providers
external_writeWrites somewhere outside the workflow: moves or flags a message, calls an HTTP API, writes to a table, a drive or a CRM.mail.move, mail.flag, http.request, notify.send, table writes, integration nodes, flow.signal
sendSends an email.mail.send

Why it matters:

  • In test runs, nodes with an effect describe what they would have done instead of doing it. Nothing is sent, moved or written. flow.signal is external_write for this reason: a real signal would change the state of other, real runs.
  • The send kill switch holds back send operations only. When sending is turned off (the SEND_ENABLED instance setting, or the organisation switch), pending sends stay queued and leave when sending is turned back on; moving, flagging and drafting keep working. See Governance.
  • Loops and sub-workflows have no effect of their own. The nodes inside them do: a mail.send in a loop body sends one email per iteration.

When a step fails ​

Each node has an error policy, set in its settings (onError in the document):

ValueLabelWhat happens
fail (default)Stop the runTemporary errors (network, rate limit, 5xx) are retried automatically; when retries run out, or on a permanent error, the run fails.
continueContinueThe step is marked done, the error stays visible on it, and the run goes on through main.
errorPortFollow the error branchThe node gains an error output; on failure the run goes on through it, so you can link a fallback.

Retrying a failed run and reading error codes are covered in Errors and retries.

Disabled nodes ​

Disabling a node keeps it in the document but stops it from running: no call, no effect, no model request. The data passes through it, and the nodes after it run as if it were not there.

The data leaves a disabled node through main when the node has that output, otherwise through its first declared output (for example the first category of a categorizer). Branches linked to its other outputs are not taken. To cut a branch, delete the link instead.

A disabled node raises the warning disabled_node. A disabled trigger does not fire. When every trigger is disabled, the workflow still publishes, with the warning all_triggers_disabled.

Validation at publication ​

Saving a draft never blocks: the editor shows problems as you build. Publishing runs the full check and refuses the publication while any error remains. Warnings are shown but do not block.

Blocking errors ​

CodeMeaning
no_triggerThe workflow has no trigger.
empty_workflowThere is no step besides the triggers.
duplicate_triggerA trigger type that a workflow can hold only once appears twice (trigger.webhook, trigger.called, trigger.mynotary, trigger.notion, trigger.yousign).
trigger_has_incomingA link points at a trigger. Nothing can flow into a trigger.
invalid_trigger_conditionsAn email trigger's conditions cannot be evaluated (see condition errors). Checked on disabled triggers too.
invalid_scheduleA schedule trigger cannot be armed. The reason is one of cron_missing, cron_field_count, cron_field_syntax, cron_too_frequent, interval_out_of_range, unknown_timezone. Checked on disabled triggers too.
duplicate_node_idTwo nodes share an id.
unknown_node_type / unknown_node_versionThe catalog has no such node, or not in this version.
invalid_paramsA setting is missing or invalid. Includes resource_missing when a setting points at something that no longer exists (for example a deleted mailbox chosen as the sending mailbox).
carrier_email_requiredThe node acts on the triggering email, or one of its settings needs it, and at least one trigger starts runs without an email. See the triggering email.
unknown_connection_source / unknown_connection_targetA link starts or ends at a node that does not exist.
self_connectionA link goes from a node to itself.
unknown_output_port / unknown_input_portA link uses a port the node does not have (often a renamed category or rule).
invalid_service_connectionA model-provider link is wrong. The reason is one of not_a_service_provider, unknown_service_output_port, unknown_service_port, service_kind_mismatch, duplicate_service_connection (two providers on one port), service_port_data_connection.
cycle_detectedThe data links form a cycle.
invalid_loopA loop body is not cleanly delimited. The reason is one of loop_body_empty, loop_body_overlaps_done, loop_body_outside_edge, loop_nesting_too_deep.

Warnings ​

CodeMeaning
unreachable_nodeNo path leads to this step from a trigger: it will never run.
unused_service_providerA model provider is linked to no node.
duplicate_connectionThe same link exists twice.
disabled_nodeThe node is disabled and will be passed through.
all_triggers_disabledEvery trigger is disabled: nothing will start the workflow on its own.
trigger_not_armedThe trigger does not fire on its own. Only trigger.manual raises it: you start it yourself.
carrier_email_inheritedThe workflow is called by other workflows and this node needs an email: it works when the caller has one, and fails when the caller was started by a schedule or a webhook.

Other publication refusals ​

Besides the graph check, publishing can be refused with:

  • workflow.forbidden_node — the organisation's node policy forbids a node type used in the graph (see Governance);
  • workflow.call_cycle — the workflow would call itself through its workflow.call nodes, directly or through other published workflows.

Loops ​

flow.loop (Loop) repeats part of the graph once per element of a list: each attachment, each table row, each recipient.

  • Two outputs. What you link to item (shown as "for each") is the loop body. It runs once per element, each time in a separate child run with its own steps, effects and trace. done fires once, after every iteration has concluded, with the counts in the loop's data.
  • No return link. The body does not loop back to the Loop node; the graph stays acyclic.
  • Inside the body, the current element is {{ data.item }}, along with {{ data.index }}, {{ data.count }}, {{ data.first }} and {{ data.last }}. Everything produced before the loop stays readable.
  • The list comes from an expression ({{ data.read.rows }}, {{ email.attachments }}) or a hand-written list (one value per line or comma-separated).
  • Settings: items per iteration from 1 to 100 (above 1, data.item is an array); iterations in parallel from 1 to 5 (1 keeps the order); on an iteration failure, stop the loop (default) or carry on and collect the error; maximum iterations from 1 to 500 (default 100, and a longer list fails the loop rather than handling part of it); a time budget in minutes (default 60, at most 24 hours).

The publish check enforces a clean body:

  • loop_body_empty — nothing is linked to item;
  • loop_body_overlaps_done — a node is both in the body and after done, so it would run twice;
  • loop_body_outside_edge — a node in the body receives a link from outside the loop (remove it: the body already sees everything before the loop);
  • loop_nesting_too_deep — more than two nested loops.

Details: Loop node.

Sub-workflows ​

workflow.call (Call a workflow) runs another workflow, so a routine you use in five workflows can live in one.

  • The target must be published and have an active trigger.called (Called by a workflow) trigger. It is chosen by id: renaming nodes inside it breaks nothing for the callers.
  • Input: the called workflow receives the caller's working data under {{ data.input }}, and the same triggering email when the caller has one ({{ email.subject }} reads the same message on both sides).
  • Two modes. "Wait for completion" (wait, default) suspends the caller until the child concludes; the child's data then becomes this node's output under output ({{ data.call.output.compose.body }} for a step named Compose in the child). "Fire and forget" (fireAndForget) starts the child and continues at once.
  • Maximum wait from 1 to 4,320 minutes (default 60). After it, the branch resumes with status: "timeout".
  • Test runs are inherited: a test run calls the child in a test run too, so nothing is sent.
  • Failure: a child that fails makes the calling step fail, and the calling node's onError applies.
  • Limits: calls chain at most three levels below the first run. A chain that loops back (A calls B, B calls A) is refused at publication (workflow.call_cycle) and, if it appears later, at run time.

Details: Call a workflow and Called by a workflow.

Waiting and signals ​

flow.wait (Wait) pauses a run for a duration, until a date, or for a number of business days or hours. Nothing is held in memory: the run is stored as waiting and resumes when the deadline or an event arrives, even after a restart.

  • Modes: duration (minutes to years; months and years are calendar months and years), until (a date taken from the data, with an offset and a time of day), business (business days or hours, skipping weekends, public holidays and company closures set in the administration).
  • Outputs: main when the deadline is reached; event when an early wake-up happens first; past when, in until mode, the date had already passed and "If the date has already passed" is set to "Take the "already passed" branch" (the other choices are "Continue right away", the default, and "Fail").
  • Early wake-up: on a reply in the thread of the triggering email (optionally only from a given address or @domain), or on a signal carrying a correlation key. Waking on a reply needs a triggering email: without one, publication refuses it (carrier_email_required).
  • The longest wait is 730 days unless the instance sets a lower ceiling.

flow.signal (Emit a signal) ends the waits that watch a given key, for example case:{{ data.extract.reference }}:documents-received. Its action is resume — "Wake the waits": they leave through event, and short values set in "What the signal carries" are readable under data.<node>.signal — or cancel — "Cancel the waiting runs": nothing that followed the wait runs. Keys are shared across the organisation. An outside system can emit the same signal with POST /api/v1/signals (session, or API key with the signals:write scope).

Details: Wait and Emit a signal.

Approvals ​

flow.approval (Approval) stops the run until a person decides.

  • Two outputs, approved and rejected, so a refusal can be handled as well as an approval.
  • Timeout from 1 to 720 hours (default 48). Without an answer, the "Without an answer" setting decides: reject (default) or approve.
  • In a test run the node approves itself at once and records that it would have asked.

Approval requests, decisions and the review of drafts before sending are described in Review and approvals.

Draft, published, paused ​

A workflow has a draft, which the editor saves as you work, and at most one published version, which is what triggers run.

  • Publishing runs the check above, freezes the draft as a new published version and arms its triggers. A published version never changes: every run uses the exact version it started with, and editing the draft changes nothing for runs in progress.
  • Unpublishing disarms the triggers.
  • Pausing keeps the published version but starts no new run from incoming emails, schedules or polling triggers; runs already running finish. Resuming needs no republication.
  • Archiving disarms the triggers and hides the workflow without deleting its history. Only a draft that was never published and never ran can be deleted.

Version history and restoring an earlier version are covered in Versions.

Copying, importing and exporting ​

In the editor, you can copy a selection of nodes and paste it into the same workflow: pasted nodes get new ids, and only the links between the copied nodes are kept. Triggers that a workflow can hold only once (trigger.webhook, trigger.called, trigger.mynotary, trigger.notion, trigger.yousign) are left out of the copy.

There is no file import or export in the editor. To move a graph programmatically, use the API: GET /api/v1/workflows/:id/draft returns the draft document, and PUT /api/v1/workflows/:id/draft saves one ({ "graph": { … } }, with an optional expectedDraft to refuse the save with 409 workflow.draft_conflict if someone else changed the draft meanwhile). Saving never blocks on validation; publishing does. See the API reference.