Yoke.sh
SDK

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

MethodBehavior
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 throw

Concurrent 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_STORAGEPersistence
pg_sqliteBYTEA in actor_sqlite table
s3Object 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.

On this page