Live events
Every change to an organization's ontology is appended to its outbox in the same transaction, and streamed to subscribers as server-sent events they can resume without gaps.
The outbox
Every ontology write appends events to the organization’s outbox
(ontology_events) inside the write’s own transaction, so an event exists
exactly when its change committed.
| Event | Payload |
|---|---|
object.created, object.updated | type, id, version, etag, updated_at, updated_by, properties, and for updates changed: all as the subscriber sees them; an update to properties they cannot see is not delivered |
object.deleted | type, id |
link.created, link.deleted | link, from_type, from, to_type, to |
type.created, type.changed, type.deleted | type, version (changed also on access changes) |
type.validation | type, rules_version, state, objects, validated, invalid: progress of the background validation |
link_type.created, link_type.changed, link_type.deleted | link, from_type, to_type |
team.member_added, team.member_removed | type, id (the team), user, and for added expires_at: a membership of a team object changed; a membership running out sends nothing |
series.points | type, series, object, tags, count, first_at, last_at, points (at most 100), truncated: one per series a batch changed, for timeseries types with live on; delivered to those who may read the series |
chat.* | type, channel, and the channel, member, message, reaction or pin: every change in a chat channel, delivered to those who can read the channel; chat.typing, without an id, is never stored |
fs.* | type, node, and what changed: a folder or file of a filesystem type created, written, changed, moved, deleted, restored, or restricted; delivered to those who can see it |
action.applied | the action’s result, after the events of its edits; about every type the action touches |
Schema changes never rewrite objects, so they emit only type.changed;
removed properties simply stop appearing in later events, and the background
validation of new rules reports its progress as type.validation events,
at most once a second (less often on large types) while the job runs, and
once when it is done with the type, so the last one always shows the finished
state. Deleting an object or a link type emits link.deleted for each of its
visible links.
Each event has a sequence number from a per-organization counter row. The writer locks that row until it commits, so an organization’s events commit in sequence order and a reader that has seen event n has seen everything before it. Writers of one organization serialize on the counter only for the moment between their last statement and commit.
Subscribing
GET /v1/ontology/events?types=employee,project
Authorization: Bearer <token>
Accept: text/event-streamid: 1042
event: object.updated
data: {"type":"employee","id":"o4b8j2e6c0t1q5w9r3y7u2i6","version":3,"etag":"Lk2pR9cXv4bQ7mNw1sT0ya","updated_at":"2026-10-24T10:05:00Z","updated_by":"h3jd8s0q2l5x7c1v9b4n6m8k","changed":["age"],"properties":{"name":"Ada","age":37}}
id: 1043
event: link.created
data: {"link":"works_on","from_type":"employee","from":"o4b8j2e6c0t1q5w9r3y7u2i6","to_type":"project","to":"o9k3m7p1s5v2x6z0b4d8f2h6"}- Needs
view_objects. types(object type api names) andobjects(object ids), each comma-separated with up to 100 values, narrow the stream to events about any of them;channels(chat channel ids) to the events in any of those channels.- Without a cursor, the stream starts at the next change.
- The SSE
idis the event’s sequence number. Reconnect withLast-Event-ID: <id>(browsers do this automatically) or?after=<id>to continue right after it.
What a subscriber receives
Delivery is decided per event, when it is read, not when the stream opened. Each read opens a fresh transaction re-authenticated with the stream’s credential, then:
- only the subscriber’s own organization’s events exist for it (row level security plus an explicit organization filter);
- an event goes out only if the subscriber may view, at that moment, every
object type it is about. A link between an
employeeand a restrictedsalary_bandis not delivered to someone who cannot viewsalary_band; - for a type the subscriber sees only in part (the
objects shared with them),
an event goes out only if they see every object it names, and the type’s
type.validationevents never do. An object shared with the subscriber starts sending them its events; - object events are written with every property, and the properties the
subscriber may not see (restricted properties)
are stripped before delivery, decided at the read like everything else.
version,etag,updated_at,updated_byandchanged(the properties the update changed) are the subscriber’s own, and anobject.updatedthat changed no property they see is not delivered to them at all; - an ended session, a deactivated user, or a lost
view_objectsends the stream.
Streams wake on NOTIFY ontology_events (the payload is just the
organization id) and also poll every 10 seconds, which bounds how long an
idle stream outlives a revoked session.
Retention and limits
-
Events are pruned after 7 days. A cursor older than what was pruned gets a
resetevent; reload the data you care about, then continue from the stream:Text event: reset data: {"reason":"events after the cursor were pruned; reload"} -
At most 20 open streams per user per replica; more get
429.
In short: a stream delivers only its own organization’s events, only those about types its subscriber may view when the event is read, and resuming from a cursor within the retained history misses no event.