Skip to main content

Events & the envelope

Every event in CNS — audit trail, activity log, or notification trigger — is the same envelope. There is no separate "notification event" type; notification intent is a per-event policy decision (see Policies) layered on top of one universal shape.

The envelope

Mirrors internal/core/event.Event and schemas/event.schema.json field-for-field. "Client-suppliable" means a caller may set it on POST /v1/events; everything else is always derived server-side, even if a client sends a value for it (extra/unknown fields are rejected outright — the schema sets additionalProperties: false).

FieldClient-suppliable?Notes
event_idNoServer-minted ULID at ingestion. Doubles as the idempotency/dedup key.
tenant_idNoDerived from the authenticated principal. Never trusted from the client.
event_typeYesDomain event name, e.g. invoice.paid.
source.app / source.service / source.environmentNoDerived from the authenticated app/service credential.
actor.subject_idNo (usually)The end-user caller, unconditionally. An app-principal caller may assert it via actor_subject_id — see Apps & credentials.
target.subject_ids[]YesWho should be notified (fan-out). Platform validates every id belongs to the tenant.
provenanceNobackend-verified | client-asserted — see Trust & render-tokens.
deliveryNo (rule-influenced)instant | batched — the latency lane; set by policy, not by the caller.
event_timeYesClient/source occur-time. Advisory (display) — never trusted for ordering.
ingest_timeNoServer-stamped at ingestion. Trusted — the ordering anchor.
session_idYesScopes seq. One per SDK-instance lifetime, or a propagated trace id.
seqYesPer-session monotonic sequence, assigned at emit-time.
monotonic_offset_msYes (optional)performance.now()-derived offset for reliable intra-session interval math.
dataYes (optional)Domain payload — whatever shape your event_type needs.
metadataYes (optional)Out-of-band context — e.g. a render-token for provenance escalation.

Two orthogonal axes, not two kinds of event

  • Persistence is universal. Every event lands in the chronological store — the activity/audit backbone. Nothing is "notification-only."
  • Notification intent is per-event, decided by policy on event_type (an explicit notify flag on the matching rule).

Two more axes ride along without changing the envelope shape:

  • Emission mechanismmanual (your code calls emit(), Phase 1) vs. auto (Phase-2 middleware calls the same emit() with telemetry defaults) is about who calls emit(), not a different envelope.
  • Latency lane (delivery: instant | batched) follows notification priority, not manual-vs-auto — an auto-emitted high-priority event can still be instant.

actor vs. target

actor is who performed the action — it's the activity-timeline key ("who did what, in order"). target is who should be notified — plural, because one action can fan out to several recipients, each with their own delivery/read state. An approval's actor is the approver; its target is the payee. A viewer acknowledging their own notice is both — dedup is automatic when actor ∈ target.

Ordering: three clocks, never conflated

  • event_time — advisory, client/source-supplied, for display only.
  • ingest_time — trusted, server-stamped, the real ordering anchor.
  • seq — per-session monotonic, incremented at emit-time by the emitting SDK instance. This — not wall-clock time — is the ordering primitive within a session: zero coordination, immune to clock skew, safe under batching (a late batch still slots in correctly by seq/event_time on read).

Cross-session timelines merge by session-start ingest_time, then seq — the exact composite position the Activity API's opaque cursor encodes. A gap or duplicate in a session's seq sequence is a provable integrity signal (dropped or replayed events), not something CNS silently papers over.

See it end to end

Quickstart walks through emitting one of these and reading it back; API Reference has the exact request/ response bodies.