Guides

Triggers and watchers

All the ways a recipe can start by itself, and the rules that keep those runs safe when nobody is watching.

A trigger is something that starts a recipe by itself. A recipe declares which kind it uses. Pick the kind by asking what you are reacting to. It is not about the recipe. The same recipe can still be run by hand. And every declared trigger installs disarmed. That means switched off. It stays off until the owner arms it, which means switches it on.

Reacting to… Use
A record in the warehouse changed. Mail came in, an event moved, a task came due event_triggers
A kind of email came in. A parcel, a bill, a booking, an order, a lead event_triggers on mail facts
An outside system pushes to your server. Stripe, GitHub, Telegram webhook_triggers
Something you must keep checking. A time window, a page changing, "N days since…" auto_run + trigger_steps
Another recipe finished event_triggers on run outcomes
A calendar rhythm the user owns Nothing. The owner arms a schedule

Prefer events over polling whenever the source is already synced. Polling means checking again and again. An event subscription costs nothing between events. A poll costs a tick forever.

#Warehouse events

Subscribe with a path pattern. * matches one segment. ** matches several:

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

There is more than created / updated / deleted. Work items also send 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, and a few hot fields. Before you do real work, read the full record again with the matching read operation. Mail facts are an exception. Their event carries the values.

Gate out first-sync noise. An update event carries payload.prev. That is a snapshot of the hot fields from before the change. Backfills and first syncs do not carry it. Two gates you will use a lot:

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 one skips fires the system makes up itself, like a first sync. The second one skips updates that changed nothing.

#Shorthand forms and recipe events

There are short forms for common subscriptions:

  • CRM changes across vendors: "on": "deal.changed". You can narrow it by field and value. The record (payload.record) names its connection in connection_name, and its target_id is the id to read or update it by.
  • Messenger messages: "on": "message.received".
  • Accepted intake responses: "on": "form_response.accepted". See Reception.
  • Visitor requests: "on": "reception.request".
  • Mail facts: "on": "mail_fact.shipment", or "on": "mail_fact" for any kind of email. See Mail facts.

Browser-page watches use "on": "element.changed" with a URL and a selector. They work for your own local recipes. They cannot be published.

One more family of events comes from recipes themselves. run.<recipe_id>.**.completed and .failed fire when a recipe finishes. That chains one stage to the next without polling.

#Mail facts

Recued can read a kind of email into a few set values. A parcel, a bill, a booking, an order, a lead. Each reading is a mail fact. You teach Recued a kind of email with a template. A template says which emails to read, and how. You make one under Data → Received → Mail facts. Some kinds need no template. Recued reads shop markup and carrier tracking numbers by itself.

The facts about one parcel, order or booking make one thing. A recipe watches a thing's values, not the email itself:

json
"event_triggers": [
  { "on": "mail_fact.shipment", "fields": ["state"], "where": { "state": "delivered" } }
]

"on": "mail_fact.<kind>" watches one kind of email. Most recipes want this. "on": "mail_fact" watches any kind that has the values you name. Neither one takes a verb. Neither one takes a connection either, because a fact is read from every mailbox.

#Which mail fact changes wake a recipe

fields picks which changes wake the recipe:

fields Wakes on
Left out Each new thing, and each change to what it says
["state"] A change to that one value
The thing's identity, like carrier and tracking_number Each new thing, once. An identity never changes
["last_email_at"] Every email about the thing, even one that says nothing new

A new thing counts as a change to every value it has. So the example above also fires for a parcel whose first email already says delivered.

#Kinds of email

Kind Values state
purchase merchant, order_id, total, ordered_at ordered, paid, cancelled
shipment carrier, tracking_number, order_id, merchant, expected_at, delivered_at label_created, in_transit, out_for_delivery, delivered, exception, returned
return_refund merchant, order_id, return_id, amount return_requested, label_issued, received, refund_issued, refund_denied
reservation kind, provider, confirmation_code, starts_at, ends_at, location, party_size, total confirmed, cancelled
bill issuer, amount_due, due_at, invoice_number, period, account_ref, document issued, paid, overdue
statement issuer, account_ref, period_start, period_end, closing_balance, minimum_due, due_at, document available
subscription service, plan, amount, renews_at active, cancelled
pay_tax_document issuer, kind, period_or_year, document available
lead source, name, email, phone, reference new
order_received platform, order_id, buyer_name, buyer_email, total, ship_by new, paid, cancelled
owner_request tag, message_id new

