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.

From a mailbox to its effects: the path every automated email takesConnected mailboxes (Gmail, Microsoft 365, IMAP) are copied into the mirror by push notifications and polling. Each new email passes the scope rules and the guardrails, then the conditions of the email triggers. Other events (webhook, schedule, integration, call from a workflow, failure of a workflow, manual run) reach the triggers directly. A trigger that fires creates a run of the published version of its workflow. The run executes the steps one by one and saves each of them. Its effects on the mailbox (send, draft, move, flag) go through the outgoing queue to the provider and come back into the mirror by synchronisation; other effects write to Tables, HTTP APIs and integrations.MailboxesGmail · Microsoft 365 ·IMAPMirrorsynchronised copy: push +pollingScope and guardrailsmay this email enterworkflows?Triggersconditions of “Emailreceived”Webhook, schedule,integration, call, failure,manual runWorkflowits published versionRunsteps run and saved oneby oneEffectssend, draft, move, flagTables, HTTP,integrationssyncnew emailin scopefiresone run per eventwritesoutgoing queue →provider
From a mailbox to its effects: the path every automated email takes

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.

The three kinds of nodes, and the two kinds of linksA trigger (Email received) has no input and starts the run. It is linked to a step, Categorize, whose outputs are one port per category (cat:Invoices, cat:Quotes) plus other; each output leads to its own branch: Move, Compose a reply, Flag. A model provider, Claude, is linked by a dashed service link to the model port of Categorize: it never runs, it only tells the AI node which model to use.Email receivedtrigger · no inputSortstep · ai.categorizeMovemail.moveCompose a replyai.composeFlagmail.flagClaudeservice provider · llm.anthropicmaincat:Invoicescat:Quotesotherservice link → model port
The three kinds of nodes, and the two kinds of links
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.

Branches not taken are skipped; a join runs as soon as one input is liveA Condition takes its true output: Draft a reply runs and succeeds. The false branch is not taken, so Notify the team is skipped, and Log a row, which depends only on it, is skipped in turn. Move to Done is linked to both Draft a reply and Notify the team: all its inputs are settled and one of them is live, so it runs.TriggerConditionflow.ifDraft a replysucceededNotify the teamskipped · branch nottakenLog a rowskipped · depends onlyon a skipped nodeMove to Doneruns: one live input isenoughtruefalselivesettled, not live
Branches not taken are skipped; a join runs as soon as one input is live
  • 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.

A Loop runs its body once per item, each time in a separate child runThe Loop node reads a list, for example the rows read from a table. What is linked to its item output is the body: it runs once per item, in a child run of its own, with data.item, data.index, data.count, data.first and data.last, and everything produced before the loop. Up to 5 iterations run in parallel (1 keeps the order), at most 500 items. Meanwhile the parent run waits. When every iteration has concluded, the done output fires once, with the counts; nothing links the body back to the Loop node.Body: one child run per itemRead the rowsdata.read.rowsLoopflow.loop · parentwaitsIteration 1data.item, data.index= 0Iteration 2data.index = 1… up to 500Send a reminderSend a reminderAfter the loopdone fires once, with thecountsitemdone: all concluded
A Loop runs its body once per item, each time in a separate child run
  • 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.

Call a workflow: wait for the child, or fire and forgetThe Call a workflow node of workflow A starts a child run of workflow B, which must be published and start with Called by a workflow. B receives A’s working data under data.input and A’s triggering email when there is one. In Wait for completion mode, A’s step waits; when the child concludes, its data becomes data.<call>.output, and a failed child makes the calling step fail. After the maximum wait (60 minutes by default), A resumes with status timeout. In Fire and forget mode, A continues at once. Calls chain at most three levels deep, and a cycle of calls is refused.Workflow AWorkflow B (published)Call a workflowworkflow.callNext step{{ data.call.output.…}}Called by a workflowdata.input + sameemailB’s stepsstarts a child run“Wait for completion”:child concluded, ortimeout“Fire and forget”: at once
Call a workflow: wait for the child, or fire and forget
  • 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.

A Wait node: the run is stored, not held in memory, and resumes on a date, a reply or a signalWhen the run reaches a Wait node, the step and then the run become Waiting and a durable entry records the deadline; no process is busy, even across restarts. When the deadline is reached the run continues through main. An early wake-up continues through event: a reply in the thread of the triggering email, or a signal with the same correlation key, emitted by an Emit a signal node or by POST /api/v1/signals. In Until a date mode, a date already passed can take the past branch. A signal whose action is cancel ends the waiting runs as Cancelled: nothing after the wait runs.Waitflow.wait · stored asWaitingDeadline reachedduration, date, businesstimeEarly wake-upreply in the thread, or asignalDate already passed“Until a date” mode, ifchosenmaineventpastEmit a signalnode, or POST /api/v1/signalsRun cancellednothing after the waitrunsresume, same keycancel
A Wait node: the run is stored, not held in memory, and resumes on a date, a reply or a signal
  • 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.

An Approval node: one request, two channels, the first decision winsWhen a run reaches an Approval node, it waits and a request goes to the owner of the workflow: an email from their own mailbox with single-use Approve and Reject links, and a card in Activity › To do. A reminder is sent halfway through the time limit. The first decision recorded, from either channel, wins and the run continues through approved or rejected. Without an answer before the time limit (48 hours by default, 30 days at most), the Without an answer setting decides: Reject by default. In a test run, the node takes approved at once and sends nothing.Approvalflow.approval · runwaitingRequest emailApprove / Reject,single-use linksActivity › To doApprove / Reject buttonsFirst decisionapprovedrejectedNo answer in time48 h by default ·reminder at half time“Without an answer”: Reject(default) or Approve
An Approval node: one request, two channels, the first decision wins
  • 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.

The life of a workflow: draft, published, paused, offline, archivedA new workflow is a Draft. Publishing freezes the draft as the live version and arms its triggers: Published. Pause stops new runs without disarming anything, and Resume restarts them; a paused workflow stays paused when it is published again. Unpublish withdraws the live version and disarms every trigger: the workflow is offline and shows as Draft until it is published again. Archive disarms every trigger and hides the workflow; unarchiving re-arms nothing, the workflow must be published again. Only a draft that was never published and never ran can be deleted. None of these actions cancels a run in progress.Draftnever publishedPublishedtriggers armedPausednothing new startsOfflineshown as Draft,history keptArchiveddisarmed, hiddenpublishpauseresumeunpublishpublish againarchive (fromany state)unarchive:re-arms nothingdelete (never published,never ran)
The life of a workflow: draft, published, paused, offline, archived
  • 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: incoming emails, schedules and polling start nothing, the webhook URL answers 503 so the sender retries later, and calls from other workflows fail; 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.

Loop & Roll — the workflow builder for email.