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 executiondefineHook— type-safe hook for receiving external method callsgetWorkflowMetadata— access the run ID for deterministic hook tokensresumeHook— invoke a method on the durable object from an API route