Triggers
Triggers fire automatically when data changes in Rekor. A trigger's action is one of three kinds:
- Webhook — Rekor sends a signed HTTP POST to a configured URL with the change details.
- Internal write — Rekor updates a referenced record for you, with no external service. On a matching change it locates a target record and applies a guarded patch, with an optional compare-and-set
preconditionso the update only lands when the target is still in the expected state. Ideal for "when A changes, update related B" — e.g. booking an appointment flips its slot to busy only if it was free. - External write — Rekor writes the changed record out to an external system by reusing an external source's write path (its field translation, endpoint, and authentication), with no executor. Use it to keep a Rekor-native record_type mirrored to a legacy API: store records natively and have each change pushed upstream in the upstream's own shape. On a create, the upstream-assigned id is linked back onto the record so the native record points at its external counterpart.
How It Works
- Create a trigger — specify the record_type, events to watch, and an action (a webhook URL, an internal write, or an external write).
- Data changes happen — agents or inbound webhooks create, update, or delete records.
- Rekor fires the trigger — sends the signed HTTP POST, applies the guarded internal write itself, or writes the change out through the external source.
Creating a Trigger
rekor triggers create --base my-base \
--name "Notify Slack on P1" \
--record_type-scope incidents \
--url https://hooks.slack.com/services/T00/B00/xxx \
--secret <hmac-secret> \
--events record.created,record.updated
Internal Writes
Instead of a webhook URL, pass --action to have Rekor update a referenced record itself — no external service. The action names the target record_type, how to find the target from the changed record (match.source_field), the patch to apply, and an optional precondition (a guard so the write only lands when the target is still in the expected state). Internal writes fire on record.created / record.updated only.
rekor triggers create --base my-base \
--name "Book slot" \
--record_type-scope appointments \
--events record.created \
--action '{"type":"internal_write","target":"slots","match":{"source_field":"data.slot_id"},"patch":{"status":"busy"},"precondition":{"field":"data.status","op":"eq","value":"free"},"mode":"async"}'
A trigger does not fire on writes that were themselves produced by an internal write (the skip_cascade_writes loop guard, on by default), so reactions can't loop back on themselves.
External Writes
Pass an external_write action to mirror a change out through an external source — Rekor reuses that source's field translation, write endpoint, and authentication, so the same contract that proxies reads and writes also keeps a native record_type in sync upstream. No executor is needed for a plain HTTP (JSON or form, token/header auth) API. The action names the record_type that defines the source, the source name, and the operation; record_type defaults to the changed record's record_type (a native mirror points at a sibling source-backed record_type), and the operation defaults from the event. On a create, the upstream-assigned id is written back as the record's external id, linking the native record to its upstream counterpart and making retries safe. By default every matching update is pushed upstream; set watched_fields to scope update dispatch to writes that actually changed an upstream-mapped field — "mapped" watches exactly the fields the source maps upstream (so a write touching only a Rekor-owned field skips the redundant no-op upstream push), or pass an explicit list of field names. Creates and deletes always dispatch. Delivery is reliable — signed, retried with backoff, and dead-lettered after repeated failure, just like a webhook. External writes fire on record.created / record.updated / record.deleted.
rekor triggers create --base my-base \
--name "Mirror appointments upstream" \
--record_type-scope appointments \
--events record.created,record.updated \
--action '{"type":"external_write","record_type":"upstream_appointments","source":"legacy","op":"create"}'
Referencing a source from a trigger is a read-only reuse of its translation contract — it never turns the changed record's record_type into a proxy; that record_type stays native. The referenced source, operation, and write endpoint are validated when the trigger is created and again at promotion.
Managing Triggers
# List all triggers
rekor triggers list --base my-base
# Get a specific trigger
rekor triggers get {trigger_id} --base my-base
# Delete a trigger
rekor triggers delete {trigger_id} --base my-base
# Inspect delivery attempts (status, retries, errors)
rekor triggers deliveries --base my-base
rekor triggers deliveries --status dead --base my-base
Events
Triggers can listen for any combination of these events (pass them comma-separated to --events):
record.created— a new record is insertedrecord.updated— an existing record is modifiedrecord.deleted— a record is soft-deletedrecord.cancelled— a record is cancelledrelationship.created/relationship.updated/relationship.deleted— a relationship changesfile.created/file.updated/file.deleted— a file is written (webhook actions only)
Filtering
Add --filter to fire a trigger only when the changed record matches a condition — the same filter language queries use, evaluated against the record (or relationship) being written. Without a filter, the trigger fires on every matching event.
rekor triggers create --base my-base \
--name "Notify on paid invoices" \
--record_type-scope invoices \
--url https://example.com/webhook \
--secret <hmac-secret> \
--events record.created,record.updated \
--filter '{"field":"data.status","op":"eq","value":"paid"}'
Combine conditions with and / or groups. Filters address the record's own fields (data.*, id, version); narrow by record_type with --record_type-scope and by event with --events. A malformed filter is rejected when the trigger is created.
HMAC Signatures
Every trigger request includes an X-Rekor-Signature header (format v1,<hex>) — an HMAC signature the receiving system verifies to confirm the request came from Rekor, plus an X-Rekor-Timestamp (unix seconds) it checks for freshness to block replays. The signature binds the id, timestamp, method, path, and body — the same scheme proxied requests use, so one verifier handles both. Each trigger signs with its own HMAC secret, supplied via --secret when the trigger is created (and regenerated on promotion). Requests also carry X-Rekor-Id / X-Rekor-Delivery-Id so receivers can dedupe retries of the same event.
Don't hand-roll this verification — the receiving service is an executor, and the rekor-sdk package verifies the signature, checks the timestamp, and dedupes retries for you. See Building an Executor.
Reliable Delivery
Triggers fire asynchronously and never block the write response. Delivery is reliable: if a destination is unreachable or returns an error, Rekor automatically retries with increasing backoff, and stops after repeated failures (the delivery is marked dead). Because a delivery can be retried, the same event may arrive more than once — use the X-Rekor-Delivery-Id header to dedupe.
Inspect delivery status, attempt counts, and the last error with rekor triggers deliveries — filter by --status (pending, delivered, failed, dead) or --trigger-id.
Preventing Loops
By default, triggers have skip_inbound_webhook_writes: true. This means writes that originated from an inbound webhook won't fire triggers, preventing infinite webhook loops (an inbound webhook creates a record → trigger fires → external system calls the inbound webhook → ...).
Environment Restrictions
Triggers can only be created or deleted in preview bases. To add triggers to production, create them in a preview base and promote. Secrets are regenerated on the production side during promotion — preview secrets are never copied.