workflow 5.0.0-beta.1 → 5.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +4 -4
  2. package/dist/api-workflow.js +1 -1
  3. package/dist/api.js +1 -1
  4. package/dist/astro.js +1 -1
  5. package/dist/index.js +1 -1
  6. package/dist/internal/builtins.js +1 -1
  7. package/dist/internal/class-serialization.js +1 -1
  8. package/dist/internal/errors.js +1 -1
  9. package/dist/nest.js +1 -1
  10. package/dist/next.cjs +1 -1
  11. package/dist/nitro.js +1 -1
  12. package/dist/nuxt.js +1 -1
  13. package/dist/observability.js +1 -1
  14. package/dist/runtime.js +1 -1
  15. package/dist/stdlib.js +1 -1
  16. package/dist/sveltekit.js +1 -1
  17. package/dist/typescript-plugin.cjs +1 -1
  18. package/dist/vite.js +1 -1
  19. package/dist/workflow.js +1 -1
  20. package/docs/ai/resumable-streams.mdx +1 -1
  21. package/docs/api-reference/workflow/create-webhook.mdx +37 -18
  22. package/docs/api-reference/workflow/get-workflow-metadata.mdx +34 -0
  23. package/docs/api-reference/workflow-ai/durable-agent.mdx +0 -4
  24. package/docs/api-reference/workflow-ai/index.mdx +0 -5
  25. package/docs/api-reference/workflow-ai/workflow-chat-transport.mdx +0 -4
  26. package/docs/cookbook/advanced/custom-serialization.mdx +168 -0
  27. package/docs/cookbook/advanced/durable-objects.mdx +148 -0
  28. package/docs/cookbook/advanced/isomorphic-packages.mdx +145 -0
  29. package/docs/cookbook/advanced/meta.json +10 -0
  30. package/docs/cookbook/advanced/publishing-libraries.mdx +279 -0
  31. package/docs/cookbook/advanced/serializable-steps.mdx +135 -0
  32. package/docs/cookbook/agent-patterns/durable-agent.mdx +191 -0
  33. package/docs/cookbook/agent-patterns/human-in-the-loop.mdx +278 -0
  34. package/docs/cookbook/agent-patterns/meta.json +10 -0
  35. package/docs/cookbook/agent-patterns/stop-workflow.mdx +216 -0
  36. package/docs/cookbook/agent-patterns/tool-orchestration.mdx +255 -0
  37. package/docs/cookbook/agent-patterns/tool-streaming.mdx +181 -0
  38. package/docs/cookbook/common-patterns/batching.mdx +179 -0
  39. package/docs/cookbook/common-patterns/child-workflows.mdx +372 -0
  40. package/docs/cookbook/common-patterns/content-router.mdx +207 -0
  41. package/docs/cookbook/common-patterns/fan-out.mdx +208 -0
  42. package/docs/cookbook/common-patterns/idempotency.mdx +107 -0
  43. package/docs/cookbook/common-patterns/meta.json +15 -0
  44. package/docs/cookbook/common-patterns/rate-limiting.mdx +228 -0
  45. package/docs/cookbook/common-patterns/saga.mdx +152 -0
  46. package/docs/cookbook/common-patterns/scheduling.mdx +249 -0
  47. package/docs/cookbook/common-patterns/webhooks.mdx +185 -0
  48. package/docs/cookbook/index.mdx +41 -0
  49. package/docs/cookbook/integrations/ai-sdk.mdx +204 -0
  50. package/docs/cookbook/integrations/chat-sdk.mdx +203 -0
  51. package/docs/cookbook/integrations/meta.json +4 -0
  52. package/docs/cookbook/integrations/sandbox.mdx +128 -0
  53. package/docs/cookbook/meta.json +5 -0
  54. package/docs/deploying/world/local-world.mdx +1 -1
  55. package/docs/deploying/world/postgres-world.mdx +1 -1
  56. package/docs/deploying/world/vercel-world.mdx +1 -1
  57. package/docs/errors/start-invalid-workflow-function.mdx +1 -1
  58. package/docs/getting-started/index.mdx +8 -1
  59. package/docs/getting-started/meta.json +2 -1
  60. package/docs/getting-started/python.mdx +165 -0
  61. package/docs/meta.json +1 -0
  62. package/docs/migration-guides/index.mdx +34 -0
  63. package/docs/migration-guides/meta.json +9 -0
  64. package/docs/migration-guides/migrating-from-aws-step-functions.mdx +311 -0
  65. package/docs/migration-guides/migrating-from-inngest.mdx +282 -0
  66. package/docs/migration-guides/migrating-from-temporal.mdx +284 -0
  67. package/docs/migration-guides/migrating-from-trigger-dev.mdx +296 -0
  68. package/package.json +13 -13
