- Documentation
- Concepts
- How triggers and actions work together
How triggers and actions work together
Understand the two node roles in a workflow and how each step's output becomes context the next steps can read.
Every workflow is a graph: one trigger plus the nodes connected after it. Those nodes fall into two roles. A trigger decides when a workflow runs and supplies the first payload. Everything after it does the work or steers the path the run takes. Once you see how a step's output is handed to the steps that follow, the rest of the builder stops feeling like configuration and starts feeling like a pipeline you are wiring by hand.
The two roles
A workflow graph has exactly one entry point and any number of downstream nodes. The distinction between them is not cosmetic. It changes what a node can do and where it can sit on the canvas.
Trigger
The trigger is the single node that starts a run. A workflow has exactly one, and it is always the first node on the canvas. When you open the Add node panel on an empty workflow, the only thing it offers is a trigger, because nothing downstream can exist until there is something to fire it.
A trigger fires from a live event (a webhook arrives, a schedule comes due, a feed polls, a form is submitted) or from a manual start. When it fires, its payload becomes the starting data for the run. Every later step can read it.
Triggers come in twelve kinds: Webhook, App Webhook, Polling, Schedule, Stream, Message Queue, Email, RSS, Sub-workflow, Error, Manual, and Form. The kind controls how the trigger receives events, but the role is the same in every case: it is the one node that begins the run and hands the first payload to everything after it.
Action and system nodes
Everything downstream of the trigger is a node that does work or controls flow. There are two kinds.
An app action is a provider operation pulled from the integrations catalog, such as sending a Slack message or creating a HubSpot contact. Action nodes call out to a real service, so most of them need a connection to authenticate.
A system node is a built-in node that is not tied to any single app. The ones you can add today are:
| Role | What it does |
|---|---|
| Branch, Switch | Route the run down different paths based on the data |
| Loop, Parallel | Repeat over a list, or run several paths at once |
| Transform | Reshape data: rename, merge, calculate, set fields |
| HTTP | Call any HTTP endpoint that is not in the catalog |
| Code | Run a custom code step |
| Delay | Pause the run for a set time |
| AI Agent, AI Generate, AI Summarize, AI Classify, AI Extract | Run a model against the run's data |
| Call Workflow | Invoke another published workflow as a sub-step |
The split that matters for building is simple. The trigger answers "when does this run start, and with what." App actions and system nodes answer "and then what happens."
Branch, Switch, Loop, and Parallel decide which steps run next. They pass the data through untouched. Only data-producing steps (app actions and Transform) change what the next step sees.
How output becomes context
A run is not a set of isolated steps. Each step's output is captured and made available to every step that comes after it. That captured output is the context of the run, and it is what lets a node read the trigger payload, or the result of an action three steps back, and use it in its own configuration.
Every step's output has the same shape
Whatever a step produces (an app action's API response, a Transform's reshaped object, the trigger's incoming payload) is stored in one canonical shape:
{
"data": {},
"meta": {}
}data is the payload the step produced. meta is engine-owned information about the step, such as a routing decision. Because every step is normalized to this shape, you reference any step's output the same way regardless of what kind of node produced it. There is no special syntax for an app action versus a Transform versus the trigger.
The run keeps three separate planes
Under the hood, the run separates the values it carries into three planes so they never collide:
datais the working payload nodes operate on. Only data-producing steps (app actions, Transform) change it. Routing steps (Branch, Switch, Loop, Parallel) pass it through unchanged.metais engine-owned control metadata, like which path a Branch chose. It never leaks into a node's input on its own.stepOutputsis an immutable snapshot of every step's output, keyed by node. Once a step finishes, its entry here cannot be rewritten by a later step. This snapshot is what you read from when you reference an earlier step by name.
The immutability is the part worth holding on to. A step five nodes down can still read exactly what step one produced, because step one's output was frozen the moment it completed.
The aliases you write
In any node's configuration field you reference upstream values through aliases. Typing $ in a field opens an autocomplete picker of what is available at that point in the graph.
| Alias | What it reads |
|---|---|
$trigger | The trigger's output payload (alias for the trigger step's output) |
$steps | The immutable output snapshot of prior nodes, read as $steps.<nodeId> |
$input | The current node's input payload |
$meta | Engine metadata (routing decisions) |
Here is the pattern in practice. A trigger receives a new order, and a downstream Slack action references fields from it:
$trigger.customer.email
$steps.lookup_account.data.planThe first reads customer.email straight off the trigger payload. The second reads the plan field from the data of an earlier step named lookup_account. Notice the .data. segment: because every step output is { data, meta }, you reach the actual payload through .data. The trigger is the one shorthand, since $trigger already points at the trigger step's output.
If a node's ID contains characters that are not letters, numbers, or underscores, wrap it in backticks:
$steps.`lookup-account`.data.planExpressions are written in JSONata, which lets you do more than read a single field: you can transform, filter, and combine values inline. The full alias reference and JSONata syntax live in the expressions reference.
The picker lists $vars labeled "coming in Stage 4." Run-scoped variables are not functional
today. The alias resolves to an empty value, so do not build a workflow that depends on storing
state in $vars.
What an upstream step has to do before you can read it
Referencing a step only works if that step has actually produced the field you are reading. Two things have to be true.
Place the source step before the one reading it
$stepsonly exposes nodes that run earlier in the graph. A node cannot read the output of a node that runs after it, or of a node on a branch the run never took.Make the field exist in that step's output
The picker shows real fields by reading each upstream node's pinned test output. To populate it, run a test on the upstream node first. Until you do, the picker shows the alias but not the fields inside it.
When an expression references a step that never ran or produced the field, the run surfaces a specific error rather than failing silently:
MISSING_NODE: "Referenced step ID not found in execution context. Ensure the referenced step has completed before this step runs." You referenced a step that did not run, often because it sits on a branch the run skipped, or it is downstream of the node reading it.MISSING_PATH: "The referenced step exists but the specified path was not found in its output." The step ran, but the field you reached for is not in itsdata. This is the usual signal that you mistyped a path or assumed a field the provider did not return.
Both errors point at the same fix: confirm the source step runs before the reader, and confirm the exact field path against that step's real output.
A note on batch triggers
Some trigger kinds collapse a batch into a single run. An RSS or Polling trigger that pulls fifty items fires one run, not fifty, and the items arrive together as an array. You read the batch as a list and the first item like this:
$trigger.items
$trigger.items[0].titleIf you want to act on each item separately, add a Loop node set to iterate over items. The Loop binds $currentItem and $currentIndex inside its body so each pass works on one element. Digest-style workflows that summarize the whole batch skip the Loop and read $trigger.items directly. This collapse applies to Webhook, RSS, and Polling triggers; other kinds deliver one event per run.
Next steps
- Workflows: how the trigger plus its connected nodes form one versioned graph, and how publishing decides which version receives live events.
- Connections: the reusable credentials an app action uses to authenticate.
- Expressions reference: the complete alias list, scope bindings, and JSONata syntax for reading context.