State & SQLite
ctx.state typed document and ctx.sqlite raw SQL — shared per-instance database, state_cas, and durability.
Each actor instance has a private SQLite database. The SDK exposes two APIs on the same underlying store: a typed document abstraction (ctx.state) and raw SQL (ctx.sqlite).
Shared database
┌─────────────────────────────────┐
│ Per-instance SQLite database │
│ │
│ __yoke_state ← ctx.state │
│ custom tables ← ctx.sqlite │
└─────────────────────────────────┘
│
▼
pg_sqlite BYTEA in Postgres
or S3 object (YOKE_STORAGE)Never write __yoke_state directly. Use ctx.state for the typed document. Direct SQL against other tables is fine via ctx.sqlite.
ctx.state — typed document
Backed by reserved __yoke_state table with JSON blob + version column.
const coder = defineActor({
stateSchema: z.object({
jobsCompleted: z.number(),
lastModel: z.string().optional(),
}),
// ...
});
async process(ctx, event) {
const s = await ctx.state.get();
await ctx.state.set({ ...s, jobsCompleted: s.jobsCompleted + 1 });
// Atomic read-modify-write (preferred)
await ctx.state.update((s) => ({
...s,
jobsCompleted: s.jobsCompleted + 1,
lastModel: ctx.config.model,
}));
}DurableState API
| Method | Behavior |
|---|---|
get() | Read current document (validated against stateSchema) |
set(value) | Overwrite entire document |
update(fn) | Atomic read-modify-write with optimistic locking |
update() calls the state_cas op under the hood:
1. state_get → { data, version }
2. Apply updater function in Deno
3. state_cas(expected_version, new_data)
4. On conflict → retry or throwConcurrent updaters serialize — no lost writes.
ctx.sqlite — raw SQL
For indexes, custom tables, and queries:
await ctx.sqlite.exec(`
CREATE TABLE IF NOT EXISTS jobs (
id TEXT PRIMARY KEY,
status TEXT NOT NULL,
created_at INTEGER NOT NULL
)
`);
await ctx.sqlite.exec(
"INSERT INTO jobs (id, status, created_at) VALUES (?, ?, ?)",
[jobId, "pending", Date.now()],
);
const rows = await ctx.sqlite.exec(
"SELECT id, status FROM jobs WHERE status = ?",
["pending"],
);Every exec() persists immediately to the storage backend.
sqliteStoreId
Advanced partitioning via sqliteStoreId on the actor definition:
defineActor({
sqliteStoreId: (ctx) => `${ctx.instanceId}:${ctx.config.tenantShard}`,
// ...
});ctx.sqliteStoreId in process() reflects the resolved store key. Default is instanceId.
Initial state
stateSchema defines the default document shape. On instance creation, the runtime initializes __yoke_state from the schema default (typically via Zod .default() or empty object parsing).
Storage backends
YOKE_STORAGE | Persistence |
|---|---|
pg_sqlite | BYTEA in actor_sqlite table |
s3 | Object at {prefix}/instances/{id}/state.db |
SDK API is identical. See Storage.
Portal state
Portals do not have ctx.state or ctx.sqlite. Store portal-adjacent durable data in an actor instance or Postgres via conversations API.
Patterns
Use ctx.state.update() for simple counters and flags — atomic, typed, minimal boilerplate.