Skip to main content

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:

RoleWhat it does
Branch, SwitchRoute the run down different paths based on the data
Loop, ParallelRepeat over a list, or run several paths at once
TransformReshape data: rename, merge, calculate, set fields
HTTPCall any HTTP endpoint that is not in the catalog
CodeRun a custom code step
DelayPause the run for a set time
AI Agent, AI Generate, AI Summarize, AI Classify, AI ExtractRun a model against the run's data
Call WorkflowInvoke 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."

Routing nodes do not change your data

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:

  • data is 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.
  • meta is engine-owned control metadata, like which path a Branch chose. It never leaks into a node's input on its own.
  • stepOutputs is 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.

AliasWhat it reads
$triggerThe trigger's output payload (alias for the trigger step's output)
$stepsThe immutable output snapshot of prior nodes, read as $steps.<nodeId>
$inputThe current node's input payload
$metaEngine 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.plan

The 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.plan

Expressions 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.

$vars is reserved, not usable yet

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.

  1. Place the source step before the one reading it

    $steps only 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.

  2. 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 its data. 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].title

If 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.
Was this helpful?