Appearance
Events
An event is a fire-and-forget publish on an actor's $events subject. Any actor or external client may subscribe; if nobody is subscribed, the event is dropped. Events are the protocol's pub/sub primitive.
Source:
WORKSTREAMS/core-and-packaging/ACTOR-COMMUNICATION-CONTRACT.md§ "static emits — Pub/Sub Events". Quoted: "I fire these. Subscribe if you care. Ephemeral — if nobody is listening, the event is lost. For live updates, not historical replay."
Wire pattern
{root}.{mount}.$eventsThe actor publishes one envelope per event. Subscribers receive a copy. There is no acknowledgement, no retry, no broker-side queue. NATS delivers to current subscribers and discards the message.
Example (root = COM.NIMBLETEC.RICHARD-SANTOMAURO):
publish: COM.NIMBLETEC.RICHARD-SANTOMAURO.system.runtimes.$events
payload: { op: 'runtimes.added', runtimeId: 'rt_chat_01', type: 'chat',
app: 'chat', sessionMount: 'chat' }The op name on the wire is the event name, taken from the actor's static emits declaration. From projects/matrix-3/packages/system-runtimes/src/RuntimeManagerActor.ts line 339:
typescript
this.emit('runtimes.added', { runtimeId, type, app, sessionMount });Subscribing
Subscribers attach a handler to {mount}/$events:
typescript
this.subscribe('system.runtimes/$events', (payload, meta) => {
// payload includes the op name (the actor encodes it)
});Wildcards work normally:
chat/# ← all events under chat
chat.conversation/+/$events ← per-conversation eventsDeclaration shape
From ACTOR-COMMUNICATION-CONTRACT.md:
typescript
static emits: Record<string, {
description: string; // What triggers this event. When. How often.
schema: Record<string, FieldDescriptor>;
frequency?: 'high' | 'medium' | 'low' | 'burst' | 'periodic';
}>;Real-world example (verbatim from projects/matrix-3/packages/system-platform/src/ServiceRegistryActor.ts:189):
typescript
static override emits = {
'service.registered': { description: 'Emitted when a service endpoint is registered' },
'service.deregistered': { description: 'Emitted when a service endpoint is removed' },
'registry.registered': { description: 'Emitted when a logical mount claim is registered' },
'registry.heartbeat': { description: 'Emitted when a logical mount claim heartbeat is accepted' },
'registry.unregistered':{ description: 'Emitted when logical mount claims are removed' },
};Each entry's description is what $introspect returns and what protocol gateways surface. The schema field (per-field descriptors) is documented in ACTOR-COMMUNICATION-CONTRACT.md; many production actors today use the short form (just description).
Frequency hints
The optional frequency declaration tells consumers what to expect:
| Value | Rate of fire |
|---|---|
high | 10+ per second (e.g., market.tick) |
medium | 1–10 per second (e.g., chat token streams) |
low | <1 per second (e.g., status changes) |
burst | bursty — bursts of high-rate then quiet |
periodic | scheduled — every N minutes |
This is a hint for consumer-side rate-limiting and UI throttling. The framework does not enforce it.
Events vs streams vs state
| Property | emits (events) | streams (JetStream) | state (KV) |
|---|---|---|---|
| Persistence | none | persisted, replayable | persisted, last-value |
| Late subscriber sees history | no | yes (replay) | yes (current value) |
| Multiple consumers | yes (fan-out) | yes (consumer groups) | yes |
| Fits high-volume telemetry | yes | yes (with retention cost) | no — overwrites |
| Fits dashboards | needs subscription on connect | yes | yes — best fit |
Use events for "right now, broadcast to whoever is here". Use streams for "replayable history". Use state for "current value, watchable".
Trace context propagation
Events carry the traceId/spanId/parentSpanId fields from the envelope when the publishing actor was acting under a trace. Subscribers that want to continue the trace start a child span using parentSpanId. The framework helpers (startSpan, getActiveTraceContext) live in projects/matrix-3/packages/core/src/telemetry/tracing.ts.
What events do NOT do
- They do not retry. If a subscriber is slow, NATS drops to backpressure rules; if a subscriber crashes, it misses events for the duration of the outage.
- They do not order across publishers. Two actors publishing to the same event topic are ordered per-publisher only.
- They are not durable. A late subscriber sees only events published after it subscribed. Use streams when this matters.
- They are not authenticated end-to-end. Subscribe is gated by NATS ACL on the subject; the publisher's identity is in the envelope's
fromfield but is not signed.
Idempotent subscription
MatrixActor subscriptions are tracked in a SubscriptionBag. Disposing the actor automatically tears down all its subscriptions. Subscribing to the same (topic, handler) twice is a no-op at the subscription bag level; at the broker level NATS allows duplicate subscriptions and will deliver duplicates if both are active.
See also
- Subjects —
$eventswire format. - Sessions — events as the streaming substrate for sessions.
- Reserved subjects —
$eventsis reserved.