Skip to content

Stop choosing. SurrealDB launch week, 12-16 October

See what's coming
Course content preview

13: Events and CI

DEFINE EVENT handles the transitions that field clauses cannot reach: rules across two fields on the same record, reactions to a status change, side effects on a different table. This chapter adds a few events to the planning schema.

The second half turns to something SurrealKit has been quietly setting up for you since init: tests. Once the catalog does real work (COMPUTED roll-ups, UNIQUE indexes, EVENT hooks), a pull request can break those behaviours without anyone noticing in CI. surrealkit test is how you catch that before merge. We will write one small suite after the events are in place.

LayerExamples
DEFAULT, VALUE, ASSERT, COMPUTEDRules that only need one field, or the record it lives on
DEFINE EVENT"When X changes to Y, do Z" across the whole write


Part 5 already used an order-paid style event. Planning schemas need the same kind of thing.

Add database/schema/events_activity.surql:

DEFINE EVENT activity_end_after_start ON activity
    WHEN $event IN ["CREATE", "UPDATE"]
        AND $after.end < $after.start
    THEN {
        THROW "Activity end must be on or after start";
    };


You could try this as an ASSERT ($after.end > $this.start), and it would mostly work on update, but it gets awkward on create, before $this is fully wired up. An EVENT sees $before and $after for both CREATE and UPDATE, so it does not run into that problem.

If you try it out, you will not see this event's message yet. The COMPUTED duration field from part 8 is evaluated during the write as well, and end - start fails first when end is earlier than start:

UPDATE activity:concrete SET end = start - 1d;


Output
'Failed to compute: "2026-12-29T03:37:34.173765Z - 2026-12-30T03:37:34.173765Z", as the operation results in a negative value.'


The bad write is rejected either way, but the error says nothing about activities. To let the event give the clearer message, make duration handle the case itself:

DEFINE FIELD duration ON activity COMPUTED IF end >= start { end - start };
DEFINE EVENT no_self_dependency ON depends_on
    WHEN $event = "CREATE" AND $after.in = $after.out
    THEN {
        THROW "An activity cannot depend on itself";
    };


This only catches the one-step case. Longer cycles (A depends on B, which depends on A) usually need application logic or a batch job to find. Treat EVENT hooks as cheap invariants on the hot path, not a general-purpose validator.

When an activity's progress reaches completion, you might want to notify something downstream. In a teaching schema, an audit record is a reasonable stand-in:

DEFINE TABLE activity_audit SCHEMAFULL;
DEFINE FIELD activity ON activity_audit TYPE record<activity>;
DEFINE FIELD note ON activity_audit TYPE string;
DEFINE FIELD at ON activity_audit TYPE datetime DEFAULT time::now();

DEFINE EVENT log_activity_complete ON activity
    WHEN $event = "UPDATE"
        AND $before.progress < 1.0
        AND $after.progress >= 1.0
    THEN {
        CREATE activity_audit SET
            activity = $after.id,
            note = "Activity marked complete";
    };


EVENT bodies run as part of the statement that triggered them: the activity_audit record exists as soon as the UPDATE returns, and a THROW inside an event (as in the two events above) rejects the whole write. DEFINE EVENT … ASYNC is the form that runs later, outside the triggering statement, and that is where you need to design for retries and make failures visible.

DEFINE EVENT belongs next to the tables it touches, or in a shared database/schema/events.surql if you would rather group them. Either way, SurrealKit applies events on sync or rollout start like any other definition. Removing one is destructive, so that waits for rollout complete, once nothing depends on it any more.

Schema in git is only half the story. You also want a machine to prove that the catalog still behaves the way the course (and your app) expect, such as:

  • Does milestone.progress still average the linked activities?

  • Does a bad date still get rejected by the event you just added?

  • Did someone remove a UNIQUE index by accident?

surrealkit test answers those questions against a throwaway database: apply the schema, run suite files under database/tests/suites/, fail if a case breaks. It is the CI twin of local sync - fine to run often, never a substitute for rollouts on shared data.

