Two features, one idea: run code when something happens, without anybody polling. Event triggers fire on insert, update or delete. Scheduled triggers fire on a cron or at a one-off future time. Both call a webhook you own and retry it if it fails.
This is also the lesson with the most awkward v3 news in the track, so read to the end before designing anything around it.
Event triggers
A trigger watches a table and posts to your endpoint when a row changes:
{
"type": "create_event_trigger",
"args": {
"name": "booking_confirmed",
"table": {"schema": "public", "name": "bookings"},
"webhook_from_env": "BOOKING_WEBHOOK_URL",
"update": {"columns": ["status"]},
"retry_conf": {"num_retries": 5, "interval_sec": 10, "timeout_sec": 60}
}
}"update": {"columns": ["status"]} is doing real work. Without it the trigger fires on
every update to the table — including the ones your own handler causes, which is how
you build an infinite loop on your first afternoon.
The payload gives you both versions of the row:
{
"event": {
"op": "UPDATE",
"data": {
"old": {"status": "PENDING", "public_id": "..."},
"new": {"status": "CONFIRMED", "public_id": "..."}
},
"session_variables": {"x-hasura-role": "customer", "x-hasura-user-id": "..."}
},
"delivery_info": {"current_retry": 0, "max_retries": 5},
"trigger": {"name": "booking_confirmed"}
}old and new together are what let you react to a
transition rather than a state — “became confirmed”, not “is
confirmed”. Checking only new is the second classic bug.
What it guarantees
Delivery is at least once. Hasura writes the event to a table inside its own schema in the same transaction as your data change, then delivers it asynchronously and retries on failure.
Two things follow, and both matter more than the configuration above:
Your handler must be idempotent. Not “should be”. A timeout that succeeded server-side gets retried, so the same event arrives twice. Key off the event id, or make the operation naturally repeatable. A handler that charges a card or sends an email without a de-duplication key will eventually do it twice.
Ordering is not guaranteed. Two rapid updates can arrive out of order. If order matters, carry a version or timestamp in the row and ignore anything older than what you have already processed.
What you do get is durability: because the event is written in the same transaction as the data, an event cannot be lost by the engine restarting, and it cannot fire for a transaction that rolled back.
Inspecting failures
Failed events do not vanish. They sit in Hasura’s schema with their attempt history, visible in the console under the trigger’s Processed Events and Invocation Logs, and re-deliverable from there.
Worth knowing before an incident, because the first symptom is usually “the emails stopped going out” with nothing in your application logs at all — the failure is on Hasura’s side of the call.
Scheduled triggers
Same delivery machinery, different clock. A cron trigger runs on a schedule:
{
"type": "create_cron_trigger",
"args": {
"name": "expire_unpaid_bookings",
"webhook": "{{BOOKING_WEBHOOK_URL}}/expire",
"schedule": "*/15 * * * *",
"include_in_metadata": true,
"retry_conf": {"num_retries": 3}
}
}And a one-off scheduled event runs once at a time you choose — scheduled when the booking is made, to remind the guest the day before check-in. That is the more interesting half, and the part a plain cron in your infrastructure does not give you.
include_in_metadata: true puts the trigger in exported metadata so it is version
controlled. Set it false and the schedule exists only on that engine, which is a surprise for the
next person.
Triggers in v3 (DDN) — read this before you plan
On the v3 sections. Everything marked v3 (DDN) is taken from the official Hasura DDN documentation as read on 2026-08-21 and was not run locally — it shows configuration, never claimed output. The v2 material was executed against a running engine.
This is the sharpest discontinuity between the two products, and it is not advertised loudly.
Event triggers are marked work in progress. They are not gone from the roadmap, and they are not available now.
Cron triggers are not supported at all. No replacement, no equivalent object.
If you run reconciliation jobs, digest emails, nightly cleanups or reminder scheduling off Hasura’s scheduler today, that work has to move somewhere else before you can migrate. Do not plan a v3 migration on the assumption these will land in time.
What to do instead
Three options, in rough order of how much they change:
Emit events from the service that writes. If a write already goes through your own API — and per lesson 6 the interesting ones should — publish the event there rather than having the database tell you about it afterwards. This is the most portable answer and it works identically in v2 and v3.
Listen to Postgres directly. Logical replication with a change-data-capture tool gives you the same at-least-once stream Hasura was giving you, independent of the API layer.
Use your platform’s scheduler. For the cron half this is the honest answer: a Kubernetes CronJob, a cloud scheduler, or whatever your infrastructure already runs. You lose the one-off scheduled event convenience and have to build that yourself.
Worth saying plainly: if triggers are load-bearing in your v2 installation, they are a genuine reason to stay on v2 for now, and that is a legitimate decision rather than a failure to keep up.
Next
Part 5 starts with getting all of this from your laptop to production.