Mental Model
How to think about Yoke — stream spine, no targets, emergent coordination, and fixed definitions with dynamic instances.
Yoke is designed around a small set of constraints that keep complex agentic systems composable. Understanding these constraints upfront prevents fighting the runtime later.
The runtime lives inside your product. Your server embeds yoke-runtime; the stream, actors, and portals below are implementation details of that process. Users never subscribe to the stream or call @yoke/sdk. See Embed Yoke in your product.
The stream is the spine
Every meaningful action in a Yoke system becomes an event on a single global, append-only stream. Actors do not call each other directly. Portals do not push into actor mailboxes. Everything flows through typed events:
Portal webhook → append(slack.message.received)
Router actor → append(work.assigned)
Coder actor → append(code.produced)The stream is the single sequencing authority. Each event gets a monotonic seq number. This is audit history, not a replay log — actors recover from their queues and SQLite state, not by re-executing the stream.
User code has no stream read API. Only the Rust kernel tails the stream internally for fan-out. This is a deliberate security boundary.
No targets — publish and subscribe
Traditional message systems route messages to named recipients. Yoke deliberately avoids this:
// Correct — publish a typed event
await ctx.append({ type: "work.assigned", payload: { role: "coder", lang: "typescript" } });
// Wrong — does not exist
await ctx.append({ type: "...", target: "coder" });Coupling is by convention, not routing tables. A coder actor subscribes to work.assigned events where payload.lang matches its config. A router actor publishes those events. Neither knows the other exists.
This enables:
- Dynamic topology — spin up N coder instances without reconfiguring publishers
- Emergent orchestration — supervisor patterns, pipelines, and meshes arise from filter overlap
- Loose coupling — add a new consumer by creating an instance with a filter
Two kinds of processors
Yoke has exactly two processor archetypes. They are not interchangeable.
Actors — reactive
Actors react to stream events matched by their filter. Each instance has:
- A persisted queue of matched events
- Durable SQLite state (
ctx.state,ctx.sqlite) - A
process(ctx, event)handler invoked by the scheduler
Actors are for work that happens because something occurred — a message arrived, a payment cleared, a timer fired.
Portals — proactive
Portals connect to the outside world. Each portal definition has one singleton instance that:
- Runs a long-lived
start(ctx)loop (WebSocket, polling, etc.) - Registers HTTP routes on the runtime server
- Optionally exposes a typed interface actors call via
ctx.portals.<name>.*
Portals have no queues and no filters. They append events (ingress) and serve egress calls.
EXTERNAL PORTAL STREAM ACTOR
│ │ │ │
│── webhook ─────►│ │ │
│ │── append ────────►│ │
│ │ │── fan-out ────►│
│ │ │ │── process
│ │ │◄── append ─────│
│◄── portal_call ─│◄──────────────────│ │Definitions vs instances
| Definition | Instance | |
|---|---|---|
| When | Build time (defineRuntime) | Runtime (ctx.instances.create) |
| What | process, queue policy, schemas | config, filter |
| Count | Fixed set per package | Unlimited, dynamic |
| Stored | TypeScript module | Postgres |
You cannot add new actor or portal types at runtime. You can create unlimited instances of registered definitions.
// Build time — register the coder definition
defineRuntime().actors(() => ({
coder: actor({ configSchema, stateSchema, process }),
}));
// Runtime — create instances with different config/filters
await ctx.instances.create("coder", {
config: { model: "gpt-4o", lang: "typescript" },
filter: { type: "work.assigned", where: { "payload.lang": "typescript" } },
});
await ctx.instances.create("coder", {
config: { model: "claude-sonnet", lang: "python" },
filter: { type: "work.assigned", where: { "payload.lang": "python" } },
});Emergent multi-agent systems
Yoke does not ship orchestration primitives — no supervisor actor, no graph DSL, no built-in agent loop. Complex systems emerge from:
- Event type conventions —
work.assigned,code.produced,conversation.message.posted - Filter overlap — multiple actors subscribe to overlapping event types
- Instance creation — actors spawn child workers via
ctx.instances.create() - Portal interfaces — typed egress to external systems
The agent mesh pattern (ingestion → processor → egress) is one emergent composition built from standard actors and the conversation store — not a kernel primitive.
What Yoke is not
| Not Yoke | Instead |
|---|---|
| Your product / coding harness | Embed yoke-runtime in your server; users talk to you |
| Public SDK for end users | @yoke/sdk is the internal actor package API |
| LLM framework | Use ctx.llm.infer() inside a processor actor |
| Message broker with topics | Global stream + per-instance filters |
| Workflow engine with replay | Queue + SQLite recovery |
| Chat server | Conversation store + stream delivery |
| Serverless functions | Always-on drain loops with durable state |
System diagram
flowchart TB
subgraph external [External World]
HTTP[HTTP / Webhooks]
WS[WebSockets]
end
subgraph portals [Portals]
P[Portal start + routes]
end
subgraph stream [Global Stream]
S[(Postgres stream_events)]
end
subgraph actors [Actors]
A1[Instance A]
A2[Instance B]
Q1[Queue]
Q2[Queue]
DB1[(SQLite)]
DB2[(SQLite)]
end
HTTP --> P
WS --> P
P -->|append| S
S -->|fan-out| Q1
S -->|fan-out| Q2
Q1 --> A1
Q2 --> A2
A1 --> DB1
A2 --> DB2
A1 -->|append| S
A2 -->|append| S
A1 -->|portal_call| P