Guides

Triggers and watchers

Every way a recipe fires without you pressing Run — warehouse events, inbound webhooks, self-ticking watchers — and the discipline that keeps unattended runs safe.

A recipe fires automatically through one of a few declared surfaces. Which one to use is a property of what you are reacting to, not of the recipe — the same recipe can still be run by hand, and every declared trigger installs disarmed until the owner arms it.

Reacting to… Use
A warehouse record changing — mail arrived, an event moved, a task came due event_triggers
An external system pushing to your server — Stripe, GitHub, Telegram webhook_triggers
A condition you must poll — a time window, a page changing, "N days since…" auto_run + trigger_steps
Another recipe finishing event_triggers on run outcomes
A calendar cadence the user owns Nothing — the owner arms a schedule

Prefer events over polling whenever the source is already synced: an event subscription costs nothing between events; a poll costs a tick forever.

#Warehouse events

Subscribe with a path pattern — * spans one segment, ** several:

json
"event_triggers": [
  { "event": "data.mail.**.created" },
  { "event": "data.calendar.**.updated" }
]

Beyond created / updated / deleted, work items emit lifecycle events — due_soon, overdue, state_changed — so a deadline radar is a subscription, not a poll.

The payload is an address, not the record. It carries routing fields (which record, a few hot fields); re-read the full record with the matching read operation before doing real work.

Gate out first-sync noise. Update events carry payload.prev, a snapshot of the prior hot fields; backfills and first syncs don't. Two standing gates:

text
"skip_when": "{{context.event.payload.prev}} is_null"
"skip_when": "{{context.event.payload.prev.start_at}} equal {{context.event.payload.start_at}}"

The first skips synthesized fires, the second skips no-op updates.

Shorthand forms exist for common subscriptions: cross-vendor CRM changes ("on": "deal.changed" with optional field and value narrowing), messenger messages ("on": "message.received"), accepted intake responses ("on": "form_response.accepted" — see Reception), and visitor requests ("on": "reception.request"). Browser-page watches ("on": "element.changed" with a URL and selector) work for your own local recipes but cannot be published.

One more event family: recipes themselves. run.<recipe_id>.**.completed and .failed fire when a recipe finishes, which chains stages without polling.

#Inbound webhooks

For systems that push, declare a webhook_triggers entry naming a verification profile — Stripe, GitHub, Telegram, Slack, Paddle, Lemon Squeezy, or a generic token / HMAC / basic-auth mechanism. The endpoint verifies the signature before your recipe ever fires; the recipe then reads the accepted event body.

Don't re-verify signatures inside the recipe — that happened at the boundary. Do treat the body as untrusted data: gate on the fields you need and skip when they're absent. Webhooks POST straight to your server; nothing is relayed through anyone's cloud.

#Self-ticking watchers

When there is no event source, a recipe can tick itself and gate each tick:

json
"auto_run": { "interval_ms": 3600000 },
"trigger_steps": [
  { "id": "window", "op": "core.watch.time",
    "args": { "weekdays": [1, 2, 3, 4, 5], "start_hour": 7, "end_hour": 19 } }
]

Every trigger step must return should_run: true or the tick passes silently.

Alongside the built-in watch operations, these transforms answer "has this changed?" from data you already fetched, so a gate costs no extra call:

Transform Answers
time_within_window Is now inside the allowed hours/weekdays? now, weekdays?, start_hour?, end_hour?
time_elapsed_since Has window_ms passed since since_ms?
mail_received Did any mail arrive matching from / subject / label?
file_changed Did any file under path_prefix change since since_ms?
calendar_new_since Were events created since since_ms?
calendar_changed_since Were events edited since since_ms?
calendar_starting_soon Does an event start within minutes_ahead?
attendee_diff Who joined or left between a prior and current attendee list?
http_changed Did an ETag or content hash move since last time?
recipe_succeeded_since Has a given recipe already succeeded since since_ms? Use it to avoid re-doing work

Each returns the decision plus what it matched, so the same step both gates the tick and hands the detail to the steps after it.

Pick the slowest interval the job tolerates — an hourly tick gated by a morning window is the norm for a daily job. And de-duplicate your own fires: a watcher that can see the same condition on consecutive ticks must stamp what it acted on (an annotation on the record, a marker row) rather than trusting the interval.

#Configuration on automated fires

An unattended fire resolves configuration in layers: the recipe's variable defaults, then the install's saved configuration, then the schedule's or trigger's own configuration (each armed row can carry one). Author variables so a fire on pure defaults either works or fails closed asking for setup — never half-runs.

#Earn each stage of work

Order gates cheap to expensive, deterministic before AI, and fail closed at every rung:

  1. Identity — is this event about the thing you watch? Can't establish it → skip.
  2. State and window — is the watch still active, the time window open?
  3. Cheap fields — subject terms, status, thresholds — before any body read or vendor call.
  4. The expensive read — only after the cheap gates pass.
  5. A purpose check on the fetched content — deterministic, from configured terms. Ambiguous → record it for review and notify, don't proceed.
  6. AI last, for synthesis or classification only — after privacy protection, and never as the thing that decides whether an action happens.

#Keep outward actions out of unattended runs

The robust pattern for anything outward-facing or hard to reverse — sending mail, posting messages, creating events with attendees — is a split:

  • the unattended recipe detects: it reads, computes, writes a proposal, and notifies you;
  • a second recipe acts, and you run it yourself — from the notification, or from a button on a review digest. Running it is the decision, and its writes still pass grants and approvals like any other run.

A background run has nobody at the keyboard: at best an outward action waits as a held approval; at worst a broadly-granted write happens with no one looking. The detect/act split means background work only ever accumulates proposals you review with context, instead of asks — or surprises.

#Checklist

  • Events over polling when the source is synced; payload re-read before use; prev is_null gate on update subscriptions.
  • Webhooks: verification by profile at the boundary; body fields gated in steps.
  • Watchers: slowest tolerable interval; fires de-duplicated with a stamp.
  • Works on defaults or fails closed asking for configuration.
  • Gates ordered cheap → expensive → AI; AI never gates an action.
  • Nothing outward-facing in the unattended path — detect, propose, notify; act in a recipe the owner runs.
  • The description says what arms, what fires it, and what it will never do.

Recued is local first; your server remains the authority.

Recued Docs

Search documentation

Start typing to search the documentation.