---
title: Durable Objects
description: Model long-lived stateful entities as workflows that persist state across requests.
type: guide
summary: Build a durable counter or session object whose state survives restarts by using a workflow's event log as the persistence layer.
---

# Durable Objects



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

## 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.

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

const counterAction = defineHook({ // [!code highlight]
  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; // [!code highlight]

    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:

```typescript lineNumbers
import { resumeHook } from "workflow/api";

export async function POST(request: Request) {
  const { runId, type, amount } = await request.json();
  await resumeHook(`counter:${runId}`, { type, amount }); // [!code highlight]
  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.

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

const messageHook = defineHook({ // [!code highlight]
  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; // [!code highlight]

    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"`](/docs/api-reference/workflow/use-workflow) — declares the orchestrator function
* [`"use step"`](/docs/api-reference/workflow/use-step) — marks functions for durable execution
* [`defineHook`](/docs/api-reference/workflow/define-hook) — type-safe hook for receiving external method calls
* [`getWorkflowMetadata`](/docs/api-reference/workflow/get-workflow-metadata) — access the run ID for deterministic hook tokens
* [`resumeHook`](/docs/api-reference/workflow-api/resume-hook) — invoke a method on the durable object from an API route


## Sitemap
[Overview of all docs pages](/sitemap.md)
