Skip to content

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.ts lines 168–205. The class is intentionally abstract; runnable actors subclass it.

What an actor IS

Three things, in this order:

  1. An addressable inbox — a NATS subject {root}.{mount}.$inbox the actor subscribes to. Every message it handles arrives there.
  2. A declared surfacestatic accepts, static emits, static streams, static state, static subscribes, static blackboard. Together these describe everything the actor does. $introspect returns the full set.
  3. A supervised lifecycle — every actor can have children, monitors them for $exit, and restarts them under a configurable ISupervisionPolicy.
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.md and the MatrixActor static 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:

DeclarationNATS patternSuffixProtocol mapping
acceptsrequest/reply$inboxOpenAPI POST, gRPC unary, MCP tool
emitspub/sub$eventsOpenAPI SSE, gRPC server-stream, AsyncAPI pub
streamsJetStream stream$stream.<name>gRPC server-stream, AsyncAPI durable
stateJetStream KV (last-value)$stateOpenAPI GET, gRPC unary, MCP resource
subscribesinbound consumer(no own suffix; documents dependencies)AsyncAPI sub channels
blackboardJetStream KV (shared)shared bucketOpenAPI 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 streams and full blackboard framework methods (publishStream, setState, publishBlackboard, watchBlackboard) are documented in ACTOR-COMMUNICATION-CONTRACT.md and backed by separate system packages, but MatrixActor itself does not yet implement all of them as base-class helpers. The declarations are still first-class because $introspect and 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: $prompt and $source-code are 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:

ScopeLifetime
sessionper session, survives F5
userper user, survives new sessions
pageper browser tab
workspaceshared across workspace members
orgcompany defaults
rootroot-wide shared
realm / globalcross-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 accepts ops are invoked.
  • Events — how emits ops fan out.
  • Principal identity — CLAUDE.md Rule 5 in context.