Running logic after creates, updates, and deletes with DEFINE EVENT.
Events let the database react to record changes: write an audit log, normalise related rows, or enqueue follow-up work. They run after the record change but inside the same transaction (unless you opt into async), and they see $before and $after snapshots of the record.
Note
Events are a side effect of normal writes. They are not fired during a bulk import.
Key ideas
$event: "CREATE", "UPDATE", or "DELETE".
$before / $after: state immediately before and after the change.
$value: the record as seen by the event (after create/update, before delete).
WHEN: optional filter so the body runs only when something meaningful changed.
THEN: the SurrealQL block to execute.
You need the usual database privileges and USE scope. Full syntax and async options are in DEFINE EVENT; Reactive patterns covers ASYNC, RETRY, and MAXDEPTH.
Example usage
Email Change Detection: Create an event that logs whenever a user's email is updated.
In this example:
The WHEN clause checks if the email has changed.
The THEN clause records this change in a log table.
-- Create a new event whenever a user changes their email address -- One-statement event DEFINEEVENTOVERWRITEtestONTABLEuserWHEN$before.email!=$after.emailTHEN ( CREATElogSET user=$value.id, // Turn events like "CREATE" into string "email created" action='email'+' '+$event.lowercase() +'d', // `email` field may be NONE, log as '' if so old_email=$before.email??'', new_email=$after.email??'', at=time::now() ); UPSERTuser:testSETemail='old_email@test.com'; UPSERTuser:testSETemail='new_email@test.com'; DELETEuser:test; SELECT * FROMlogORDERBYatASC;
You can trigger events based on specific events. You can use the variable $event to detect what type of event is triggered on the table.
-- UPDATE event -- Here we are creating a notification when a user is updated. DEFINEEVENTuser_updatedONTABLEuser WHEN$event="UPDATE" THEN ( CREATEnotificationSETmessage="User updated", user_id=$after.id, created_at=time::now() );
-- DELETE event is triggered when a record is deleted from the table. -- Here we are creating a notification when a user is deleted. DEFINEEVENTuser_deletedONTABLEuser WHEN$event="DELETE" THEN ( CREATEnotificationSETmessage="User deleted", user_id=$before.id, created_at=time::now() );
-- You can combine multiple events based on your use cases. -- Here we are creating a log when a user is created, updated or deleted. DEFINEEVENTuser_eventONTABLEuser WHEN$event="CREATE"OR$event="UPDATE"OR$event="DELETE" THEN ( CREATElogSET table="user", event=$event, happened_at=time::now() );
This longer example shows an event that updates all posts for a publication to "published" status once a publication containing them is created.
-- Define an event DEFINEFIELDstatusONpostTYPE"submitted" | "published"DEFAULT"submitted"; DEFINEEVENTpublish_postONTABLEpublication WHEN$event="CREATE" THEN ( FOR$postIN$after.posts { UPDATE$postSETstatus="published"; } );
CREATEpost:oneSETcontent="I read the news today, oh boy..."; CREATEpost:twoSETcontent="On the banks of Tuonela Bleach the skeletons of kings"; CREATEpost:threeSETcontent="뭐 화끈한 일 뭐 신나는 일 없을까"; CREATEpublicationSETposts=[post:one, post:two, post:three];
SELECT * FROMpost;
Output
[ { content: 'I read the news today, oh boy...', id: post:one, status: 'submitted' }, { content: '뭐 화끈한 일 뭐 신나는 일 없을까', id: post:three, status: 'submitted' }, { content: 'On the banks of Tuonela Bleach the skeletons of kings', id: post:two, status: 'submitted' } ]
Events and permissions
Queries inside the event always execute without any permission checks, even when triggered by changes made by the currently authenticated user.
Consider a CREATE query sent by a record user that has CREATE access to the comment table only:
Logic can be added to the event itself to modify the behaviour depending on a user's permissions or any other condition.
DEFINEEVENTon_comment_createdONTABLEcomment WHEN$event="CREATE" THEN{ -- Check if the post allows for adding comments. -- User record doesn't have access to the `post` table. IF$after.post.disable_comments{ THROW"Can't create a comment - Comments are disabled for this post"; };
-- Set the `approved` field on the new comment - automatically approve -- comments made by the author of the post. -- For security reasons, record users don't have any permissions for the `approved` field. UPDATE$after.idSET approved=$after.post.author==$after.author; };
Accessing $input in events
The behaviour of events can be further refined via the $input parameter, which represents the record in question for the event.
-- Set CREATE in event to only trigger when record has `true` for `log_event` DEFINEEVENTsomethingONpersonWHEN$input.log_event=trueTHEN{ CREATElogSETat=time::now(), of=$input; };
-- Set to `false`, does not trigger CREATE CREATEperson:debugSETname="Billy", log_event=false; -- Triggers CREATE CREATEperson:realSETname="Bobby", log_event=true;
Events in SurrealDB are executed synchronously within the same transaction that triggers them. While this ensures consistency, it can lead to increased latency for write operations if the event logic is complex or resource intensive.
To allow events to execute independently of the transaction that triggers them, the ASYNC clause can be used.
How async events are processed
Async events are processed in an interval dependant on the environment variable SURREAL_ASYNC_EVENT_PROCESSING_INTERVAL (or --async-event-interval when starting the server) which is set to 5 seconds as the default. Lowering this will reduce the latency between a document change and its events, while leading to more frequent polls by the background worker.
Some more notes on the characteristics of async events:
Atomicity: The event is enqueued within the same transaction as the document change. If the transaction fails, the event is never queued.
Consistency: Asynchronous events run in a separate transaction from the original change. They see the database state at the time they are executed.
Ordering: Events are generally processed in the order they were created, though parallel processing may occur within a single batch.
The easiest way to demonstrate that async events do not occur in the same transaction is by causing one to throw an error. As an error inside any part of a transaction will cause the transaction to fail and roll back, the following event which fails about 50% of the time would cause the CREATE statement that follows to fail if it were not async. As an async event, however, the events that follow the statement are each run in their own transaction
DEFINETABLEdid_not_throw;
DEFINEEVENTmay_throwONperson ASYNC THEN{ IFrand::bool() { THROW"This message will never show"; }ELSE{ CREATEdid_not_throw; } };
While the MAXDEPTH in this case is only one greater than the default, the sheer number of records created results in the final count() score being 20503, compared to 2278 if the default is used.
This is somewhat similar to recursive queries which can also quickly add up.
-- Create five people CREATE |person:1..=5|; -- Make each person friends with each of the four others UPDATEpersonSETfriends= (SELECTVALUEidFROMperson).complement([$this.id]); -- Count after five levels of depth is already 1024! count(person:1.{..5}.friends);
The RETRY clause
The RETRY clause is suitable for events that may fail but can succeed on successive attempts.
The example below shows two events that each have a 50% chance of failure and zero retries.