#States and notices

Every value of the kind is always there. A value nothing read is null. A thing's state comes from its newest email that has one. So old mail read late never moves a parcel back from delivered.

Some kinds also have a notice. A notice is news about the thing, not a new state. A booking can have changed or reminder. A bill can have reminder. A subscription can have renewal, price_change, trial_ending or payment_failed. A notice never changes the state. And notice holds only the newest email's notice. So a payment after a reminder leaves none.

Money is an amount and a currency, like { "amount": "12.50", "currency": "USD" }. You can also make kinds of your own. A recipe that names one works only on the server where you made it.

#Filter a mail fact trigger

where narrows a mail fact trigger. Its rules are strict:

  • It takes the kind's values, plus complete and template. It cannot compare money, files, or a value that holds a time of day, like delivered_at. Watch those with fields instead. A plain date works.
  • Write each value the way the fact stores it. Never empty. One space between words. An id without a leading #, like 112-3345. A carrier by its short name, like UPS.
  • A {{config.*}} value is refused. The filter runs before the recipe does, and it has no settings then. Check a setting in a step's skip_when instead.
  • With "on": "mail_fact", a kind that lacks a value never matches it. A value nothing read, null, never matches either. So "where": { "notice": "price_change" } never wakes on a purchase. A purchase has no notice.
  • where.template names a template on your own server. It matches nothing anywhere else. So leave it out of a recipe you publish. Use template_variable instead. See A recipe that brings its own template.

The Kitchen checks these rules as you type, and says what to fix.

#A recipe that brings its own template

A recipe you publish can bring the template you built it with. Give the recipe a setting of type mail_template. Put your template in its starter. Then name that setting in the trigger, like "template_variable": "template". The trigger then wakes only on facts that this template read.

  • Installing the recipe makes the template, with its AI off. The setting holds the new template.
  • The owner can pick another template in the recipe's settings. The trigger follows the pick. Until the setting holds a template, the trigger does nothing.
  • Each dish has its own settings. So two dishes can read two templates.
  • The owner cannot change a starter's rules. Duplicate to edit makes their own copy. An update to your recipe brings your new rules.
  • Uninstalling removes the template, unless another recipe's setting holds it.

The checks are strict here too. A trigger takes template_variable or where.template, never both. The setting takes no default. A starter must use a kind of email built into Recued, the same kind the trigger watches. It must hold nothing from your own mail, like an email address, a phone number, or a long number.

In a run, {{context.event.payload.record.template}} is the template that read the fact.

#What a mail fact event carries

Most events only point at a record. A mail fact event carries the thing itself:

Field What it holds
record_id The thing's id. core.mail.fact.get reads it with all its facts
record Every value of the kind, plus complete, missing, template, last_email_at and passes
record.passes Who filled each value: standard, rule or ai
record.fact The fact that caused the change. It points at its email, and holds any extra data the template read
prev The thing before the change. A new thing has none
changed_fields What changed

The template decides whether AI fills in values. The recipe does not. Must your recipe never act on an AI value? Then check its pass in skip_when, like "{{context.event.payload.record.passes.tracking_number}} equal ai".

#When a mail fact starts a recipe

  • Only news starts a recipe. News is new mail, after the mailbox's first sync. Mail found again on a restart starts nothing. Each fact starts recipes once.
  • So never use the prev is_null gate on a mail fact trigger. A new thing has no prev, and it is real news. The gate would drop every new parcel.
  • Reading past mail starts recipes only when you ask it to. Those runs are marked as past-mail runs (run_mode: backfill).
  • Each email's event arrives on its own. Events are not merged.
  • A fact lasts only as long as its email. Keep what must last longer. Make a booking from a reservation, or a commitment from a bill.
  • A thing's later emails wake the recipe again. So tie what the recipe makes to the thing, and look for it before making another. Tie it to the thing's identity (record.identity_keys), not only its id. An email that is removed and read again makes a new thing. It has a new id and the same identity, and it starts the recipe again.
  • A recipe a fact starts runs without you, so each write it makes waits for your approval. Look before writing, and write only to change something. Then a run with nothing to do ends without asking you.

