Overview

Durable Objects

Model long-lived stateful entities as workflows that persist state across requests.

This is an advanced guide. It dives into workflow internals and is not required reading to use workflow.

The Idea

A workflow's event log already records every step result and replays them to reconstruct state. This is the same property that makes an "object" durable — its fields survive cold starts, crashes, and redeployments. Instead of using a workflow to model a process, you can use one to model an entity with methods.

Each "method call" is a hook that the object's workflow loop awaits. External callers resume the hook with a payload describing the operation. The workflow applies the operation, updates its internal state, and waits for the next call.

Pattern: Durable Counter

A counter that persists its value without a database. Each increment/decrement is recorded in the event log.

import { defineHook, getWorkflowMetadata } from "workflow";
import { z } from "zod";

const counterAction = defineHook({
  schema: z.object({
    type: z.enum(["increment", "decrement", "get"]),
    amount: z.number().default(1),
  }),
});

export async function durableCounter() {
  "use workflow";

  let count = 0;
  const { workflowRunId } = getWorkflowMetadata();

  while (true) {
    const hook = counterAction.create({ token: `counter:${workflowRunId}` });
    const action = await hook;

    switch (action.type) {
      case "increment":
        count += action.amount;
        await recordState(count);
        break;
      case "decrement":
        count -= action.amount;
        await recordState(count);
        break;
      case "get":
        await emitValue(count);
        break;
    }
  }
}

async function recordState(count: number) {
  "use step";
  // Step records the state transition in the event log.
  // On replay, the step result restores `count` without re-executing.
  return count;
}

async function emitValue(count: number) {
  "use step";
  return { count };
}

Calling the Object

From an API route, resume the hook to "invoke a method" on the durable object:

import { resumeHook } from "workflow/api";

export async function POST(request: Request) {
  "use step";

  const { runId, type, amount } = await request.json();
  await resumeHook(`counter:${runId}`, { type, amount });
  return Response.json({ ok: true });
}

Pattern: Durable Session

A chat session where conversation history is the durable state. Each user message is a hook event; the workflow accumulates messages and generates responses.

import { defineHook, getWritable, getWorkflowMetadata } from "workflow";
import { DurableAgent } from "@workflow/ai/agent";
import { anthropic } from "@workflow/ai/providers/anthropic";
import { z } from "zod";
import type { UIMessageChunk, ModelMessage } from "ai";

const messageHook = defineHook({
  schema: z.object({
    role: z.literal("user"),
    content: z.string(),
  }),
});

export async function durableSession() {
  "use workflow";

  const writable = getWritable<UIMessageChunk>();
  const { workflowRunId: runId } = getWorkflowMetadata();
  const messages: ModelMessage[] = [];

  const agent = new DurableAgent({
    model: anthropic("claude-sonnet-4-20250514"),
    instructions: "You are a helpful assistant.",
  });

  while (true) {
    const hook = messageHook.create({ token: `session:${runId}` });
    const userMessage = await hook;

    messages.push({
      role: userMessage.role,
      content: userMessage.content,
    });

    await agent.stream({ messages, writable });
  }
}

When to Use This

  • Entity-per-workflow: Each user, document, or device gets its own workflow run. The run ID is the entity ID.
  • No external database needed: State lives in the event log. Reads replay from the log; writes append to it.
  • Automatic consistency: Only one execution runs at a time per workflow run, so there are no race conditions on the entity's state.

Trade-offs

  • Read latency: Accessing current state requires replaying the event log (or caching the last known state in a step result).
  • Not a replacement for databases: If you need to query across entities (e.g., "all counters above 100"), you still need a database. Durable objects are for single-entity state.
  • Log growth: Long-lived objects accumulate large event logs. Consider periodic "snapshot" steps that checkpoint the full state.

Key APIs

  • "use workflow" — declares the orchestrator function
  • "use step" — marks functions for durable execution
  • defineHook — type-safe hook for receiving external method calls
  • getWorkflowMetadata — access the run ID for deterministic hook tokens
  • resumeHook — invoke a method on the durable object from an API route