# Automation triggers, nodes, and expressions

_Every automation trigger and node kind, the values each needs and produces, expression paths, templates, operations, predicates, and graph outcomes._

Canonical: https://snoze.dev/help/automations/

An Automation is a versioned graph. A trigger creates a durable run, nodes read
the trigger or earlier outputs, and labelled edges decide what runs next.
Published versions are immutable; editing creates draft state until you publish
again. Each run keeps its source, trigger payload, node attempts, resolved
input, output, and error.

## Canonical contract lists

<!-- GENERATED:automation-trigger-kinds:START -->

Canonical contract values:

- `manual`
- `schedule`
- `event`
- `webhook`

<!-- GENERATED:automation-trigger-kinds:END -->

<!-- GENERATED:automation-node-kinds:START -->

Canonical contract values:

- `action`
- `condition`
- `transform`
- `delay`
- `for_each`
- `approval`
- `connector_operation`
- `provider_operation`
- `connector_action`
- `ai_prompt`
- `web`
- `sub_automation`
- `merge`
- `database_record`
- `database`
- `notify`

<!-- GENERATED:automation-node-kinds:END -->

<!-- GENERATED:automation-expression-operators:START -->

Canonical contract values:

- `coalesce`
- `concat`
- `add`
- `subtract`
- `multiply`
- `divide`
- `lowercase`
- `uppercase`
- `length`
- `format_payload`
- `payload_title`
- `format_date`
- `add_months`
- `extract_text`
- `filter_list`

<!-- GENERATED:automation-expression-operators:END -->

<!-- GENERATED:automation-predicate-operators:START -->

Canonical contract values:

- `equals`
- `not_equals`
- `contains`
- `not_contains`
- `greater_than`
- `greater_than_or_equal`
- `less_than`
- `less_than_or_equal`
- `is_empty`
- `is_not_empty`
- `exists`
- `not_exists`

<!-- GENERATED:automation-predicate-operators:END -->

## Trigger kinds

### Manual trigger `manual`

Starts only when a person or caller explicitly runs or tests the Automation.
It has no trigger-specific settings. The caller may supply a trigger payload;
paths under `trigger` read that payload. A test run is recorded with source
`test`, while an ordinary invocation uses `manual`.

### Schedule trigger `schedule`

Starts at an interval or cron schedule. An interval requires
`intervalMinutes`. Cron requires both `cronExpression` and an IANA `timezone`.
The scheduler validates the schedule and records the next run time. Scheduled
runs do not acquire an event-catalog payload merely because they run on a
clock.

### Event trigger `event`

Listens to one or more catalog definitions. Each saved event contains a
`definitionId`, required resource `scope`, and optional catalog refinements.
The trigger can also carry predicates over the produced event payload. The old
single `eventType` and `subjectType` fields still decode, but the editor writes
the multi-event shape.

