Appearance
Actors
A Matrix actor is a process-local object that owns a NATS subject prefix and responds to messages on it. Every actor in the system extends the MatrixActor abstract class in @open-matrix/core and inherits the same mailbox shape, discovery commands, and supervision lifecycle.
Every actor is one of three primitive types (the substrate distinction you should keep in mind, especially when reading discovery/catalog code):
- Feed — publishes a stream. Market ticks, sensor data, LLM token streams, file-change events, heartbeats.
- Service — responds to requests. Inference, database query, image transform, translation, OCR.
- Component — consumes feeds, calls services, and (optionally) has a UI surface. Charts, dashboards, form widgets, whole pages, an agent's reasoning loop.
Feed/Service/Component are not subclasses; they are intent shapes an actor declares through its accepts / emits / streams declarations. The base class is the same; the bus contract differs. See WORKSTREAMS/thesis/THESIS.md Part 1.
Source:
projects/matrix-3/packages/core/src/core/MatrixActor.tslines 168–205. The class is intentionallyabstract; runnable actors subclass it.
What an actor IS
Three things, in this order:
- An addressable inbox — a NATS subject
{root}.{mount}.$inboxthe actor subscribes to. Every message it handles arrives there. - A declared surface —
static accepts,static emits,static streams,static state,static subscribes,static blackboard. Together these describe everything the actor does.$introspectreturns the full set. - A supervised lifecycle — every actor can have children, monitors them for
$exit, and restarts them under a configurableISupervisionPolicy.
typescript
// projects/matrix-3/packages/core/src/core/MatrixActor.ts:168
export abstract class MatrixActor {
static accepts: Record<string, unknown> = {};
static emits: Record<string, unknown> = {};
static subscribes: Record<string, { filter?: string[]; description?: string }> = {};
// ... see the source for streams, state, blackboard, skills, contract, ...
}In Erlang/OTP terms, every actor is a process that supervises children. There is no leaf/composite distinction; the file header documents this explicitly: "In the actor model (Erlang/OTP), every process can supervise children. There is no leaf/composite distinction. This class reflects that truth."
The six static declarations
Source:
WORKSTREAMS/core-and-packaging/ACTOR-COMMUNICATION-CONTRACT.mdand theMatrixActorstatic field list in source. The vocabulary is the protocol's conceptual surface.
typescript
class MyActor extends MatrixActor {
static accepts = { ... }; // Request/Reply — "call me and I answer"
static emits = { ... }; // Pub/Sub — "I fire these, subscribe if you care"
static streams = { ... }; // JetStream — "I produce replayable history"
static state = { ... }; // KV last-value — "watch for changes"
static subscribes = { ... }; // Inbound — "I listen for these from others"
static blackboard = { ... }; // Shared facts — "publish/consume on the blackboard"
}Each declaration maps to a NATS pattern, a protocol-gateway feature, and a specific subject suffix:
| Declaration | NATS pattern | Suffix | Protocol mapping |
|---|---|---|---|
accepts | request/reply | $inbox | OpenAPI POST, gRPC unary, MCP tool |
emits | pub/sub | $events | OpenAPI SSE, gRPC server-stream, AsyncAPI pub |
streams | JetStream stream | $stream.<name> | gRPC server-stream, AsyncAPI durable |
state | JetStream KV (last-value) | $state | OpenAPI GET, gRPC unary, MCP resource |
subscribes | inbound consumer | (no own suffix; documents dependencies) | AsyncAPI sub channels |
blackboard | JetStream KV (shared) | shared bucket | OpenAPI GET/PUT, gRPC bidi |
Today the runtime support for these declarations varies. accepts/emits are fully wired through MatrixActor and exposed via $introspect. streams, state (typed KV), and blackboard are first-class declarations the static surface understands; their full JetStream wiring lives in the system-blackboard, system-kv, and system-observability packages and is still landing per workstream. Treat the contract as authoritative — actors should declare even where the framework helpers are not yet attached, because introspection and gateway projection consume the declarations.
Status: target state, partial. The
static streamsand full blackboard framework methods (publishStream,setState,publishBlackboard,watchBlackboard) are documented inACTOR-COMMUNICATION-CONTRACT.mdand backed by separate system packages, butMatrixActoritself does not yet implement all of them as base-class helpers. The declarations are still first-class because$introspectand protocol gateways read them.
System commands every actor handles
MatrixActor auto-subscribes a fixed list of system commands at the actor's inbox. These are NOT in static accepts; they are infrastructure. The authoritative list is in source:
typescript
// projects/matrix-3/packages/core/src/core/MatrixActor.ts:111
const SYSTEM_COMMANDS = [
'$join',
'$join_ack',
'$leave',
'introspect',
'$introspect',
'$ping',
'$reply',
'$getState',
'$getHistory',
'$createControl',
'$activity',
'$preferences',
'$config',
'$membrane-inherit',
'$membrane',
'$skill',
'$cognitive.set',
'$cognitive.get',
'$triggers.add',
'$triggers.remove',
'$triggers.list',
'$skills.add',
'$skills.remove',
] as const;Note:
$promptand$source-codeare intentionally NOT in this list. The source comment explains: "They live in headlessFrameworkOps (see _subscribeToInbox) so that shell-managed actors defer to the shell handler, avoiding the dual-dispatch race (actor + shell both fire for the same op)."
The aspirational Plan 9-aligned set documented in ACTOR-COMMUNICATION-CONTRACT.md ($ctl, $status, $children, $ns, $env, $health) is target state. Where these are not yet wired, callers should rely on $introspect plus mount-specific control ops.
Cognitive fields
MatrixActor carries an opt-in cognitive layer — fields like purpose, systemPrompt, regime, promptable, promptTriggers, memoryScope, continuationType, planScope. When static promptable = false (the default), there is zero cognitive overhead: no oracle loop, no trigger subscriptions, no plan tracking, no budget tracking. Actors opt in by setting promptable = true and declaring purpose/systemPrompt. See projects/matrix-3/packages/core/src/core/MatrixActor.ts lines 320–404 for the full set.
Persistence scope
Every actor declares static stateScope (default 'session'). The scope determines the KV key prefix and cascade resolution order:
| Scope | Lifetime |
|---|---|
session | per session, survives F5 |
user | per user, survives new sessions |
page | per browser tab |
workspace | shared across workspace members |
org | company defaults |
root | root-wide shared |
realm / global | cross-root, federation-wide |
static persistState = false opts the actor out of KV persistence entirely (useful for ephemeral UI actors).
Identity is from transport metadata
Per CLAUDE.md Rule 5: "Identity from transport metadata, NEVER from payload. Transport metadata = authenticated identity. Payload = untrusted. Never trust payload.principalId."
When an actor receives a request, the principal is the one the transport authenticated, not whatever the payload claims. Actor handlers that authorize based on principal must read the principal from MessageContext, never from the request body.
See also
- Subjects — how an actor's inbox/events/state map to wire subjects.
- Mounts — the logical name vs the runtime identity vs the wire prefix.
- Request/reply — how
acceptsops are invoked. - Events — how
emitsops fan out. - Principal identity — CLAUDE.md Rule 5 in context.