Yoke.sh
Concepts

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

DefinitionInstance
WhenBuild time (defineRuntime)Runtime (ctx.instances.create)
Whatprocess, queue policy, schemasconfig, filter
CountFixed set per packageUnlimited, dynamic
StoredTypeScript modulePostgres

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:

  1. Event type conventions — work.assigned, code.produced, conversation.message.posted
  2. Filter overlap — multiple actors subscribe to overlapping event types
  3. Instance creation — actors spawn child workers via ctx.instances.create()
  4. 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 YokeInstead
Your product / coding harnessEmbed yoke-runtime in your server; users talk to you
Public SDK for end users@yoke/sdk is the internal actor package API
LLM frameworkUse ctx.llm.infer() inside a processor actor
Message broker with topicsGlobal stream + per-instance filters
Workflow engine with replayQueue + SQLite recovery
Chat serverConversation store + stream delivery
Serverless functionsAlways-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

Next steps

On this page