Quick Start
Create a minimal package, start the runtime, append a test event, and watch actors process it end to end.
This walkthrough uses a minimal inline package — a Slack portal, a router actor, and a coder actor — so you can see stream fan-out without writing a host yet. Production still means embedding the kernel in your server; yoke dev is not the product interface.
By the end you will have a live runtime, an appended slack.message.received event, and actors that react through the global stream.
What your package does
The package wires three pieces together:
- Slack portal — HTTP webhook ingress and a typed
sendMessageinterface - Router actor — listens for
slack.message.received, emitswork.assigned - Coder actor — listens for
work.assigned(function filter onlang), emitscode.produced, callsctx.portals.slack.sendMessage
Step 1 — Prerequisites
Start Postgres and set environment variables (see Installation):
docker compose up -d
export DATABASE_URL=postgres://yoke:yoke@localhost:5432/yoke
export YOKE_STORAGE=pg_sqliteInstall the yoke CLI (see Installation).
Step 2 — Create your package
Create a folder ./your-package with deno.json and yoke.ts:
// your-package/deno.json
{
"imports": {
"@yoke/sdk": "jsr:@yoke/sdk@^0.1.0"
}
}// your-package/yoke.ts
import { defineEvents, defineFor, defineRuntime, z } from "@yoke/sdk";
const events = defineEvents({
"slack.message.received": z.object({
channel: z.string(),
text: z.string(),
}),
"work.assigned": z.object({
text: z.unknown().optional(),
role: z.string(),
lang: z.string().optional(),
}),
"code.produced": z.object({ code: z.string() }),
});
export default defineRuntime()
.events(events)
.bootstrap(async (ctx) => {
await ctx.instances.create("router", {
id: "router",
config: { channels: ["#general"] },
filter: { type: "slack.message.received" },
});
await ctx.instances.create("coder", {
id: "coder",
config: { model: "claude-sonnet", lang: "typescript" },
});
})
.portals(({ events }) => {
const { portal } = defineFor(events);
return {
slack: portal({
configSchema: z.object({ botToken: z.string().optional() }),
interface: {
async sendMessage(channel, text) {
console.log(`slack: ${channel} <- ${text}`);
},
},
routes: [
{
method: "POST",
path: "/webhooks/slack",
handler: async (req, ctx) => {
const body = JSON.parse(
new TextDecoder().decode(req.body ?? new Uint8Array()),
);
await ctx.append({
type: "slack.message.received",
payload: {
channel: body.channel ?? "#general",
text: body.text ?? "",
},
});
return { status: 200, body: "ok" };
},
},
],
}),
};
})
.actors(({ events, portals }) => {
const { actor } = defineFor(events, portals);
return {
router: actor({
configSchema: z.object({ channels: z.array(z.string()) }),
stateSchema: z.object({ lastSeq: z.number() }),
queue: { mode: "fifo", concurrency: 1 },
async process(ctx, event) {
if (event.type === "slack.message.received") {
await ctx.append({
type: "work.assigned",
payload: {
text: event.payload.text,
role: "coder",
lang: "typescript",
},
});
}
await ctx.state.update((s) => ({ ...s, lastSeq: event.seq }));
},
}),
coder: actor({
configSchema: z.object({ model: z.string(), lang: z.string() }),
stateSchema: z.object({ jobsCompleted: z.number() }),
filterFromConfig: (config) => (event) =>
event.type === "work.assigned" && event.payload.lang === config.lang,
queue: { mode: "fifo", concurrency: 1 },
async process(ctx, event) {
await ctx.append({
type: "code.produced",
payload: { code: "// generated" },
});
await ctx.portals.slack.sendMessage("#general", "Code ready");
await ctx.state.update((s) => ({
...s,
jobsCompleted: s.jobsCompleted + 1,
}));
},
}),
};
})
.build();See Your first package for a deeper walkthrough of each piece.
Step 3 — Start the runtime
yoke dev ./your-package \
--database "$DATABASE_URL" \
--storage pg_sqlite(yoke start is an alias for yoke dev. For production compile/deploy, see Compile & deploy.)
On boot the runtime:
- Runs Postgres migrations
- Spawns the embedded Deno worker and loads
yoke.ts - Ensures portal singletons and runs
bootstrap()(creates router/coder instances) - Starts fan-out and actor drain loops
- Registers portal HTTP routes (including
POST /webhooks/slack)
Leave this process running in a terminal. On success you should see the Deno worker load and the runtime reach a ready state (health check returns ok).
Step 4 — Append a test event
In a second terminal, use the CLI append subcommand:
yoke append \
--database "$DATABASE_URL" \
--storage pg_sqlite \
--type slack.message.received \
--payload '{"channel":"#general","text":"hello"}'The command prints the assigned stream position:
appended seq=1 id=<uuid>Alternatively, POST to the portal webhook while the runtime is running:
curl -X POST http://localhost:8080/webhooks/slack \
-H 'Content-Type: application/json' \
-d '{"channel":"#general","text":"hello from webhook"}'Step 5 — Watch actors process
In the runtime terminal you should see processing unfold:
- Router receives
slack.message.received(declarative filter from instance creation) - Router appends
work.assignedwithrole: "coder"andlang: "typescript" - Coder matches via its function filter (
filterFromConfig) - Coder appends
code.producedand callsctx.portals.slack.sendMessage("#general", "Code ready")
Expected console output from the coder path:
slack: #general <- Code readyEvent flow diagram
sequenceDiagram
participant CLI as CLI / Webhook
participant Stream as Global Stream
participant Router as router instance
participant Coder as coder instance
participant Slack as slack portal
CLI->>Stream: append slack.message.received
Stream->>Router: fan-out to queue
Router->>Stream: append work.assigned
Stream->>Coder: fan-out to queue
Coder->>Stream: append code.produced
Coder->>Slack: sendMessage("#general", "Code ready")Inspect runtime state
| Endpoint | Purpose |
|---|---|
GET /health | Liveness check — returns ok |
GET /metrics | Prometheus metrics (yoke_stream_appends_total, yoke_queue_depth, …) |
Check queue depth per instance:
curl -s http://localhost:8080/metrics | grep yoke_queue_depthStop the runtime
Press Ctrl+C in the runtime terminal. The runtime shuts down the Deno worker and flushes storage backends.
The append CLI command starts a minimal runtime, appends one event, and stops. For sustained processing while authoring, use yoke dev with your package directory. To ship, embed the kernel in your server.
Next steps
- Read Embed Yoke in your product to put this kernel inside your own server
- Read Your first package to understand the
yoke.tssource in more detail - Learn how instances are created in Instances
- Learn the mental model in Concepts