@@ -0,0 +1,185 @@
1
+ ---
2
+ title: Webhooks & External Callbacks
3
+ description: Receive HTTP callbacks from external services, process them durably, and respond inline.
4
+ type: guide
5
+ summary: Create webhook endpoints that your workflow can await, process incoming requests in steps, and respond to the caller — all within durable workflow context.
6
+ ---
7
+
8
+ Use webhooks when external services push events to your application via HTTP callbacks. The workflow creates a webhook URL, suspends with zero compute cost, and resumes when a request arrives.
9
+
10
+ ## When to use this
11
+
12
+ - Accepting callbacks from payment processors (Stripe, PayPal)
13
+ - Waiting for third-party verification or processing results
14
+ - Any integration where an external system calls you back asynchronously
15
+
16
+ ## Pattern: Processing webhook events
17
+
18
+ Create a webhook with manual response control, then iterate over incoming requests:
19
+
20
+ ```typescript
21
+ import { createWebhook, type RequestWithResponse } from "workflow";
22
+
23
+ declare function processEvent(request: RequestWithResponse): Promise<{ type: string }>; // @setup
24
+
25
+ export async function paymentWebhook(orderId: string) {
26
+ "use workflow";
27
+
28
+ const webhook = createWebhook({ respondWith: "manual" }); // [!code highlight]
29
+ // webhook.url is the URL to give to the external service
30
+
31
+ const ledger: { type: string }[] = [];
32
+
33
+ for await (const request of webhook) { // [!code highlight]
34
+ const entry = await processEvent(request);
35
+ ledger.push(entry);
36
+
37
+ // Break when we've received a terminal event
38
+ if (entry.type === "payment.succeeded" || entry.type === "refund.created") {
39
+ break;
40
+ }
41
+ }
42
+
43
+ return { orderId, webhookUrl: webhook.url, ledger, status: "settled" };
44
+ }
45
+ ```
46
+
47
+ ### Step function for processing
48
+
49
+ Each webhook request is processed in its own step, giving you full Node.js access for validation, database writes, and responding to the caller:
50
+
51
+ ```typescript
52
+ import { type RequestWithResponse } from "workflow";
53
+
54
+ async function processEvent(
55
+ request: RequestWithResponse
56
+ ): Promise<{ type: string }> {
57
+ "use step";
58
+
59
+ const body = await request.json().catch(() => ({}));
60
+ const type = body?.type ?? "unknown";
61
+
62
+ // Validate, process, and respond inline
63
+ if (type === "payment.succeeded") {
64
+ // Record the payment in your database
65
+ await request.respondWith(Response.json({ ack: true, action: "captured" })); // [!code highlight]
66
+ } else if (type === "payment.failed") {
67
+ await request.respondWith(Response.json({ ack: true, action: "flagged" }));
68
+ } else {
69
+ await request.respondWith(Response.json({ ack: true, action: "ignored" }));
70
+ }
71
+
72
+ return { type };
73
+ }
74
+ ```
75
+
76
+ ## Pattern: Async request-reply with timeout
77
+
78
+ Submit a request to an external service, pass it your webhook URL, then race the callback against a deadline:
79
+
80
+ ```typescript
81
+ import { createWebhook, sleep, FatalError, type RequestWithResponse } from "workflow";
82
+
83
+ export async function asyncVerification(documentId: string) {
84
+ "use workflow";
85
+
86
+ const webhook = createWebhook({ respondWith: "manual" });
87
+
88
+ // Submit to vendor, passing our webhook URL for the callback
89
+ await submitToVendor(documentId, webhook.url);
90
+
91
+ // Race: wait for callback OR timeout after 30 seconds
92
+ const result = await Promise.race([ // [!code highlight]
93
+ (async () => {
94
+ for await (const request of webhook) {
95
+ const body = await processCallback(request);
96
+ return body;
97
+ }
98
+ throw new FatalError("Webhook closed without callback");
99
+ })(),
100
+ sleep("30s").then(() => ({ status: "timed_out" as const })), // [!code highlight]
101
+ ]);
102
+
103
+ return { documentId, ...result };
104
+ }
105
+
106
+ async function submitToVendor(documentId: string, callbackUrl: string): Promise<void> {
107
+ "use step";
108
+ await fetch("https://vendor.example.com/verify", {
109
+ method: "POST",
110
+ body: JSON.stringify({ documentId, callbackUrl }),
111
+ });
112
+ }
113
+
114
+ async function processCallback(
115
+ request: RequestWithResponse
116
+ ): Promise<{ status: string; details: string }> {
117
+ "use step";
118
+ const body = await request.json();
119
+ await request.respondWith(Response.json({ ack: true }));
120
+ return {
121
+ status: body.approved ? "verified" : "rejected",
122
+ details: body.details ?? body.reason ?? "",
123
+ };
124
+ }
125
+ ```
126
+
127
+ ## Pattern: Large payload by reference
128
+
129
+ When payloads are too large to serialize into the event log, pass a lightweight reference (a "claim check") instead. Use a hook to signal when the data is ready:
130
+
131
+ ```typescript
132
+ import { defineHook } from "workflow";
133
+
134
+ export const blobReady = defineHook<{ blobToken: string }>(); // [!code highlight]
135
+
136
+ export async function importLargeFile(importId: string) {
137
+ "use workflow";
138
+
139
+ // Suspend until the external system signals the blob is uploaded
140
+ const { blobToken } = await blobReady.create({ token: `upload:${importId}` }); // [!code highlight]
141
+
142
+ // Process by reference -- the full payload never enters the event log
143
+ await processBlob(blobToken);
144
+
145
+ return { importId, blobToken, status: "indexed" };
146
+ }
147
+
148
+ async function processBlob(blobToken: string): Promise<void> {
149
+ "use step";
150
+ // Fetch the blob using the token, process it
151
+ const res = await fetch(`https://storage.example.com/blobs/${blobToken}`);
152
+ const data = await res.arrayBuffer();
153
+ // Index, transform, or store the data
154
+ }
155
+ ```
156
+
157
+ Resume from an API route when the upload completes:
158
+
159
+ ```typescript
160
+ import { resumeHook } from "workflow/api";
161
+
162
+ // POST /api/upload-complete
163
+ export async function POST(request: Request) {
164
+ const { importId, blobToken } = await request.json();
165
+ await resumeHook(`upload:${importId}`, { blobToken }); // [!code highlight]
166
+ return Response.json({ ok: true });
167
+ }
168
+ ```
169
+
170
+ ## Tips
171
+
172
+ - **`respondWith: "manual"`** gives you control over the HTTP response from inside a step. Use this when you need to validate the request before responding.
173
+ - **`for await` on a webhook** lets you process multiple events from the same URL. Use `break` to stop listening after a terminal event.
174
+ - **Webhooks auto-generate URLs** at `/.well-known/workflow/v1/webhook/:token`. Pass this URL to external services.
175
+ - **Race webhooks against `sleep()`** for deadlines. If the callback doesn't arrive in time, the workflow can take a fallback action.
176
+ - **For large payloads**, use a hook + reference token instead of passing the data through the workflow. The event log serializes all step inputs/outputs, so large payloads hurt performance.
177
+
178
+ ## Key APIs
179
+
180
+ - [`"use workflow"`](/docs/foundations/workflows-and-steps) -- marks the orchestrator function
181
+ - [`"use step"`](/docs/foundations/workflows-and-steps) -- marks functions with full Node.js access
182
+ - [`createWebhook()`](/docs/api-reference/workflow/create-webhook) -- creates an HTTP endpoint the workflow can await
183
+ - [`defineHook()`](/docs/api-reference/workflow/define-hook) -- creates a typed hook for signal-based patterns
184
+ - [`sleep()`](/docs/api-reference/workflow/sleep) -- durable timer for deadlines
185
+ - [`FatalError`](/docs/api-reference/workflow/fatal-error) -- prevents retry on permanent failures
@@ -0,0 +1,41 @@
1
+ ---
2
+ title: Cookbook
3
+ description: Best-practice workflow patterns with copy-paste code examples.
4
+ type: overview
5
+ ---
6
+
7
+ A curated collection of workflow patterns with clean, copy-paste code examples for real use cases.
8
+
9
+ ## Common Patterns
10
+
11
+ - [**Saga**](/cookbook/common-patterns/saga) — Coordinate multi-step transactions with automatic rollback when a step fails
12
+ - [**Batching**](/cookbook/common-patterns/batching) — Process large collections in parallel batches with failure isolation
13
+ - [**Rate Limiting**](/cookbook/common-patterns/rate-limiting) — Handle 429 responses and transient failures with RetryableError and backoff
14
+ - [**Fan-Out**](/cookbook/common-patterns/fan-out) — Send to multiple channels in parallel with independent failure handling
15
+ - [**Scheduling**](/cookbook/common-patterns/scheduling) — Use durable sleep to schedule actions minutes, hours, or weeks ahead
16
+ - [**Idempotency**](/cookbook/common-patterns/idempotency) — Ensure side effects happen exactly once, even when steps retry
17
+ - [**Webhooks**](/cookbook/common-patterns/webhooks) — Receive HTTP callbacks from external services and process them durably
18
+ - [**Conditional Routing**](/cookbook/common-patterns/content-router) — Route payloads to different step handlers based on content
19
+ - [**Child Workflows**](/cookbook/common-patterns/child-workflows) — Spawn and orchestrate child workflows from a parent
20
+
21
+ ## Agent Patterns
22
+
23
+ - [**Durable Agent**](/cookbook/agent-patterns/durable-agent) — Replace a stateless AI agent with one that survives crashes and retries tool calls
24
+ - [**Tool Streaming**](/cookbook/agent-patterns/tool-streaming) — Stream real-time progress updates from tools to the UI
25
+ - [**Human-in-the-Loop**](/cookbook/agent-patterns/human-in-the-loop) — Pause an agent for human approval, then resume based on the decision
26
+ - [**Tool Orchestration**](/cookbook/agent-patterns/tool-orchestration) — Choose between step-level and workflow-level tools, or combine both
27
+ - [**Stop Workflow**](/cookbook/agent-patterns/stop-workflow) — Gracefully cancel a running agent workflow using a hook signal
28
+
29
+ ## Integrations
30
+
31
+ - [**AI SDK**](/cookbook/integrations/ai-sdk) — Use AI SDK model providers, tool calling, and streaming inside durable workflows
32
+ - [**Chat SDK**](/cookbook/integrations/chat-sdk) — Build durable chat sessions with workflow persistence and AI SDK chat primitives
33
+ - [**Sandbox**](/cookbook/integrations/sandbox) — Orchestrate Vercel Sandbox lifecycle inside durable workflows
34
+
35
+ ## Advanced
36
+
37
+ - [**Serializable Steps**](/cookbook/advanced/serializable-steps) — Wrap non-serializable objects so they cross the workflow boundary
38
+ - [**Durable Objects**](/cookbook/advanced/durable-objects) — Model long-lived stateful entities as workflows
39
+ - [**Isomorphic Packages**](/cookbook/advanced/isomorphic-packages) — Publish packages that work inside and outside the workflow runtime
40
+ - [**Custom Serialization**](/cookbook/advanced/custom-serialization) — Make custom classes survive workflow serialization
41
+ - [**Publishing Libraries**](/cookbook/advanced/publishing-libraries) — Ship npm packages that export reusable workflow functions
@@ -0,0 +1,204 @@
1
+ ---
2
+ title: AI SDK
3
+ description: Use AI SDK model providers, tool calling, and streaming inside durable workflows.
4
+ type: guide
5
+ summary: Turn any AI SDK model call into a retryable, observable workflow step with built-in streaming.
6
+ related:
7
+ - /docs/ai
8
+ - /docs/ai/defining-tools
9
+ - /docs/ai/resumable-streams
10
+ - /docs/api-reference/workflow-ai/durable-agent
11
+ ---
12
+
13
+ Workflow SDK integrates with [AI SDK](https://ai-sdk.dev) through the `@workflow/ai` package. This turns your LLM calls and tool executions into durable, retryable steps with built-in streaming and observability.
14
+
15
+ ## What It Enables
16
+
17
+ - **Durable LLM calls** -- Model invocations become steps that survive crashes and cold starts
18
+ - **Any model provider** -- Use OpenAI, Anthropic, Google, Bedrock, or any AI SDK-compatible provider through [Vercel Gateway](https://vercel.com/docs/gateway) or direct provider configuration
19
+ - **Tool durability** -- Tool executions become steps with automatic retries and event logging
20
+ - **Resumable streaming** -- Clients reconnect mid-stream without losing data
21
+
22
+ ## When to Use
23
+
24
+ Use this integration when your application calls an LLM and needs:
25
+
26
+ - Reliability for long-running agent loops (multi-step tool calling)
27
+ - Automatic retry on transient model API failures
28
+ - Stream resumption after disconnects
29
+ - Observability into each model call and tool execution
30
+
31
+ ## DurableAgent with Model Providers
32
+
33
+ The `DurableAgent` wraps AI SDK's streaming interface. Pass any model string supported by [Vercel Gateway](https://vercel.com/docs/gateway) or a provider-specific model ID.
34
+
35
+ ```typescript title="workflows/research.ts" lineNumbers
36
+ import { DurableAgent } from "@workflow/ai/agent";
37
+ import { convertToModelMessages, type UIMessage, type UIMessageChunk } from "ai";
38
+ import { getWritable } from "workflow";
39
+ import z from "zod/v4";
40
+
41
+ async function searchWeb(input: { query: string }): Promise<{ results: string[] }> {
42
+ "use step";
43
+ const response = await fetch(
44
+ `https://api.example.com/search?q=${encodeURIComponent(input.query)}`
45
+ );
46
+ const data = await response.json();
47
+ return { results: data.items.map((item: { title: string }) => item.title) };
48
+ }
49
+
50
+ async function summarize(input: { text: string }): Promise<{ summary: string }> {
51
+ "use step";
52
+ // Each step is individually retried on failure
53
+ const response = await fetch("https://api.example.com/summarize", {
54
+ method: "POST",
55
+ body: JSON.stringify({ text: input.text }),
56
+ });
57
+ const data = await response.json();
58
+ return { summary: data.summary };
59
+ }
60
+
61
+ export async function researchAgent(messages: UIMessage[]) {
62
+ "use workflow";
63
+
64
+ const agent = new DurableAgent({ // [!code highlight]
65
+ model: "anthropic/claude-sonnet-4-20250514",
66
+ instructions: "You are a research assistant. Search the web and summarize findings.",
67
+ tools: {
68
+ searchWeb: {
69
+ description: "Search the web for information",
70
+ inputSchema: z.object({
71
+ query: z.string().describe("The search query"),
72
+ }),
73
+ execute: searchWeb,
74
+ },
75
+ summarize: {
76
+ description: "Summarize a block of text",
77
+ inputSchema: z.object({
78
+ text: z.string().describe("The text to summarize"),
79
+ }),
80
+ execute: summarize,
81
+ },
82
+ },
83
+ });
84
+
85
+ const result = await agent.stream({ // [!code highlight]
86
+ messages: await convertToModelMessages(messages),
87
+ writable: getWritable<UIMessageChunk>(),
88
+ });
89
+
90
+ return { messages: result.messages };
91
+ }
92
+ ```
93
+
94
+ ### Using Different Providers
95
+
96
+ #### Vercel Gateway (string model IDs)
97
+
98
+ All string model IDs route through [Vercel Gateway](https://vercel.com/docs/gateway). Switch providers by changing the model string -- no other code changes required.
99
+
100
+ {/* @skip-typecheck - illustrative snippets with intentional redeclarations */}
101
+ ```typescript
102
+ // All string model IDs route through Vercel Gateway
103
+ const agent = new DurableAgent({ model: "anthropic/claude-sonnet-4-20250514" });
104
+ const agent = new DurableAgent({ model: "openai/gpt-4o" });
105
+ const agent = new DurableAgent({ model: "google/gemini-2.5-pro" });
106
+ const agent = new DurableAgent({ model: "bedrock/claude-haiku-4-5-20251001-v1" });
107
+ ```
108
+
109
+ #### Direct Provider Access
110
+
111
+ Import from a provider package to bypass Gateway and connect to the provider directly.
112
+
113
+ ```typescript
114
+ import { DurableAgent } from "@workflow/ai/agent";
115
+ import { openai } from "@workflow/ai/openai";
116
+
117
+ const agent = new DurableAgent({ model: openai("gpt-4o") });
118
+ ```
119
+
120
+ ### Provider-Specific Options
121
+
122
+ Pass provider options for features like reasoning or extended thinking.
123
+
124
+ ```typescript
125
+ const agent = new DurableAgent({
126
+ model: "anthropic/claude-sonnet-4-20250514",
127
+ providerOptions: {
128
+ anthropic: { thinking: { type: "enabled", budgetTokens: 10000 } },
129
+ },
130
+ // ...tools and instructions
131
+ });
132
+ ```
133
+
134
+ ## Tool Functions with Steps
135
+
136
+ Tool `execute` functions can optionally include steps by using the `"use step"` directive. When a tool is **not** a step, it runs inside the workflow context and can modify workflow state directly. When a tool **is** marked with `"use step"`, it becomes a durable step with:
137
+
138
+ - **Automatic retries** -- If a tool fails (network error, API timeout), the framework retries it
139
+ - **Event logging** -- Inputs and outputs are recorded for observability and replay
140
+ - **Idempotency** -- On replay after a crash, completed steps return their cached result
141
+
142
+ ```typescript
143
+ async function bookFlight(input: {
144
+ origin: string;
145
+ destination: string;
146
+ date: string;
147
+ }): Promise<{ confirmationId: string }> {
148
+ "use step";
149
+ // This call is retried on transient failures and its result is persisted
150
+ const response = await fetch("https://api.airline.com/book", {
151
+ method: "POST",
152
+ headers: { "Content-Type": "application/json" },
153
+ body: JSON.stringify(input),
154
+ });
155
+ if (!response.ok) throw new Error(`Booking failed: ${response.status}`);
156
+ return response.json();
157
+ }
158
+ ```
159
+
160
+ ## Resumable Streaming
161
+
162
+ Use `WorkflowChatTransport` on the client to automatically reconnect to a workflow's stream if the connection drops.
163
+
164
+ ```typescript title="app/api/chat/route.ts" lineNumbers
165
+ import { createUIMessageStreamResponse } from "ai";
166
+ import { start } from "workflow/api";
167
+ import { researchAgent } from "@/workflows/research";
168
+
169
+ export async function POST(request: Request) {
170
+ const { messages } = await request.json();
171
+ const run = await start(researchAgent, [messages]); // [!code highlight]
172
+
173
+ return createUIMessageStreamResponse({
174
+ stream: run.readable, // [!code highlight]
175
+ headers: { "x-workflow-run-id": run.runId },
176
+ });
177
+ }
178
+ ```
179
+
180
+ ```typescript title="components/chat.tsx" lineNumbers
181
+ "use client";
182
+
183
+ import { useChat } from "@ai-sdk/react";
184
+ import { WorkflowChatTransport } from "@workflow/ai";
185
+
186
+ export function Chat() {
187
+ const chat = useChat({
188
+ transport: new WorkflowChatTransport({ // [!code highlight]
189
+ api: "/api/chat",
190
+ }),
191
+ });
192
+
193
+ // Standard useChat usage -- reconnection is handled automatically
194
+ return (
195
+ <div>
196
+ {chat.messages.map((m) => (
197
+ <div key={m.id}>{m.content}</div>
198
+ ))}
199
+ </div>
200
+ );
201
+ }
202
+ ```
203
+
204
+ See [Resumable Streams](/docs/ai/resumable-streams) for advanced options like `startIndex` and `prepareReconnectToStreamRequest`.
@@ -0,0 +1,203 @@
1
+ ---
2
+ title: Chat SDK
3
+ description: Build durable chat sessions by combining workflow persistence with AI SDK's chat primitives.
4
+ type: guide
5
+ summary: Use workflow hooks and streaming to create chat sessions that survive disconnects and server restarts.
6
+ related:
7
+ - /docs/ai/chat-session-modeling
8
+ - /docs/ai/resumable-streams
9
+ - /docs/ai/message-queueing
10
+ - /docs/api-reference/workflow-ai/durable-agent
11
+ - /docs/api-reference/workflow/define-hook
12
+ ---
13
+
14
+ AI SDK provides chat primitives (`useChat`, message types, streaming utilities) for building chat interfaces. Workflow SDK makes those chat sessions durable -- surviving disconnects, cold starts, and server restarts -- by persisting every message and LLM response as workflow events.
15
+
16
+ ## What It Enables
17
+
18
+ - **Durable chat history** -- Messages and responses are persisted in the workflow event log, not just client state
19
+ - **Resumable sessions** -- Users reconnect and pick up where they left off, even after server restarts
20
+ - **Multi-turn conversations** -- A single workflow manages an entire chat session with hook-based message injection
21
+ - **Server-side message queueing** -- Inject follow-up messages while the agent is still processing
22
+
23
+ ## When to Use
24
+
25
+ Use this pattern when your chat application needs:
26
+
27
+ - Persistence beyond the browser session
28
+ - Recovery from server failures mid-conversation
29
+ - Long-running agent sessions (minutes to hours)
30
+ - Server-driven message injection (system messages, external events)
31
+
32
+ ## Single-Turn: Stateless Sessions
33
+
34
+ Each user message starts a new workflow run. The client owns the message history and sends the full array with each request. This is the simplest pattern.
35
+
36
+ ```typescript title="workflows/chat.ts" lineNumbers
37
+ import { DurableAgent } from "@workflow/ai/agent";
38
+ import { convertToModelMessages, type UIMessage, type UIMessageChunk } from "ai";
39
+ import { getWritable } from "workflow";
40
+
41
+ export async function chat(messages: UIMessage[]) {
42
+ "use workflow";
43
+
44
+ const agent = new DurableAgent({
45
+ model: "anthropic/claude-sonnet-4-20250514",
46
+ instructions: "You are a helpful assistant.",
47
+ tools: { /* your tools here */ },
48
+ });
49
+
50
+ const result = await agent.stream({ // [!code highlight]
51
+ messages: await convertToModelMessages(messages),
52
+ writable: getWritable<UIMessageChunk>(),
53
+ });
54
+
55
+ return { messages: result.messages };
56
+ }
57
+ ```
58
+
59
+ ```typescript title="app/api/chat/route.ts" lineNumbers
60
+ import { createUIMessageStreamResponse } from "ai";
61
+ import { start } from "workflow/api";
62
+ import { chat } from "@/workflows/chat";
63
+
64
+ export async function POST(request: Request) {
65
+ const { messages } = await request.json();
66
+ const run = await start(chat, [messages]); // [!code highlight]
67
+
68
+ return createUIMessageStreamResponse({
69
+ stream: run.readable,
70
+ headers: { "x-workflow-run-id": run.runId },
71
+ });
72
+ }
73
+ ```
74
+
75
+ The client uses `WorkflowChatTransport` for automatic stream resumption.
76
+
77
+ ```typescript title="components/chat.tsx" lineNumbers
78
+ "use client";
79
+
80
+ import { useChat } from "@ai-sdk/react";
81
+ import { WorkflowChatTransport } from "@workflow/ai";
82
+
83
+ export function Chat() {
84
+ const chat = useChat({
85
+ transport: new WorkflowChatTransport({ api: "/api/chat" }), // [!code highlight]
86
+ });
87
+
88
+ return (
89
+ <div>
90
+ {chat.messages.map((m) => (
91
+ <div key={m.id}>{m.content}</div>
92
+ ))}
93
+ <form onSubmit={chat.handleSubmit}>
94
+ <input value={chat.input} onChange={chat.handleInputChange} />
95
+ </form>
96
+ </div>
97
+ );
98
+ }
99
+ ```
100
+
101
+ ## Multi-Turn: Durable Sessions
102
+
103
+ A single workflow manages the entire conversation. The workflow loops, waiting for new messages via a hook. This gives you server-side ownership of the full chat history.
104
+
105
+ ```typescript title="workflows/durable-chat.ts" lineNumbers
106
+ import { DurableAgent } from "@workflow/ai/agent";
107
+ import {
108
+ convertToModelMessages,
109
+ type UIMessage,
110
+ type UIMessageChunk,
111
+ } from "ai";
112
+ import { defineHook, getWritable, getWorkflowMetadata } from "workflow";
113
+ import { z } from "zod";
114
+
115
+ const chatMessageHook = defineHook({ // [!code highlight]
116
+ schema: z.object({
117
+ messages: z.array(z.any()),
118
+ }),
119
+ });
120
+
121
+ export async function durableChat(initialMessages: UIMessage[]) {
122
+ "use workflow";
123
+
124
+ const { workflowRunId } = getWorkflowMetadata();
125
+ let allMessages = await convertToModelMessages(initialMessages);
126
+
127
+ const agent = new DurableAgent({
128
+ model: "anthropic/claude-sonnet-4-20250514",
129
+ instructions: "You are a helpful assistant.",
130
+ tools: { /* your tools here */ },
131
+ });
132
+
133
+ // First turn
134
+ const firstResult = await agent.stream({
135
+ messages: allMessages,
136
+ writable: getWritable<UIMessageChunk>(),
137
+ preventClose: true,
138
+ });
139
+ allMessages = firstResult.messages;
140
+
141
+ // Subsequent turns -- wait for new messages via hook
142
+ while (true) {
143
+ const hook = chatMessageHook.create({ token: workflowRunId });
144
+ const { messages: newMessages } = await hook; // [!code highlight]
145
+
146
+ allMessages = [
147
+ ...allMessages,
148
+ ...await convertToModelMessages(newMessages),
149
+ ];
150
+
151
+ const result = await agent.stream({
152
+ messages: allMessages,
153
+ writable: getWritable<UIMessageChunk>(),
154
+ preventClose: true,
155
+ });
156
+ allMessages = result.messages;
157
+ }
158
+ }
159
+ ```
160
+
161
+ ### Multi-Turn API Routes
162
+
163
+ You need two routes: one to start the session, another to send follow-up messages.
164
+
165
+ ```typescript title="app/api/chat/route.ts" lineNumbers
166
+ import { createUIMessageStreamResponse } from "ai";
167
+ import { start } from "workflow/api";
168
+ import { durableChat } from "@/workflows/durable-chat";
169
+
170
+ export async function POST(request: Request) {
171
+ const { messages } = await request.json();
172
+ const run = await start(durableChat, [messages]); // [!code highlight]
173
+
174
+ return createUIMessageStreamResponse({
175
+ stream: run.readable,
176
+ headers: { "x-workflow-run-id": run.runId },
177
+ });
178
+ }
179
+ ```
180
+
181
+ ```typescript title="app/api/chat/follow-up/route.ts" lineNumbers
182
+ import { resumeHook } from "workflow/api";
183
+
184
+ export async function POST(request: Request) {
185
+ const { runId, messages } = await request.json();
186
+ await resumeHook(runId, { messages }); // [!code highlight]
187
+ return new Response("OK");
188
+ }
189
+ ```
190
+
191
+ ## Choosing a Pattern
192
+
193
+ | | Single-Turn | Multi-Turn |
194
+ |---|---|---|
195
+ | **State ownership** | Client | Server (workflow event log) |
196
+ | **Message injection** | Not needed | Via hooks |
197
+ | **Complexity** | Low | Medium |
198
+ | **Session duration** | Per-request | Minutes to hours |
199
+ | **Crash recovery** | Client resends full history | Workflow replays from event log |
200
+
201
+ Start with single-turn. Move to multi-turn when you need server-owned state, message injection from external sources, or sessions that outlive the browser tab.
202
+
203
+ See [Chat Session Modeling](/docs/ai/chat-session-modeling) for the full guide including multiplayer patterns and message queueing.
@@ -0,0 +1,4 @@
1
+ {
2
+ "title": "Integrations",
3
+ "pages": ["ai-sdk", "sandbox", "chat-sdk"]
4
+ }