Yoke.sh
SDK

defineRuntime

Runtime builder — defineEvents, defineFor, inferenceStack, and the user package contract.

defineRuntime() returns a fluent runtime builder that registers events, portals, actors, and an optional inference stack. Call .build() to produce a RuntimeDefinition the worker loads at boot.

Basic shape

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({
    role: z.string(),
    lang: z.string().optional(),
  }),
});

export default defineRuntime()
  .events(events)
  .portals(({ events }) => {
    const { portal } = defineFor(events);
    return {
      slack: portal({
        interface: {
          async sendMessage(channel: string, text: string) {
            // ...
          },
        },
      }),
    };
  })
  .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() }),
        async process(ctx, event) {
          // ctx.portals.slack is fully typed
        },
      }),
    };
  })
  .build();

RuntimeBuilder methods

.events(map)

Register event type → Zod payload schemas. Merges with any previously registered events.

const events = defineEvents({
  "order.created": z.object({ orderId: z.string() }),
});

defineRuntime()
  .events(events)
  .events({
    "order.shipped": z.object({ orderId: z.string(), tracking: z.string() }),
  });

Returns a builder with an expanded EventMap type used for typed ctx.append() and process() event narrowing.

.inferenceStack(stack)

Attach an LLM routing configuration used by ctx.llm.infer() and agent processors. Without this, agent inference throws at runtime.

import { defineInferenceStack } from "@yoke/sdk";

const stack = defineInferenceStack({
  models: {
    "claude-sonnet": { id: "claude-sonnet-4-20250514", contextWindow: 200_000 },
  },
  providers: {
    anthropic: anthropicProvider({ apiKey: process.env.ANTHROPIC_API_KEY! }),
  },
  routing: {
    "claude-sonnet": ["anthropic"],
  },
  defaultModel: "claude-sonnet",
});

export default defineRuntime()
  .events(events)
  .inferenceStack(stack)
  // ...
  .build();

InferenceStackConfig fields:

FieldTypePurpose
modelsRecord<string, Model>Logical model ids → provider model metadata
providersRecord<string, LlmProvider>Named provider implementations
routingRecord<string, string[]>Model id → ordered provider ids (round-robin failover)
defaultModelstring?Default when callers omit runConfig.model

.agents(fn)

Register high-level agent definitions. The SDK expands each into the standard actor stack (ingestion, egress, processor, optional follow-up and guardian actors).

.agents(() => ({
  support: defineAgent({
    name: "support-bot",
    model: "local-tool-loop",
    tools: [echoToolchain],
    followUp: false,
    guardianAngel: false,
  }),
}))

The callback receives { events, inferenceStack }. Returns a map of agent keys → defineAgent() inputs. Agent refs (agents.support) are available to .chatPlatforms().

.chatPlatforms(fn)

Register chat platform integrations. Each expands to a portal (webhook ingress + outbound API) and a bridge actor that wires the platform adapter to an agent via createAgent / joinConversation.

.chatPlatforms(({ agents }) => ({
  slack: defineChatPlatform({
    adapter: createStubChatAdapter("slack"),
    agent: agents.support,
    webhookPath: "/webhooks/slack",
  }),
}))

Requires .agents() (or manual agent refs) when binding agent. See Agents for chat platform integration patterns.

.bootstrap(fn)

One-shot boot hook — provision instances with stable ids. Must complete quickly; no long-running loops.

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

BootstrapCtx exposes ctx.instances and ctx.append(). Idempotent by instance id. Runs after portal singleton rows exist, before HTTP routes mount.

.portals(fn)

Register portal definitions. The callback receives { events } — the accumulated event map.

.portals(({ events }) => {
  const { portal } = defineFor(events);
  return {
    slack: portal({ /* PortalDefinition */ }),
    webhook: portal({ /* ... */ }),
  };
})

Updates builder types:

  • InferPortalRegistry — registered portal names
  • InferPortalApis — portal interface types available in .actors() as portals

.actors(fn)

Register actor definitions. The callback receives { events, portals }.

.actors(({ events, portals }) => {
  const { actor } = defineFor(events, portals);
  return {
    coder: actor({ /* ActorDefinition */ }),
    router: actor({ /* ... */ }),
  };
})

Updates InferActorRegistry with typed config/state per actor name.

.portal(name, definition) / .actor(name, definition)

Incremental registration without callbacks. Equivalent to merging one definition at a time:

defineRuntime()
  .events(events)
  .portal("slack", slackDefinition)
  .actor("coder", coderDefinition)
  .build();

Prefer .portals() / .actors() for type inference across the registry.

.build()

Returns RuntimeDefinition:

type RuntimeDefinition = {
  actors: ActorRegistry;
  portals: PortalRegistry;
  events: EventMap;
  inferenceStack?: InferenceStackConfig;
  agents?: Record<string, AgentDefinition>;
  chatPlatforms?: Record<string, ChatPlatformDefinitionInput>;
  bootstrap?: (ctx: BootstrapCtx) => Promise<void>;
};

The worker reads this object (or calls .build() on a builder export) to register specs, event schemas, and portal routes.

defineEvents

defineEvents() preserves literal event type keys for TypeScript inference:

const events = defineEvents({
  "work.assigned": z.object({ lang: z.string() }),
});

// events is typed as { "work.assigned": ZodObject<...> }

Event types not in the map still append at runtime (EventInput fallback), but known types get payload checking in the editor and Zod validation in the worker.

defineFor

Scoped factories that omit the redundant events field inside runtime callbacks:

const { actor, portal } = defineFor(events, portals);
//            ^ optional — pass portals in .actors() for typed ctx.portals.*
FactoryWrapsInjects
actor(input)defineActorevents from closure
portal(input)definePortalevents from closure

Standalone definitions outside the builder still use defineActor({ events, ... }) / definePortal({ events, ... }) directly.

Type parameters

The builder carries generic state through the chain:

RuntimeBuilder<
  TActors,      // registered actor registrations
  TPortals,     // registered portal registrations
  TEventMap,    // merged event schemas
  TPortalApis,  // portal interface map for actors
  TInferenceStack
>

Utility types:

TypePurpose
Registry<TActors, TPortals>{ actors, portals } pair
RuntimeHandleLive runtime with .portals client map and .stop()
EmptyRegistryInitial builder state
InferPortalClients<TPortals>Portal API types from registrations

User package contract

  1. File: yoke.ts at package root.
  2. Export: default export must be a defineRuntime() builder or its .build() result.
  3. Validation: actor configSchema / stateSchema and portal configSchema are validated when instances are created.
  4. Boot: Deno worker imports the module, extracts actor specs (get_actor_specs), portal routes (list_portal_routes), and event schemas for Zod parsing.
// Valid — builder (worker calls .build())
export default defineRuntime().events(events).actors(...);

// Valid — definition object
export default defineRuntime().events(events).actors(...).build();

inferenceStack in actor code

Once configured, actors and portals call:

async process(ctx, event) {
  const messages = await ctx.llm.infer(
    [{ type: "message", role: "user", body: { text: "Hello" } }],
    { model: "claude-sonnet", temperature: 0.2 },
  );
}

See Context API — LLM client for InferRunConfig fields.

On this page