SyndrDB supports BEFORE and AFTER triggers that fire automatically in response to INSERT, UPDATE, and DELETE operations. Triggers execute user-defined SyndrQL commands, enabling validation, audit logging, cascading updates, and other automated behaviors.
CREATE TRIGGER "trigger_name" ON BUNDLE "bundle_name"
BEFORE|AFTER INSERT[, UPDATE[, DELETE]]
FOR EACH DOCUMENT
[WHEN (condition)]
[PRIORITY n]
EXECUTE (
trigger_body
)
CREATE TRIGGER "set_updated_at" ON BUNDLE "users"
BEFORE UPDATE
FOR EACH DOCUMENT
EXECUTE (
SET NEW "updated_at" = F:NOW()
)
| Timing | When It Fires | Can Modify Data? | Can Abort? | Error Behavior |
|---|---|---|---|---|
| BEFORE | Before the operation is applied | Yes (via SET NEW) |
Yes (return error) | Aborts the operation |
| AFTER | After the operation has been committed | No | No | Logged but non-fatal |
Triggers can fire on one or more DML events. Events can be combined:
-- Single event
BEFORE INSERT
-- Multiple events
BEFORE INSERT, UPDATE
-- All three events
AFTER INSERT, UPDATE, DELETE
| Event | OLD Available? | NEW Available? | Description |
|---|---|---|---|
| INSERT | No | Yes | Fires when a new document is added |
| UPDATE | Yes | Yes | Fires when an existing document is modified |
| DELETE | Yes | No | Fires when a document is removed |
Within a trigger body, you can reference the document before and after the operation using qualified identifiers:
OLD."field_name" — The field value before the operation (available for UPDATE and DELETE)NEW."field_name" — The field value after the operation (available for INSERT and UPDATE)-- Use OLD/NEW in conditions
WHEN (NEW."status" != OLD."status")
-- Use OLD/NEW in the trigger body
ADD DOCUMENT TO BUNDLE "audit_log" WITH ({
"old_value" = OLD."email",
"new_value" = NEW."email"
})
In a BEFORE trigger, you can modify the document that will be written using SET NEW:
-- Set a field on the new document
SET NEW "updated_at" = F:NOW()
SET NEW "status" = "processed"
Important:
SET NEWis only allowed in BEFORE triggers. Using it in an AFTER trigger will produce a validation error.
The optional WHEN clause adds a condition that must be true for the trigger to fire. If the condition evaluates to false, the trigger is skipped for that document.
-- Only fire when status changes
CREATE TRIGGER "status_change_log" ON BUNDLE "orders"
AFTER UPDATE
FOR EACH DOCUMENT
WHEN (NEW."status" != OLD."status")
EXECUTE (
ADD DOCUMENT TO BUNDLE "status_history" WITH ({
"order_id" = NEW."order_id",
"old_status" = OLD."status",
"new_status" = NEW."status",
"changed_at" = F:NOW()
})
)
When multiple triggers are defined for the same event on the same bundle, the PRIORITY value determines the execution order. Lower numbers fire first.
-- This fires first (priority 100)
CREATE TRIGGER "validate_email" ON BUNDLE "users"
BEFORE INSERT
FOR EACH DOCUMENT
PRIORITY 100
EXECUTE (...)
-- This fires second (priority 200)
CREATE TRIGGER "set_defaults" ON BUNDLE "users"
BEFORE INSERT
FOR EACH DOCUMENT
PRIORITY 200
EXECUTE (...)
The default priority is 1000.
Removes a trigger from a bundle.
DROP TRIGGER "trigger_name" ON BUNDLE "bundle_name"
DROP TRIGGER "set_updated_at" ON BUNDLE "users"
Temporarily enable or disable a trigger without dropping it.
-- Disable a trigger
DISABLE TRIGGER "set_updated_at" ON BUNDLE "users"
-- Re-enable it
ENABLE TRIGGER "set_updated_at" ON BUNDLE "users"
Disabled triggers remain in the bundle metadata but do not fire. This is useful for bulk data loads or maintenance operations where trigger overhead is not desired.
The trigger body is executed within the same transaction context as the triggering operation. The following restrictions apply:
A trigger can cause another trigger to fire (cascading). SyndrDB limits cascade depth to prevent infinite loops.
| Setting | Default | Description |
|---|---|---|
TriggerMaxDepth |
16 | Maximum cascade depth before aborting |
TriggerAllowRecursion |
false | Whether a trigger can fire itself (direct recursion) |
If the maximum depth is exceeded, the operation is aborted with a recursion error.
| Setting | Default | Description |
|---|---|---|
TriggersEnabled |
true | Global toggle to enable/disable all triggers |
TriggerMaxDepth |
16 | Maximum cascade depth |
TriggerMaxPerBundle |
100 | Maximum number of triggers per bundle |
TriggerTimeoutMs |
5000 | Maximum execution time per trigger in milliseconds |
TriggerAllowRecursion |
false | Allow a trigger to invoke itself |
-- Log all changes to the users bundle
CREATE TRIGGER "audit_user_changes" ON BUNDLE "users"
AFTER UPDATE
FOR EACH DOCUMENT
EXECUTE (
ADD DOCUMENT TO BUNDLE "audit_log" WITH ({
"bundle" = "users",
"action" = "UPDATE",
"old_email" = OLD."email",
"new_email" = NEW."email",
"changed_at" = F:NOW()
})
)
-- Ensure price is never negative
CREATE TRIGGER "validate_price" ON BUNDLE "products"
BEFORE INSERT, UPDATE
FOR EACH DOCUMENT
WHEN (NEW."price" < 0)
PRIORITY 100
EXECUTE (
SET NEW "price" = 0
)
-- Automatically set created_at on INSERT
CREATE TRIGGER "set_created_at" ON BUNDLE "orders"
BEFORE INSERT
FOR EACH DOCUMENT
PRIORITY 100
EXECUTE (
SET NEW "created_at" = F:NOW()
)
-- Automatically set updated_at on UPDATE
CREATE TRIGGER "set_updated_at" ON BUNDLE "orders"
BEFORE UPDATE
FOR EACH DOCUMENT
PRIORITY 100
EXECUTE (
SET NEW "updated_at" = F:NOW()
)
-- When a user is deactivated, cancel their pending orders
CREATE TRIGGER "deactivate_user_orders" ON BUNDLE "users"
AFTER UPDATE
FOR EACH DOCUMENT
WHEN (OLD."active" == TRUE AND NEW."active" == FALSE)
EXECUTE (
UPDATE DOCUMENTS IN BUNDLE "orders" (status = "cancelled") CONFIRMED
WHERE customer_id == NEW."user_id" AND status == "pending"
)
-- Log all deletions
CREATE TRIGGER "log_deletions" ON BUNDLE "documents"
AFTER DELETE
FOR EACH DOCUMENT
EXECUTE (
ADD DOCUMENT TO BUNDLE "deletion_log" WITH ({
"deleted_id" = OLD."doc_id",
"deleted_title" = OLD."title",
"deleted_at" = F:NOW()
})
)
| Command | Description |
|---|---|
CREATE TRIGGER "name" ON BUNDLE "b" ... |
Create a new trigger |
DROP TRIGGER "name" ON BUNDLE "b" |
Remove a trigger |
ENABLE TRIGGER "name" ON BUNDLE "b" |
Re-enable a disabled trigger |
DISABLE TRIGGER "name" ON BUNDLE "b" |
Temporarily disable a trigger |
SET NEW "field" = value |
Modify the incoming document (BEFORE only) |
OLD."field" |
Reference pre-operation field value |
NEW."field" |
Reference post-operation field value |
Last updated: March 2026