#Inbound webhooks

A webhook is a message another system sends to your server when something happens. For systems that push like this, add a webhook_triggers entry. It names a verification profile: Stripe, GitHub, Telegram, Slack, Paddle, Lemon Squeezy, or a generic token / HMAC / basic-auth mechanism. The endpoint checks the signature before your recipe ever fires. Then the recipe reads the accepted event body.

Do not check the signature again inside the recipe. That already happened at the door. Do treat the body as untrusted data. Gate on the fields you need. Skip when they are missing. Webhooks POST straight to your server. Nothing is passed through anyone's cloud.

#Self-ticking watchers

When there is no event source, a recipe can tick by itself. It gates 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. If not, the tick passes quietly.

#Gating a tick without an extra call

There are built-in watch operations. There are also these transforms. They 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 and weekdays? now, weekdays?, start_hour?, end_hour?
time_elapsed_since Has window_ms passed since since_ms?
mail_received Did any mail arrive that matches 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 an old and a new attendee list?
http_changed Did an ETag or content hash change since last time?
recipe_succeeded_since Has a given recipe already succeeded since since_ms? Use it to avoid doing work twice

Each one returns the decision plus what it matched. So the same step gates the tick and hands the detail to the steps after it.

Pick the slowest interval the job can live with. For a daily job, the normal choice is an hourly tick gated by a morning window. And stop your own repeats. A watcher may see the same thing on two ticks in a row. So it must stamp what it acted on. Use an annotation on the record, or a marker row. Do not trust the interval alone.

#Configuration on automated fires

A run with nobody watching works out its settings in layers. First, the recipe's variable defaults. Then the settings saved at install. Then the schedule's or trigger's own settings. Each armed row can carry its own. Write your variables so a run on pure defaults either works or stops and asks for setup. It must never half-run.

#Earn each stage of work

Order your gates from cheap to costly. Put fixed checks before AI. Stop safely at every step:

  1. Identity. Is this event about the thing you watch? Cannot tell? Skip.
  2. State and window. Is the watch still on? Is the time window open?
  3. Cheap fields. Subject words, status, thresholds. Check these before any body read or vendor call.
  4. The costly read. Only after the cheap gates pass.
  5. A purpose check on what you fetched. Fixed rules, from words the user set up. Not sure? Record it for review and notify. Do not go on.
  6. AI last. Only to summarize or classify. Only after privacy protection. Never as the thing that decides whether an action happens.

#Keep outward actions out of unattended runs

Some actions reach outside or are hard to undo. Sending mail. Posting messages. Making events with other people on them. The strong pattern for these is a split:

  • The unattended recipe detects. It reads, works things out, writes a proposal, and notifies you.
  • A second recipe acts. You run it yourself. From the notification, or from a button on a review digest. Running it is the decision. 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 write with a wide grant happens with no one looking. With the detect-then-act split, background work only piles up proposals. You review them with context. You do not get asks, or surprises.

#Checklist

  • Events over polling when the source is synced. Read the payload's record again before use. Use the prev is_null gate on update subscriptions, but never on a mail fact trigger.
  • Mail facts: one kind in on, unless any kind will do. where values written as the fact stores them. No template id in a recipe you publish. Bring a starter and use template_variable instead.
  • Webhooks: the profile checks the signature at the door. Steps gate on body fields.
  • Watchers: the slowest interval you can live with. Repeats stopped with a stamp.
  • Works on defaults, or stops and asks for setup.
  • Gates ordered cheap → costly → AI. AI never gates an action.
  • Nothing that reaches outside in the unattended path. Detect, propose, notify. Act in a recipe the owner runs.
  • The description says what arms it, what fires it, and what it will never do.

Recued Docs

Search documentation

Start typing to search the documentation.