Yoke.sh
Concepts

Instances

Runtime instances — creation, config, filters, lifecycle, and who can spawn them.

Definitions are fixed at build time. Instances are dynamic runtime objects created from those definitions with per-instance config and filters.

Understanding this distinction is essential for building scalable Yoke systems.

Definition vs instance

defineRuntime()
  .actors(() => ({
    coder: actor({ configSchema, stateSchema, process }),  // DEFINITION
  }));

// At runtime:
await ctx.instances.create("coder", {                        // INSTANCE
  config: { model: "gpt-4o", lang: "typescript" },
  filter: { type: "work.assigned", where: { "payload.lang": "typescript" } },
});
DefinitionInstance
CountOne per name in yoke.tsUnlimited
StorageTypeScriptPostgres actor_instances / portal_instances
Identity"coder" (definition name)UUID instanceId
FilterN/A (on instance)Declarative or function
SQLiteSchema in stateSchemaProvisioned per instance

Portal definitions always have one singleton instance per tenant. Actor definitions can have many.

Creating instances

From actor or portal code

const handle = await ctx.instances.create("coder", {
  config: { model: "claude-sonnet", lang: "rust" },
  filter: { type: "work.assigned", where: { "payload.lang": "rust" } },
});

console.log(handle.id); // instance UUID

config is validated against the definition's configSchema. filter determines stream subscription.

From bootstrap()

Packages typically provision fixed topology in defineRuntime().bootstrap() using stable instance ids:

.bootstrap(async (ctx) => {
  await ctx.instances.create("router", {
    id: "router",
    config: { channels: ["#general"] },
    filter: { type: "slack.message.received" },
  });
})

Re-running bootstrap() is safe — creation is idempotent by id.

From HTTP / CLI

Multi-tenant control plane and executor APIs can create instances externally. The same Postgres rows and instance.created events are emitted.

Instance options

type CreateInstanceOpts = {
  id: string;  // required — idempotent key for this instance
  config: TConfig;
  filter?: DeclarativeFilter | FunctionFilter;
  typeHint?: string;  // narrows slow-path function filter evaluation
};

Declarative filters

Persisted in Postgres, indexed for fast fan-out:

filter: { type: "conversation.message.posted" }
filter: { type: "work.*" }
filter: {
  type: "work.assigned",
  where: { "payload.lang": "typescript" },
}

Function filters

Require filterFromConfig on the definition:

await ctx.instances.create("coder", {
  config: { lang: "go" },
  filterKind: "function",
});

On boot/restart, the runtime calls filterFromConfig(config) to rebuild the function. Without the factory, persisted function-filter instances exist but receive no events.

Lifecycle

create → active → destroy
EventStream typeWhen
Createdinstance.createdRow inserted, SQLite provisioned, filter registered
Destroyedinstance.destroyedonDestroy called, queue cleared, rows deleted
await ctx.instances.destroy(instanceId);

Destroy is permanent — SQLite blob deleted, queue rows removed, filter deregistered.

Instance record in Postgres

actor_instances stores:

ColumnPurpose
idInstance UUID
definition_namee.g. "coder"
configJSON blob
filter_kinddeclarative or function
filterSerialized declarative filter or config for function rebuild
queue_policySerialized mode + concurrency
worker_idHorizontal scaling assignment
tenant_idMulti-tenant isolation
statusactive, etc.

On restart, the runtime loads all instance rows, rebuilds filters, provisions SQLite from actor_sqlite or S3, and resumes drain loops.

Worker assignment

In multi-worker deployments, new instances inherit the creating worker's worker_id. Orphan instances (worker_id IS NULL) are claimed on worker startup.

Only the owning worker drains an instance's queue. See Scaling.

Agent instances

Agent bootstrap creates multiple instances per join:

const agent = await createAgent(ctx, { name: "support-bot" });
// → { actorId, egressInstanceId }

const join = await joinConversation(ctx, agent, room.id, { model: "gpt-4o" });
// → { ingestionInstanceId, processorInstanceId, monologueId, ... }

Each join stint gets fresh ingestion and processor instances. One egress instance per agent routes replies across all forum joins.

sqliteStoreId

Advanced: actors can resolve a logical SQLite store segment per invocation via sqliteStoreId on the definition. Default is instanceId. Used when one instance manages multiple logical state partitions.

On this page