Workflows
The canvas, nodes and edges, and how Handlebars context resolves between them.
A workflow is a directed graph: nodes are the steps, edges are the order they run in. This page covers the canvas mechanics and - the part that matters most once you're building anything real - exactly how data flows from one node to the next.
The canvas
Workflows are built on a React Flow canvas. Every workflow starts as a single placeholder node; picking a trigger from the palette replaces it and the graph becomes editable. From there:
- Drag a node from the palette onto the canvas.
- Drag from a node's output handle to another node's input handle to connect them.
- Double-click a node (or use its context menu) to open its configuration dialog.
- Right-click a node for actions - open settings, duplicate, delete, copy node id - and right-click empty canvas to add a node at the cursor position, organized into the same categories as the palette (Triggers, Core Actions, Flow Control, Data & Utilities, and the app-integration groups).
- Toggle the layout direction between horizontal and vertical reading order, with an auto-layout button that tidies an existing graph into clean ranks.
- Drop a Sticky Note anywhere for documentation - a resizable, colored, markdown note that never executes and is excluded from JSON export's validation, so it's purely for annotating the graph for yourself or a teammate.
The Add-node palette is searchable once you have more than a handful of nodes to choose from - see the full list in the node reference.
Nodes and edges
Every node has:
- A type (
WEBHOOK_TRIGGER,HTTP_REQUEST,BRANCH,SLACK, and so on - see the node reference for the full catalog). - A config object, edited in the node's dialog - the fields differ per node type, but most action and data nodes have a
variableNamefield that names where the node's result is stored. - Zero or more credentials, referenced by id and resolved (decrypted) only at execution time. See Credentials.
Edges connect an output handle on one node to an input handle on another. Most nodes have a single main output, but a few have more than one - Branch has true/false, Loop has loop (the per-item body) and done (runs once, after), and any node with an error edge attached gets a second error output that only fires when the node throws after its retries are exhausted.
The run context
Every execution carries a single context object that accumulates as the graph runs. Each node reads whatever it needs from the context (usually via Handlebars templates in its config fields) and returns an updated context - almost always the previous context plus one new key, named after that node's variableName.
A trigger seeds the context first. What key it writes under depends on the trigger:
| Trigger | Context key | Example |
|---|---|---|
| Webhook | webhook - the raw JSON body of the POST | {{webhook.email}} |
| Google Form | googleForm - formId, formTitle, responseId, respondentEmail, responses, raw | {{googleForm.respondentEmail}} |
| Stripe | stripe - eventId, eventType, timestamp, livemode, raw (the Stripe object) | {{stripe.eventType}} |
| Form | form - your declared field values | {{form.email}} |
| On Workflow Failure | failure - workflowId, workflowName, executionId, error, errorStack, failedNodeId | {{failure.error}} |
| Manual / Schedule | whatever sample payload or context you provide when testing | - |
Every account also gets a standing vars key seeded automatically into every execution's context, resolved from whatever you've set on the Variables page - so {{vars.apiBase}} resolves in any workflow without per-workflow setup.
From there, every action node adds its own key. An HTTP Request node configured with variableName: myApi returns its response under {{myApi.httpResponse.status}} and {{myApi.httpResponse.data}}. An OpenAI, Anthropic, or Gemini node configured with variableName: summary returns its generated text as {{summary.text}}. Downstream nodes reference any of these the same way, in any templated field - a Slack message body, an HTTP request URL, a Branch condition's left-hand value, and so on.
Webhook -> HTTP Request (variableName: lookup) -> Slack
message: "Status: {{lookup.httpResponse.status}}, email {{webhook.email}}"Expressions are resolved with Handlebars, so dot-path access and the built-in #if/#each blocks work, on top of a library of 91 functions for text, numbers, dates, arrays, objects and comparison - {{upper contact.name}}, {{dateFormat order.createdAt "yyyy-MM-dd"}}, {{join (pluck items "sku") ", "}}. See Expressions for the full reference. A malformed template resolves to an empty string rather than failing the run.
Related
- Expressions - the
{{ }}language and every function you can call in it. - Triggers - every way to start a run, and what each one puts in the context.
- Flow control - branching, looping, delays, and error handling.
- Executions - where to see exactly what a node's input and output looked like.
- Tables and variables -
{{vars.X}}and data tables in full.