Skip to content
NEW

Introducing SurrealDB Studio, the new official app of SurrealDB

Learn more

1/5

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.

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 asynchronously relative to the statement that triggered them, so design for idempotency, and make failures visible, before you rely on this in production.

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/.

  • 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.

Previous

12: People and indexes

Next lesson

14: Capstone

SurrealDB

The context layer for AI agents.

Documents, graphs, vectors, time-series, and memory.
One transaction, one query, one deployment.

Explore with AI

Stay in the loop

Tutorials, AI agent recipes, and product updates, every two weeks.

Independently verified

SOC 2 Type 2

GDPR

Cyber Essentials Plus

ISO 27001

Trust Centre

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