Skip to content

Actor declarations

Declaring an actor is a two-part exercise: a manifest declaration in matrix.json:components[] and a static contract on the actor class in source. The manifest tells the runner which class to mount and where; the static contract tells callers (humans and LLMs) what the actor accepts, emits, and streams.

Part one: the manifest entry

A components[] entry minimally looks like this:

json
{
  "type": "MyServiceActor",
  "export": "MyServiceActor",
  "mount": "my-service"
}

Validator-required only type. Everything else is optional but strongly recommended in production packages.

Full shape, with all the fields the live tree uses:

json
{
  "type": "ChatConversationProxy",
  "export": "ChatConversationProxy",
  "mount": "conversation",
  "surface": "headless",
  "autoStart": true,
  "description": "Conversation session management and prompt routing",
  "accepts": ["conversation.start", "conversation.send", "conversation.list"],
  "emits": ["conversation.message"],
  "fsPolicy": "none",
  "props": { "maxSessions": 1000 }
}

Field reference:

FieldRequiredPurpose
typeyesThe class name (used as the binding identifier)
exportnoNamed export from runtime.entry; default is type
mountnoRelative bus mount inside the package namespace; final wire address adds the runtime root
surfaceno"headless" (process-side) or "browser" (custom element)
autoStartnoIf true, the runtime mounts this actor at startup without waiting for a request
descriptionnoOne-line description; surfaces in introspection and the discovery index
acceptsnoOp names the actor handles
emitsnoEvent names the actor publishes
fsPolicynoPer-component override of the package-level permissions.fsPolicy
propsnoObject passed into the actor constructor

Note: accepts/emits in matrix.json are advisory; the ground truth lives on the class itself. The discovery extractor (packages/mx-cli/src/utils/discovery-extractor.ts) reads both, with the static contract winning when they conflict.

Part two: the static contract on the class

CLAUDE.md Rule 3 (Actor Communication Contract) requires every actor to declare:

  • static accepts — ops the actor handles
  • static emits — events the actor publishes
  • static streams — long-running streams the actor exposes
  • static state — schema of the actor's persisted state
  • static subscribes — events the actor subscribes to
  • static blackboard — bus topics the actor reads/writes

Every op and field MUST have a description. The mx actor scaffold creates a minimal contract:

ts
import { MatrixActor } from "@open-matrix/core";

export class UserStoreActor extends MatrixActor {
  static override accepts = {
    getStatus: {},
  };

  static override emits = {
    statusChanged: { status: "string" },
  };

  private _status = "ready";

  protected onGetStatus(): { status: string } {
    return { status: this._status };
  }
}

A production actor declaration looks more like this (paraphrased from system-runtimes):

ts
export class RuntimeManagerActor extends MatrixActor {
  static override accepts = {
    "runtimes.list": {
      description: "List runtime records visible from this Host",
      cursor: { type: "string", description: "opaque pagination cursor" },
      limit: { type: "number", description: "max items to return", default: 100 },
    },
    "runtimes.start": {
      description: "Start a runtime described by a startSpec",
      startSpec: { type: "object", required: true,
        description: "shape defined in IHostRuntimeStartSpec" },
      idempotencyKey: { type: "string",
        description: "optional dedup key for the start request" },
    },
  };

  static override emits = {
    "runtimes.changed": {
      description: "Emitted whenever the runtime registry mutates",
      runtimeId: { type: "string", description: "Runtime that changed" },
    },
  };

  static override state = {
    runtimes: { type: "array",
      description: "Live runtime records keyed by runtimeId" },
  };
}

Two-half consistency

The runner uses both halves. The manifest tells the runner which class to instantiate and where to mount it. The static contract tells the runner what to validate, what to log, and what to surface in introspection.

A discrepancy between the two is allowed (the validator does not cross-check), but it always indicates a bug. Specifically:

  • If matrix.json:components[].accepts lists an op the class doesn't implement → the actor will throw unknown-op at runtime.
  • If the class implements an op but the manifest doesn't list it → it works at runtime but is invisible to discovery and audit tools.

Identity from transport metadata

CLAUDE.md Rule 5: actors derive caller identity from transport metadata, never from payload fields. Do not write op handlers that trust payload.principalId. The runner's transport plumbing fills in authenticated identity on every envelope.

See also

Source: projects/matrix-3/packages/mx-cli/src/utils/manifestValidator.ts validates the manifest half; WORKSTREAMS/core-and-packaging/ACTOR-COMMUNICATION-CONTRACT.md is the authoritative spec for the static contract.