Workflow Nodes
Workflows are built from connected nodes that execute in sequence. Each node has a specific type that determines its behavior. This page documents all available node types.
Overview
| Node Type | Category | Purpose |
|---|---|---|
| Event | Trigger | Entry point — reacts to component data events |
| Recycle | Trigger | Timer-based recurring trigger |
| Expression | Operation | Runs JavaScript code |
| Condition | Control Flow | Branches based on a boolean result |
| Switch | Control Flow | Multi-path branching |
| Each | Control Flow | Iterates over a collection |
Triggers
Trigger nodes are the entry points of a workflow. Every workflow must start with exactly one trigger node.
Event
The Event node starts the workflow when a data operation occurs on the component. It listens for a specific event and passes the affected documents to downstream nodes.
Use Event nodes when you need to react to data changes in real time — validating input before it's saved, syncing data to external systems after a create, sending notifications on updates, or cleaning up related records on delete.
Supported Events
| Event | Description |
|---|---|
Create |
After a new record is created |
Before::Create |
Before a new record is created (synchronous — can modify or block) |
Update |
After a record is updated |
Before::Update |
Before a record is updated (synchronous — can modify or block) |
Delete |
After a record is deleted |
Before::Delete |
Before a record is deleted (synchronous — can block) |
For components with multiple schemas, events are scoped per schema (e.g. orders::Create, orders::Before::Update).
Output
When an Event node triggers, it populates the execution context with:
$models— array ofModelinstances representing the affected documents$args— contextual arguments (e.g.current_revisionfor Update events)$context— metadata about the event (event name, initiator, component reference)$event— shorthand for the event name$initiator— the user or service that triggered the operation
See Expression Context for full details on all available variables.
Before Events
Before:: events run synchronously before the operation is committed. They can:
- Modify the data using the
Decoratorobject - Block the operation by throwing an error
Warning
Before:: events block the API response until the workflow completes. Keep them fast and focused.
Recycle
The Recycle node is a timer-based trigger that executes on a fixed schedule. It fetches all records from the component and processes them through the workflow at each interval.
Use Recycle nodes for background jobs that need to run periodically — syncing stale data, sending digest emails, cleaning up expired records, generating daily reports, or polling external APIs for updates.
Configuration
| Field | Type | Description |
|---|---|---|
timer |
string |
Schedule interval |
Available Intervals
| Value | Schedule |
|---|---|
30min |
Every 30 minutes |
1h |
Every hour |
3h |
Every 3 hours |
6h |
Every 6 hours |
9h |
Every 9 hours |
12h |
Every 12 hours |
1d |
Once per day |
mondays |
Every Monday |
tuesdays |
Every Tuesday |
wednesdays |
Every Wednesday |
thursdays |
Every Thursday |
fridays |
Every Friday |
saturdays |
Every Saturday |
sundays |
Every Sunday |
month_start |
First day of each month |
month_end |
Last day of each month |
Output
The Recycle node queries all records from the component matching the optional ruleset filter. The results are available in child nodes as the models variable.
Ruleset
You can filter which records to process by setting ruleset conditions. For example, to only process active users:
Info
A Recycle node can be paused without removing it. Paused recycles are skipped during scheduled ticks.
Operations
Operation nodes perform actions — running code, sending emails, or making HTTP calls.
Expression
The Expression node executes JavaScript code in a secure sandbox. It is the most versatile node type, with access to the full platform SDK and all workflow context variables.
Use Expression nodes for any custom logic — transforming data, calling the SDK, interacting with external APIs, computing values, or preparing data for downstream nodes. It is the general-purpose workhorse of the workflow engine.
Avoid loops for batch operations
If you need to perform the same operation on many items (e.g. send 100 emails, update 100 records), do not use a for loop inside an Expression node. If the loop fails on item 50, the entire node is marked as failed. When the workflow engine retries, it re-executes the entire expression from scratch — repeating items 1–49 that already succeeded. Use an Each node instead for fault-tolerant batch processing.
Configuration
| Field | Type | Description |
|---|---|---|
expression |
string |
The JavaScript code to execute |
Execution Context
Expression nodes have access to the full set of runtime variables and libraries documented in the Expression Context page, including:
$models,$args,$context,$event,$initiator$config,$globalEnv,$currentEnv$sdk— the platform SDKio— data flow control- Platform services (Component, Query, Thunder, Functions, etc.)
- Utility libraries (Lodash, Moment, Handlebars, Crypto, etc.)
Data Flow
Use the io object to pass data to downstream nodes:
Reading Upstream Data
Access data piped from a specific upstream node:
Empty Return
If an expression returns nothing (bare return or no return statement), child nodes are skipped. This is useful for conditionally stopping execution:
if (!$models[0].data.active) {
return // stop here — don't execute downstream nodes
}
io.pipe({ id: $models[0].data.uuid })
Error Handling
| Error Type | Behavior |
|---|---|
throw new Error(...) |
Node fails permanently — no retry |
throw RetryableError(...) |
Node retries with exponential backoff |
See RetryableError for retry behavior details.
Control Flow
Control flow nodes determine which path the workflow takes next.
Condition
The Condition node evaluates a JavaScript expression to either true or false and routes execution to the corresponding branch.
Use Condition nodes for simple yes/no decisions — checking if a value meets a threshold, whether a flag is set, or if a user has a specific role. For decisions with more than two outcomes, use a Switch node instead.
Configuration
| Field | Type | Description |
|---|---|---|
expression |
string |
JavaScript code that calls resolver.resolve() or resolver.reject() |
How It Works
The expression has access to a resolver object with two methods:
| Method | Effect |
|---|---|
resolver.resolve() |
Marks the condition as true — follows the pass connection |
resolver.reject() |
Marks the condition as false — follows the reject connection |
Connections
A Condition node has two outgoing connection types:
| Connection Type | When Followed |
|---|---|
pass |
resolver.resolve() was called |
reject |
resolver.reject() was called |
Nodes on the path not taken are marked as skipped.
Available Variables
Condition nodes have access to a limited context:
$models(via themodelvariable)io— for reading upstream piped datamoment,_(Lodash)
Switch
The Switch node evaluates a JavaScript expression that selects one or more named paths to follow. Unlike Condition which only supports two branches, Switch supports unlimited named paths and can activate multiple paths simultaneously.
Use Switch nodes when a workflow needs to fan out based on a category or type — routing orders by fulfillment method, dispatching notifications to different channels, or triggering different processing pipelines based on record status. Each path can carry its own piped data, so downstream nodes receive only what's relevant to their branch.
Configuration
| Field | Type | Description |
|---|---|---|
expression |
string |
JavaScript code that calls follow.path(name) |
How It Works
The expression has access to a follow object:
| Method | Description |
|---|---|
follow.path(name) |
Selects a named path to follow. Returns a Path object |
Each named path corresponds to a named connection on the node. You can select multiple paths:
if ($models[0].data.type === "subscription") {
follow.path("billing")
follow.path("notifications")
} else {
follow.path("one-time")
}
Path Data
Each path can pipe its own data to downstream nodes:
| Method | Description |
|---|---|
path.pipe(data) |
Pipe data to nodes on this specific path |
path.each(array) |
Pass an array to an Each node on this path |
const order = $models[0].data
if (order.requires_shipping) {
follow.path("shipping").pipe({ address: order.shipping_address })
}
if (order.has_digital_items) {
follow.path("digital").pipe({ items: order.digital_items })
}
Connections
Each outgoing connection has a name that matches the string passed to follow.path(). Paths not selected are marked as skipped.
Available Variables
Switch nodes have access to:
$models(via themodelvariable)io— for reading upstream piped datafollow— the path selection objectDebug— for debuggingmoment,_(Lodash)
Each
The Each node iterates over a collection, executing its child nodes once per item. It is fed data from an upstream Expression or Switch node that calls io.each(array) or path.each(array).
Use Each nodes whenever you need to perform the same operation on multiple items — updating records one by one, calling APIs per item, or processing a batch of data.
Why Each Instead of a Loop?
The critical advantage of Each over a JavaScript for loop inside an Expression node is fault-tolerant, resumable iteration.
Consider this scenario: you need to process 100 items.
// DON'T DO THIS for batch operations
const items = await component.find({ perPage: 100 })
for (const item of items.data) {
await http.post("https://api.example.com/sync", item.data)
}
If the HTTP call fails on item 50, the entire Expression node fails. When the workflow engine retries, it re-executes the whole expression from the beginning — items 1 through 49 are processed again, potentially causing duplicates or wasted work.
// Prepare node: pass items to Each
const items = await component.find({ perPage: 100 })
io.each(items.data)
The Each node processes each item as an independent execution. The workflow engine saves progress after every iteration. If item 50 fails:
- Items 1–49 are already completed and will not be re-executed
- Only item 50 is retried
- Items 51–100 continue processing independently
Rule of thumb
If you're iterating over a list to perform side effects (API calls, emails, data writes), always use an Each node. Reserve Expression loops for pure data transformations that don't have side effects.
How It Works
- An upstream node calls
io.each(array)to provide the collection - The Each node receives the array and iterates over it
- For each item, all child nodes are executed with the current item available via the cursor
- The workflow engine saves a cursor after each iteration, enabling resume from the last successful position
Cursor
Inside child nodes of an Each loop, the current iteration data is available through the cursor:
| Template Variable | Description |
|---|---|
{{_cursor.current}} |
The current item |
{{_cursor.current.<field>}} |
A field on the current item |
{{_cursor.index}} |
The current zero-based index |
{{_cursor.total}} |
Total number of items |
In expression nodes, access the cursor via:
Supported Child Nodes
The following node types can be direct children of an Each node:
- Expression
Error Handling
Each iteration is independent — if one item fails, it does not affect other iterations. Failed iterations retry independently with their own backoff schedule.
This is the key differentiator from a manual loop: the workflow engine tracks which iterations completed, which failed, and which are pending. On retry, only the failed iteration is re-executed — completed iterations are never repeated.
Example
An expression node prepares a list of recipients:
const users = await component.find({ perPage: 100 })
io.each(users.data.map(u => ({ email: u.data.email, name: u.data.name })))
The connected Each node iterates, and its child Expression node processes each item:
const cursor = io.cursor()
const user = cursor.current
// process each user individually
await component.update(user.uuid, { last_notified: new Date().toISOString() })
Node Execution Lifecycle
Every node transitions through the following statuses during execution:
pending → running → completed
→ errored (may retry → running)
→ skipped (condition/switch path not taken)
| Status | Description |
|---|---|
pending |
Node is waiting to execute |
running |
Node is currently executing |
completed |
Node finished successfully |
errored |
Node failed — may retry depending on error type |
skipped |
Node was not executed (unused branch in Condition/Switch) |
Retry Behavior
Expression nodes support automatic retries:
- Retryable errors (timeouts, network issues,
RetryableError) are retried with exponential backoff - Non-retryable errors (syntax errors, validation failures) fail immediately
- Child nodes of an Each loop retry independently per iteration
Connections
Nodes are linked by connections that define the execution flow. Each connection has a type that determines when it is followed:
| Connection Type | Used By | When Followed |
|---|---|---|
| (default) | Operation, Event, Each | Always — after the node completes |
pass |
Condition | When resolver.resolve() is called |
reject |
Condition | When resolver.reject() is called |
| (named) | Switch | When follow.path(name) selects this path |