Init already left database/tests/suites/smoke.toml. Add a second suite beside it at database/tests/suites/milestone_rollups.toml:

name = "milestone_rollups"
tags = ["computed"]

[[cases]]
name = "mean_progress_matches_activities"
kind = "schema_behavior"
actor = "root"
setup_sql = [
  "CREATE project:test SET name = 'Test';",
  "CREATE activity:a SET name = 'A', start = time::now(), end = time::now() + 1d, progress = 0.5;",
  "CREATE activity:b SET name = 'B', start = time::now(), end = time::now() + 2d, progress = 1.0;",
]
action_sql = "CREATE milestone:m SET project = project:test, activities = [activity:a, activity:b], name = 'M1';"
verify_sql = "SELECT VALUE progress FROM ONLY milestone:m;"
expect_success = true
assertions = [
  { path = ".", equals = 0.75 },
]


Run it with:

surrealkit test --user root --pass secret --ns main --db main


Test run: PASS (187ms)

Summary
- suites: 2 total, 0 failed
- cases : 2 total, 2 passed, 0 failed


SurrealKit loads every *.toml under database/tests/suites/ (including the default smoke suite, which is the second suite in that count). test spins up an ephemeral database per suite, applies the schema, runs your seed files, runs the cases, and fails CI the moment an assertion breaks. Because it seeds, expect the schema-apply and seed output to repeat once per suite before the summary; --no-seed skips that step if a suite would rather start from an empty database. Good early targets:

  • COMPUTED roll-ups, so a schema edit cannot quietly change the numbers

  • UNIQUE indexes, to confirm they are actually there

  • EVENT definitions, to confirm they apply without error (and later, that they fire when you write the right data)

  • Permissions and DEFINE API surfaces, if you add either later

See testing with SurrealKit for the rest of the options.

# .github/workflows/schema.yml (illustrative)
- run: surrealkit test
  env:
    SURREALDB_HOST: http://localhost:8000
    SURREALDB_NAMESPACE: ci
    SURREALDB_NAME: planning


Pair test with a review of the rollout plan on any pull request that touches database/schema/. Two more checks catch a schema change that nobody planned a rollout for:

  • surrealkit rollout lint, with no id, checks every manifest and how they chain, without a database. It fails when database/schema/ has changes that no rollout plans yet.

  • schema_from = "both" under [defaults] in database/tests/config.toml runs every suite twice: once on a database built with sync, and once on a database built by replaying every rollout from empty, the way production got its schema. A parity check runs first and lists any definition on which the two disagree.

# database/tests/config.toml
[defaults]
timeout_ms = 10000
schema_from = "both"
  • DEFINE EVENT for date validation, edge-case rules, and completion side effects

  • surrealkit test to guard against regressions

  • Events and indexes versioned right alongside the tables

Next: capstone, aligning with the docs sample, rollout baseline, and running schema across environments.

THE PLATFORM

Everything an application and its agents know. Five surfaces, one engine.

IN PRODUCTION

Trusted at scale. Samsung, Nvidia, Verizon, Tencent, and Walmart run on SurrealDB.

14,000+

Developers building on SurrealDB Cloud

4M+

Developers building on SurrealDB worldwide

FROM THE TEAMS

SurrealDB gives us a foundation where we can unify semantic search, knowledge graphs, and AI-driven decision making without stitching together multiple systems. Collapsing responsibility into SurrealDB has become our default engineering posture.
Justin Foley

VP of Engineering, Later

SurrealDB

The context and memory layer for AI agents

Database. Graphs, vectors, documents and relational data in one engine, in a single ACID transaction.
Agent Memory. Connects and retrieves context wherever your data lives, every fact carrying its source.
Cloud. Fully managed, in the cloud provider and region you choose.

Explore with AI

Copyright © 2026 SurrealDB Ltd. Registered in England and Wales. Company no. 13615201

Registered address: 3rd Floor 1 Ashley Road, Altrincham, Cheshire, WA14 2DT, United Kingdom

Trading address: Huckletree Oxford Circus, 213 Oxford Street, London, W1D 2LG, United Kingdom