Skip to content
PricingBlog
✨ Markdown

Writing handlers

The handler is the function that runs each time an entity wakes. It receives a HandlerContext and a WakeEvent describing what triggered the invocation.

Signature

ts
handler(ctx: HandlerContext, wake: WakeEvent) => void | Promise<void>

HandlerContext

ts
interface HandlerContext<TState extends StateProxy = StateProxy> {
  firstWake: boolean
  tags: Readonly<EntityTags>
  entityUrl: string
  entityType: string
  args: Readonly<Record<string, unknown>>
  db: EntityStreamDBWithActions
  state: TState
  events: Array<ChangeEvent>
  actions: Record<string, (...args: unknown[]) => unknown>
  electricTools: AgentTool[]
  useAgent: (config: AgentConfig) => AgentHandle
  useContext: (config: UseContextConfig) => void
  timelineMessages: (opts?: TimelineProjectionOpts) => Array<TimestampedMessage>
  insertContext: (id: string, entry: ContextEntryInput) => void
  removeContext: (id: string) => void
  getContext: (id: string) => ContextEntry | undefined
  listContext: () => Array<ContextEntry>
  agent: AgentHandle
  spawn: (
    type: string,
    id: string,
    args?: Record<string, unknown>,
    opts?: {
      initialMessage?: unknown
      wake?: Wake
      tags?: Record<string, string>
      observe?: boolean
    }
  ) => Promise<EntityHandle>
  observe: (
    source: ObservationSource,
    opts?: { wake?: Wake }
  ) => Promise<EntityHandle | SharedStateHandle | ObservationHandle>
  mkdb: <T extends SharedStateSchemaMap>(
    id: string,
    schema: T
  ) => SharedStateHandle<T>
  send: (
    entityUrl: string,
    payload: unknown,
    opts?: { type?: string; afterMs?: number }
  ) => void
  recordRun: () => RunHandle
  setTag: (key: string, value: string) => Promise<void>
  removeTag: (key: string) => Promise<void>
  sleep: () => void
}

Property reference

PropertyDescription
firstWaketrue during the initial setup pass while the entity has no persisted manifest entries. Use state checks for one-time plain state initialization.
tagsEntity tags -- key/value metadata associated with this entity.
entityUrlThe entity's URL path, e.g. "/assistant/my-chat".
entityTypeThe registered type name, e.g. "assistant".
argsArguments passed when the entity was spawned. Immutable.
dbThe entity's stream database. Use db.actions for writes and db.collections for reads.
stateProxy object keyed by collection name. Each property is a StateCollectionProxy.
eventsChange events that triggered this wake.
actionsCustom non-CRUD action functions from the entity definition's actions factory.
electricToolsHost-provided runtime-level tools to pass to useAgent when needed. May be empty.
useAgentConfigures the LLM agent. Returns an AgentHandle. See Configuring the agent.
useContextDeclares context sources with token budgets and cache tiers. See Context composition.
timelineMessagesProjects the entity timeline into LLM messages. See Context composition.
insertContextInserts a durable context entry. See Context composition.
removeContextRemoves a context entry by id.
getContextGets a context entry by id, or undefined if not found.
listContextLists all context entries.
agentThe configured agent handle. Call agent.run() to start the agent loop.
spawnCreates a child entity. See Spawning and coordinating.
observeConnects to another entity's stream or shared db. See Reactive observers and Shared state.
mkdbCreates a new shared state stream. See Shared state.
sendSends a message to another entity's inbox. Supports delayed delivery via afterMs.
recordRunRecords non-LLM work in the built-in runs collection so runFinished observers are woken.
setTagSets a tag on this entity.
removeTagRemoves a tag from this entity.
sleepReturns the entity to idle without re-waking.

WakeEvent

Describes what triggered this handler invocation.

ts
type WakeEvent = {
  source: string
  type: string
  fromOffset: number
  toOffset: number
  eventCount: number
  payload?: unknown
  summary?: string
  fullRef?: string
}
FieldDescription
sourceThe stream or entity that caused the wake.
typeThe wake type: "inbox" for inbox messages or "wake" for child completion, observed changes, cron, and timeouts.
fromOffsetStart offset of the events that triggered this wake.
toOffsetEnd offset of the events that triggered this wake.
eventCountNumber of new events since last wake.
payloadOptional payload from the trigger event.
summaryOptional human-readable summary.
fullRefOptional full reference string for the trigger.