Scope is part of subscription matching, not a condition evaluated after every
event. An event definition with required scope must save those ResourceRefs,
including their human labels. The live definitions and produced paths are in
[Automation event catalog](https://snoze.dev/help/automation-triggers/).

### Webhook trigger `webhook`

Starts when an external caller posts to the Automation's generated webhook
path. Publishing owns the secret, and publishing can rotate it. The parsed
request becomes the trigger payload. The trigger config itself has no URL,
method, or body schema fields; validate expected data with early Conditions.
See [Automate with webhooks](https://snoze.dev/help/automate-with-webhooks/).

## Node kinds

Every node has a stable ID, optional label, canvas position, and optional retry
policy: maximum attempts, initial delay, multiplier, and maximum delay. A node
may only reference nodes that are guaranteed to have run before it.

### Action node `action`

Requires a saved `actionId`. Its optional input map resolves expressions and is
passed as runtime Action input, along with the typed trigger when available.
The node produces the Action run's result. Because the exact shape belongs to
the selected Action, a generic Action node has no static output shape of its
own.

### Condition node `condition`

Requires one predicate: left expression, operator, and an optional right
expression. It produces `{ result: boolean }` and takes the `true` or `false`
edge accordingly. Empty and existence operators do not need a right side.

### Transform node `transform`

Requires a map from output keys to expressions. It evaluates every entry and
produces that map unchanged in shape. This is the main way to rename paths,
combine text, calculate numbers, or reduce a large earlier result to the values
later steps need.

### Delay node `delay`

Requires `durationSeconds`. The run parks durably until its ready time instead
of holding a process open. On resumption it produces `{ resumedAt }` as an ISO
date. A negative or nonsensical duration is not described as useful behaviour;
authors should use a non-negative duration.

### For-each node `for_each`

Its `items` is either an expression that must resolve to an array, or a
`database_records` source with a fixed or expression-derived database ID and
optional limit. It produces `{ items, count }`; each iteration exposes its
current value and index through `scope`, and `item`/`done` edges separate the
loop body from continuation.

The runtime accepts at most 2,000 items per run. A direct database source pages
up to that limit; a searched Database record node is separately capped at 100.
`maxConcurrency` controls how many iterations may progress together.

### Approval node `approval`

Pauses the run with optional human instructions. Resuming requires an
`approved` or `rejected` decision and may include additional output. The
declared downstream value is `{ decision }`. This is a durable wait, not a
Condition that guesses consent.

### Connector operation node `connector_operation`

Requires a legacy connector and operation. `settings` and runtime `input` are
expression maps. At execution Snoze chooses the workspace's oldest installed
connection for that connector; a missing connection is a non-retryable
configuration error. The raw connector result becomes the output, and a
catalog shape is used when one is declared.

### Provider operation node `provider_operation`

Requires an exact `connectionId` and operation, with optional resource ID,
expression input, and confirmation flag. Picked ResourceRefs are unwrapped to
provider IDs at execution. The provider result becomes the output, whose named
tokens come from the operation catalog. Provider permissions, mutation flags,
and confirmation requirements remain in force.

### App action node `connector_action`

Runs one action of a connected app live, such as `linear.create_issue`,
`slack.post_message` or `stripe.list_invoices`. `input` is an expression map;
each value is converted to the type the action's field expects, and the
action's own output fields are what later nodes can read. The account is the
automation's Run as connection when it belongs to that app, otherwise the
step's chosen workspace connection, otherwise the app's only workspace
connection; a person's own account (their email) is used only when the
automation runs as that person. Actions that cannot be taken back, like
posting a message or sending an email, run only after a person turns on
"Allow this action to make changes" for the step. A failure the app calls
temporary is retried only for reads and actions that are safe to repeat.

### Web access node `web`

Search public web sources, read a webpage, or research named output fields.
Search supports domain and publication-date filters; search and page reads can
require a maximum cache age. Retrieval time is not the date of a quote: verify
the date and units in the source before using time-sensitive values.

Structured research returns typed values with citations for each field and
resumes its saved research run while waiting. It supports domain filters, but
strict cache-age and publication-date guarantees require search/read steps.
Missing, incomplete or insufficient evidence fails the step; use a failure
route to handle unavailable sources. Research objectives, field descriptions
and source controls share a 25,000-character request budget.

Parallel must be configured for extraction guarantees and structured research.
Basic search remains available through configured legacy providers when no
source or freshness controls are requested.

### AI prompt node `ai_prompt`

Requires a prompt expression that resolves to non-empty content. Structured
values are rendered rather than converted to `[object Object]`. Choose a model
or use Auto, and optionally show the transcript in the workspace chat list.

**Access** has three choices: **Read only**, **Edit selected items**, and
**Full access**. Selected folders include their current descendants; selected
databases include their records. Selected-item access allows ordinary reads,
while restricting changes to those items and excluding workspace settings and
connected-app changes. Every choice still respects the automation's existing
permissions. Older steps retain their saved tool restrictions until an access
option replaces them.

**Attachments** accepts a workspace file ID, a `file://` reference, or a list
of those values from an earlier step or a database file field. The step uses
the same file reading support as AI chat. For Gmail triggers, choose **Email →
Attachments** to pass the files directly: Snoze downloads them through the
connected account and stages private AI files when the step runs. They follow
chat-file access until explicitly saved to the workspace. Attachment names and
counts describe the files but cannot be used to read their contents. A step can
read up to 10 files, with a 25 MB limit per email attachment.

**Allow user input** is off by default. The AI knows it is running an automation,
uses the available context, and stops with a blocker if essential input is
missing. When enabled, it can ask an essential question or request approval;
the run shows **Waiting for user input** and resumes after a response. Downstream
steps wait with it.

It produces `text`, `toolCalls`, `blocked`, and `chatId`. A blocked turn does
not report success or let successors run.

### Sub-automation node `sub_automation`

Requires another `automationId` and may map expressions into its trigger input.
The parent waits for the child run and exposes the child Automation's result.
That shape is dynamic because it belongs to the selected child. Use this for a
reused workflow, while avoiding recursive call chains.

### Merge node `merge`

Joins graph paths with optional strategy `all` or `any`. `all` waits for all
required incoming paths; `any` can continue when an eligible path arrives. It
produces `{ merged: true }`. It combines control flow, not the object outputs
from those branches; use a Transform to construct a combined value.

### Database record node `database_record`

Performs `create`, `update`, `delete`, `find`, or `list` in a fixed database or
one produced by an earlier node. Create accepts title and field expression
maps and returns record ID, title, and data. Update additionally requires a
record ID and returns the post-update record. Delete requires a record ID and
returns record ID plus `deleted: true`. Find accepts optional text search and
returns `found` and a record or null. List accepts search and limit and returns
`records` and `count`.

Unsearched List pages up to a clamped 1–2,000 limit, default 50. Searched List
uses the search path and returns at most 100. Writes still enforce writable
field kinds, validation, and database permissions.

### Database node `database`

Performs `duplicate` or `instantiate_template`. Duplicate needs a fixed or
expression-derived source database ID, optional target label, and
`includeRecords` (default true). Instantiate template needs `templateId` and an
optional target label. Both produce `{ databaseId, label }`, which can feed a
Database record or For-each node later in the same run.

### Send to Inbox node `notify`

Sends a Snoze notification to workspace members' Inbox. `recipients` resolves
to user IDs or emails — fixed people, a person field such as a record's
assignee, or a list from an earlier step — and is capped at 50 people per run.
`title` is required and `body` is an optional message shown under it. `link` is
optional: a record ID, a page or other item ID, or an `https://` address that
opening the notification shows. A recipient who cannot open the linked item
still receives the message, without the link. Recipients who are not members
of the workspace are skipped, not an error. Each person's notification
preferences decide whether it also arrives by push or email (push is on and
email off by default). It produces `{ recipients, count, skipped }`. A retried
step never notifies anyone twice.

## Value expressions

A literal expression stores a fixed JSON value. A path expression has
`source: trigger | node | scope` and a string `path`; node paths also require
`nodeId`. Array indices are decimal path components. A missing segment resolves
to `undefined`.

A template interpolates `{{ trigger.path }}`, `{{ scope.path }}`, or
`{{ nodes.nodeId.path }}`. Missing values become an empty string. It is string
interpolation, not JavaScript. An operation evaluates its argument expressions
first and applies one of the following operators.

Use the **Format value** button beside an expression to choose a formatting
operation and edit its arguments. **Before** and **After** use the values you
entered or the selected run sample; an unavailable source stays unresolved.
**Apply** saves the expression, while **Cancel** leaves the original value
unchanged. Formatting previews do not execute steps or write records. You can
edit an existing formula, nest formulas, or remove its outer formatting to
restore the first argument.

### First non-empty `coalesce`

Returns the first argument that is not undefined, null, an empty string, or an
empty array. Other objects and zero are not empty.

### Join text `concat`

Converts every argument to text, treats null or undefined as an empty string,
and joins without an automatic separator.

### Add numbers `add`

Converts finite numeric values and adds them. A non-numeric argument
contributes zero.

### Subtract numbers `subtract`

Starts with the first finite numeric argument, or zero, then subtracts each
remaining finite value. A non-numeric remaining argument subtracts zero.

### Multiply numbers `multiply`

Multiplies finite numeric arguments, starting from one. A non-numeric argument
is treated as zero and therefore makes the result zero.

### Divide numbers `divide`

Starts with the first finite number, or zero, and divides by each remaining
number. Division by zero or by a non-numeric value produces `NaN`; do not rely
on that as a portable downstream value.

### Lowercase text `lowercase`

Converts the first argument to text and applies Unicode-aware JavaScript
lowercasing. A null or missing value becomes an empty string.

### Uppercase text `uppercase`

Converts the first argument to text and uppercases it. A null or missing value
becomes an empty string.

### Value length `length`

Returns string length, array length, or the number of keys on an object. Other
values return zero.

### Format payload `format_payload`

Formats a trigger or step payload into readable text. Its optional second
argument controls the payload presentation settings. This is the product's
bounded payload formatter, not arbitrary serialisation.

### Payload title `payload_title`

Derives the short display title Snoze uses for a payload. Use it for labels or
notifications when the full formatted payload would be too large.

### Format date `format_date`

Renders an instant (an ISO string such as a schedule trigger's `scheduledFor`,
or a date field value) as text in a time zone. Arguments: the value, a pattern,
and an optional IANA time zone (UTC when omitted). Patterns: `month_year`
("September 2026"), `year_month` ("2026-09"), `month`, `year`, `date` ("September
1, 2026"), `iso_date` ("2026-09-01", the default). A date without a time of day
(a date field's "2026-09-01") is that day in every zone. A recurring automation
that names a period should derive the name this way rather than typing a month.

### Add months `add_months`

Shifts an instant by whole months in a time zone and returns the ISO instant the
same day of the target month (clamped to that month's length) begins in that
zone. A date without a time of day moves to a date: "2026-01-31" plus one month
is "2026-02-28" in every zone. Arguments: the value, the number of months
(negative for earlier), and an optional IANA time zone. Combine with `format_date` to label the period that just ended:
`format_date(add_months(scheduledFor, -1), "month_year", zone)`.

### Extract text `extract_text`

Arguments: source text, text to start after, and text to stop before. Delimiters
are literal, case-sensitive text, not regular expressions. Extraction starts
after the first opening delimiter and ends at the first closing delimiter
after it. Leave the opening delimiter empty to start at the beginning, or the
closing delimiter empty to continue to the end.

For example, `extract_text("Order [AB-42] accepted", "[", "]")` returns
`"AB-42"`. An absent delimiter returns empty text, which can feed `coalesce`
for a fallback. A source that is not text returns no value. The operation does
not extract every occurrence or parse arbitrary HTML.

### Filter a list `filter_list`

Arguments: a list, an item field path, a rule, and an optional comparison value.
Leave the path empty to compare each whole item, or use a dotted path such as
`order.amount` to read a field within each item. Only the item's own properties
are read. Matching items retain their original values and order; the source
list is unchanged.

Rules are `equals`, `not_equals`, `contains`, `greater_than`, `less_than`,
`exists`, and `is_empty`. Equality preserves types: numeric `1` does not equal
text `"1"`; objects and arrays compare by identity, not deep structure.
Contains uses case-sensitive text matching or array membership. Greater-than
and less-than compare finite numeric conversions; missing or empty values do
not qualify. In this list filter, **Have a value** (`exists`) excludes null,
undefined, empty text, and empty lists, but retains false and zero. **Are
empty** (`is_empty`) selects those empty values. These two rules do not need a
comparison value.

For example, `filter_list([{ "amount": 0 }, { "amount": 12 }], "amount",
"greater_than", 5)` returns `[{ "amount": 12 }]`. A non-list source returns no
value. For literal comparisons, choose Text, Number, or True / false beside
**Compare with**; run-data values retain their source type. A missing sample
does not produce a fabricated filtered result in the preview.

## Predicate operators

### Exactly equal `equals`

Uses identity equality. Objects and arrays compare by identity rather than by
deep structure.

### Not exactly equal `not_equals`

Negates identity equality.

### Contains `contains`

For text, tests a case-sensitive substring after converting the right value to
text. For arrays, tests whether an element is identity-equal to the right
value. Other left values return false.

### Does not contain `not_contains`

Negates `contains`, so unsupported left values return true.

### Greater than `greater_than`

Converts both sides to finite numbers and compares them. A failed conversion
does not behave like ordinary text comparison and evaluates false.

### Greater than or equal `greater_than_or_equal`

The inclusive numeric comparison, with the same finite-number conversion.

### Less than `less_than`

The strict numeric comparison, with the same finite-number conversion.

### Less than or equal `less_than_or_equal`

The inclusive numeric comparison, with the same finite-number conversion.

### Is empty `is_empty`

True for undefined, null, an empty string, or an empty array. It does not treat
an empty object, false, or zero as empty.

### Is not empty `is_not_empty`

Negates the Automation definition of empty.

### Exists `exists`

True when the left side is neither undefined nor null. An empty string exists.

### Does not exist `not_exists`

True only for undefined or null.

## Edges and outcomes

Edges may run `always`, on `success` or `failure`, on Condition `true` or
`false`, or for For-each `item` and `done`. Failure edges are explicit recovery
paths; a graph can also name a separate failure Automation. The compiler
rejects invalid references and graph arrangements before publication, but a
valid graph can still fail at run time when live data, a connection, or an
external provider is unavailable.