Quickstart
Five minutes from “what’s an actor” to “I have one running, sending and receiving messages.” This guide assumes no prior actor-model experience.
What you’ll have at the end
Section titled “What you’ll have at the end”A small program that creates an ActorSystem, spawns a single Greeter
actor inside it, sends the actor a message, and shuts down. The same file
runs unchanged under Bun, Node, and Deno — picking the right runtime
backend automatically.
After this guide you’ll know:
- How to define an actor by extending
Actor<T>. - How to spawn it via
system.spawnAnonymous(Props.create(...)). - The difference between
tell(fire-and-forget) and the actor’sonReceivehandler. - How to shut the system down without leaking timers or sockets.
Install
Section titled “Install”bun add actor-tsnpm install actor-tsNo install step. Import directly from npm:
import { Actor, ActorSystem, Props } from 'npm:actor-ts';Run with --allow-net (for the TCP cluster transport) and
--allow-read (for reading config files) when you need them.
For a full installation walkthrough — including optional peer dependencies
(better-sqlite3, @hono/node-server, broker clients) — see the
Installation page.
Hello, actor
Section titled “Hello, actor”Create hello.ts and paste:
import { Actor, ActorSystem, Props } from 'actor-ts';
// 1. Define an actor. The type parameter `<string>` constrains what// messages this actor accepts — the type system catches bad sends// at compile time.class Greeter extends Actor<string> { override onReceive(name: string): void { console.log(`hello, ${name}!`); }}
// 2. Create the system. One `ActorSystem` per process is the norm;// everything else lives inside it.const system = ActorSystem.create('hello');
// 3. Spawn the actor. `Props.create(() => new Greeter())` is the// factory; `'greeter'` is the actor's name in the hierarchy.const ref = system.spawn(Props.create(() => new Greeter()), 'greeter');
// 4. Send a message. `tell` is fire-and-forget — it returns// immediately, the actor processes the message asynchronously on// its own mailbox.ref.tell('world');
// 5. Give the mailbox a tick to drain, then shut down. In a real// app you'd terminate on SIGTERM via `CoordinatedShutdown`; for// this script a short sleep is enough.await new Promise((resolve) => setTimeout(resolve, 20));await system.terminate();Run it:
bun run hello.tsnpx tsx hello.ts# or compile first: tsc hello.ts && node hello.jsdeno run hello.tsYou’ll see:
hello, world!That’s the whole loop: define → spawn → tell → terminate.
What just happened
Section titled “What just happened”The five lines map to the actor model’s five core ideas:
-
class Greeter extends Actor<string>— an actor is a class with a private mailbox and a singleonReceivehandler. The type parameter says “this actor acceptsstringmessages” — sending it a number would be a TypeScript error at compile time. -
ActorSystem.create('hello')— theActorSystemis the runtime container. It owns the dispatcher (which schedules message processing), the supervisor hierarchy (which catches actor failures), the scheduler, the event stream. There’s typically one per process. -
system.spawnAnonymous(Props.create(() => new Greeter()))—Propsis actor-ts’s way to defer construction. The system needs to control when the actor instance is created (on its mailbox thread, not on yours), so you hand it a factory rather than a pre-built instance. The returnedActorRefis a handle, not the actor itself — you can pass it across the cluster, store it, hand it to other actors. -
ref.tell('world')—tellis the primary actor verb. It enqueues the message into the actor’s mailbox and returns immediately. The actor processes its mailbox one message at a time on a single logical thread, so you never have to think about locks or races insideonReceive. -
system.terminate()— graceful shutdown. Stops the dispatcher, stops all actors (via the supervisor tree), closes the transports. Returns a promise that resolves when teardown is complete. For real apps, hook this into a SIGTERM handler — see Coordinated shutdown.
Going further
Section titled “Going further”A string mailbox is the absolute minimum — the framework gets
interesting once messages start carrying structure. Two short
extensions of the hello-actor sample to make that concrete:
Typed messages with a discriminated union
Section titled “Typed messages with a discriminated union”Real actors receive commands, not bare values. The convention is a
discriminated union — every message carries a kind literal that
narrows the union inside onReceive. match(cmd).exhaustive() from
ts-pattern gives you a
compile-time check that every variant is handled: add a new kind
without a matching with(...) arm and TypeScript fails the build.
import { Actor, ActorSystem, Props, type ActorRef } from 'actor-ts';import { match } from 'ts-pattern';
type Cmd = | { kind: 'inc' } | { kind: 'dec' } | { kind: 'get'; replyTo: ActorRef<number> };
class Counter extends Actor<Cmd> { private count = 0; override onReceive(cmd: Cmd): void { match(cmd) .with({ kind: 'inc' }, () => { this.count++; }) .with({ kind: 'dec' }, () => { this.count--; }) .with({ kind: 'get' }, (m) => m.replyTo.tell(this.count)) .exhaustive(); }}
const system = ActorSystem.create('counters');const counter = system.spawnAnonymous(Props.create(() => new Counter()));
counter.tell({ kind: 'inc' });counter.tell({ kind: 'inc' });counter.tell({ kind: 'dec' });The { kind: 'get'; replyTo: ActorRef<number> } variant is the
standard shape for “give me a reply”: the requester passes its own
ref (or, more commonly, one provided by ask, below) and the
counter tells the answer back. See
Messages and
Pattern matching
for the deep dive.
Getting a reply with ask
Section titled “Getting a reply with ask”tell is fire-and-forget. For request/response there are two
equivalent shapes:
import { ActorSystem, Props } from 'actor-ts';
const system = ActorSystem.create('counters');const counter = system.spawnAnonymous(Props.create(() => new Counter()));
counter.tell({ kind: 'inc' });counter.tell({ kind: 'inc' });
// Method-on-ref — terse, infers the reply type:const value = await counter.ask<number>({ kind: 'get' }, 5_000);console.log(value); // 2
await system.terminate();Or use the free function for the same effect:
import { ask } from 'actor-ts';
const value = await ask<Cmd, number>(counter, { kind: 'get' }, 5_000);Either way, the framework spawns a one-shot reply actor under the
hood, fills it in as replyTo AND as context.sender on the
target, and resolves the promise with the first reply (or rejects
with AskTimeoutError). Callers never write replyTo themselves.
See Ask pattern for timeout behaviour, error handling, and the dead-letter case (target actor stopped before replying).
Where to next
Section titled “Where to next”You have a running actor. From here the docs branch out depending on what you want to do:
-
Send more interesting messages: see Messages for the conventions (discriminated unions, immutability, the
kindfield) and Pattern matching for thematch().exhaustive()dispatch idiom this codebase uses everywhere. -
Get a reply back from the actor: see Ask pattern — the request/response equivalent of
tell. -
Handle failures: see Supervision for how parent actors recover from child crashes.
-
Distribute across machines: see the Cluster overview and Sharding overview — the same code you wrote above can run across N nodes with a single extra line.
Cluster.bootstrapbuilds theActorSystem, joins the cluster, starts the Receptionist and wiresSIGTERM/SIGINTshutdown in one call:import { Cluster, Props } from 'actor-ts';const { system } = await Cluster.bootstrap({ name: 'hello' });const ref = system.spawn(Props.create(() => new Greeter()), 'greeter');ref.tell('world');Discovery defaults to an env-driven chain —
CLUSTER_SEEDS→ Kubernetes API → DNS — so the same code runs single-node in local dev and joins an existing cluster in production without a config change. -
Persist state across restarts: see PersistentActor for event sourcing and Snapshots for bounded recovery time.
If you want to see the framework’s whole surface area at once, the chat sample is the most-comprehensive end-to-end demonstration — it uses sharding, persistence, distributed pubsub, distributed data, cluster singleton, HTTP routing, six interchangeable frontends, the lot.