Typical handler pattern

Most LLM handlers follow the same structure: initialize missing state idempotently, configure the agent, run the agent.

ts
registry.define("assistant", {
  description: "A general-purpose assistant",
  state: {
    status: { primaryKey: "key" },
  },

  async handler(ctx) {
    if (!ctx.db.collections.status.get("current")) {
      ctx.db.actions.status_insert({ row: { key: "current", value: "idle" } })
    }

    ctx.useAgent({
      systemPrompt: "You are a helpful assistant.",
      model: "claude-sonnet-4-5-20250929",
      tools: [...ctx.electricTools],
    })
    await ctx.agent.run()
  },
})

AgentConfig

Passed to ctx.useAgent():

ts
interface AgentConfig {
  systemPrompt: string
  model: string | Model<any>
  provider?: KnownProvider
  tools: AgentTool[]
  streamFn?: StreamFn
  getApiKey?: (provider: string) => Promise<string | undefined> | string | undefined
  onPayload?: SimpleStreamOptions["onPayload"]
  testResponses?: string[] | TestResponseFn
}

firstWake and initialization

ctx.firstWake is true during the initial setup pass while the entity has no persisted manifest entries. It is useful for setup that creates manifest-backed resources such as ctx.spawn(), ctx.observe(), ctx.mkdb(), context entries, or schedules.

For plain state rows, prefer checking the collection itself so initialization stays idempotent even for entities that do not create manifest entries:

ts
async handler(ctx) {
  if (!ctx.db.collections.status.get("current")) {
    ctx.db.actions.status_insert({ row: { key: 'current', value: 'idle' } })
  }
  if (!ctx.db.collections.counters.get("runs")) {
    ctx.db.actions.counters_insert({ row: { key: 'runs', value: 0 } })
  }
  // ...
}

After an entity persists manifest entries, subsequent wakes set firstWake to false.

sleep

Call ctx.sleep() to return the entity to idle without triggering a re-wake. The handler exits and the entity waits for the next external event.

ts
async handler(ctx, wake) {
  if (wake.type === 'some-condition') {
    // Nothing to do right now
    ctx.sleep()
    return
  }
  // Otherwise, run the agent
  ctx.useAgent({ ... })
  await ctx.agent.run()
}

recordRun

Call ctx.recordRun() when a handler does work without ctx.agent.run() but still needs to publish run lifecycle events. This is how non-LLM entities can wake parents observing them with wake: "runFinished".

ts
async handler(ctx) {
  const run = ctx.recordRun()
  try {
    const result = await runExternalJob()
    run.attachResponse(result.summary)
    run.end({ status: "completed" })
  } catch (error) {
    run.end({ status: "failed", finishReason: "error" })
    throw error
  }
}

Using spawn args

Arguments passed at spawn time are available as ctx.args. This is how you parameterize entity behavior:

ts
// Spawning side
const child = await ctx.spawn('worker', 'analysis-1', {
  systemPrompt: 'You are an analyst.',
  tools: ['read'],
})

// Worker handler
async handler(ctx) {
  const { systemPrompt } = ctx.args as { systemPrompt: string }
  ctx.useAgent({
    systemPrompt,
    model: 'claude-sonnet-4-5-20250929',
    tools: [...ctx.electricTools],
  })
  await ctx.agent.run()
}

Adding custom tools

Combine ctx.electricTools with custom tools:

ts
async handler(ctx) {
  const myTool: AgentTool = {
    name: 'lookup',
    label: 'Lookup',
    description: 'Looks up a value by key',
    parameters: Type.Object({
      key: Type.String({ description: 'The key to look up' }),
    }),
    execute: async (_toolCallId, params) => {
      const { key } = params as { key: string }
      const row = ctx.db.collections.kv?.get(key)
      return {
        content: [{ type: 'text', text: row ? JSON.stringify(row) : 'Not found' }],
        details: {},
      }
    },
  }

  ctx.useAgent({
    systemPrompt: 'You are an assistant with lookup capabilities.',
    model: 'claude-sonnet-4-5-20250929',
    tools: [...ctx.electricTools, myTool],
  })
  await ctx.agent.run()
}

Sending messages

Use ctx.send() to deliver a message to another entity's inbox:

ts
ctx.send("/worker/task-1", { action: "process", data: payload })
ctx.send("/worker/task-1", payload, { type: "custom_type" })

The target entity will be woken to process the message.