workflow 5.0.0-beta.0 → 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.
- package/README.md +4 -4
- package/dist/api-workflow.d.ts +1 -3
- package/dist/api-workflow.d.ts.map +1 -1
- package/dist/api-workflow.js +2 -6
- package/dist/api.js +1 -1
- package/dist/astro.js +1 -1
- package/dist/index.js +1 -1
- package/dist/internal/builtins.js +1 -1
- package/dist/internal/class-serialization.js +1 -1
- package/dist/internal/errors.js +1 -1
- package/dist/nest.js +1 -1
- package/dist/next.cjs +4 -2
- package/dist/next.d.cts +1 -1
- package/dist/next.d.cts.map +1 -1
- package/dist/nitro.js +1 -1
- package/dist/nuxt.js +1 -1
- package/dist/observability.d.ts +1 -1
- package/dist/observability.js +2 -2
- package/dist/runtime.js +1 -1
- package/dist/stdlib.js +1 -1
- package/dist/sveltekit.js +1 -1
- package/dist/typescript-plugin.cjs +1 -1
- package/dist/vite.js +1 -1
- package/dist/workflow.js +1 -1
- package/docs/ai/resumable-streams.mdx +1 -1
- package/docs/api-reference/workflow/create-webhook.mdx +37 -18
- package/docs/api-reference/workflow/get-workflow-metadata.mdx +61 -0
- package/docs/api-reference/workflow-ai/durable-agent.mdx +0 -4
- package/docs/api-reference/workflow-ai/index.mdx +0 -5
- package/docs/api-reference/workflow-ai/workflow-chat-transport.mdx +0 -4
- package/docs/api-reference/workflow-api/get-world.mdx +6 -6
- package/docs/api-reference/workflow-api/index.mdx +1 -1
- package/docs/api-reference/workflow-api/world/index.mdx +2 -2
- package/docs/api-reference/workflow-api/world/observability.mdx +1 -1
- package/docs/api-reference/workflow-api/world/queue.mdx +1 -1
- package/docs/api-reference/workflow-api/world/storage.mdx +8 -8
- package/docs/api-reference/workflow-api/world/streams.mdx +38 -36
- package/docs/cookbook/advanced/custom-serialization.mdx +168 -0
- package/docs/cookbook/advanced/durable-objects.mdx +148 -0
- package/docs/cookbook/advanced/isomorphic-packages.mdx +145 -0
- package/docs/cookbook/advanced/meta.json +10 -0
- package/docs/cookbook/advanced/publishing-libraries.mdx +279 -0
- package/docs/cookbook/advanced/serializable-steps.mdx +135 -0
- package/docs/cookbook/agent-patterns/durable-agent.mdx +191 -0
- package/docs/cookbook/agent-patterns/human-in-the-loop.mdx +278 -0
- package/docs/cookbook/agent-patterns/meta.json +10 -0
- package/docs/cookbook/agent-patterns/stop-workflow.mdx +216 -0
- package/docs/cookbook/agent-patterns/tool-orchestration.mdx +255 -0
- package/docs/cookbook/agent-patterns/tool-streaming.mdx +181 -0
- package/docs/cookbook/common-patterns/batching.mdx +179 -0
- package/docs/cookbook/common-patterns/child-workflows.mdx +372 -0
- package/docs/cookbook/common-patterns/content-router.mdx +207 -0
- package/docs/cookbook/common-patterns/fan-out.mdx +208 -0
- package/docs/cookbook/common-patterns/idempotency.mdx +107 -0
- package/docs/cookbook/common-patterns/meta.json +15 -0
- package/docs/cookbook/common-patterns/rate-limiting.mdx +228 -0
- package/docs/cookbook/common-patterns/saga.mdx +152 -0
- package/docs/cookbook/common-patterns/scheduling.mdx +249 -0
- package/docs/cookbook/common-patterns/webhooks.mdx +185 -0
- package/docs/cookbook/index.mdx +41 -0
- package/docs/cookbook/integrations/ai-sdk.mdx +204 -0
- package/docs/cookbook/integrations/chat-sdk.mdx +203 -0
- package/docs/cookbook/integrations/meta.json +4 -0
- package/docs/cookbook/integrations/sandbox.mdx +128 -0
- package/docs/cookbook/meta.json +5 -0
- package/docs/deploying/building-a-world.mdx +45 -43
- package/docs/deploying/world/local-world.mdx +1 -1
- package/docs/deploying/world/postgres-world.mdx +10 -5
- package/docs/deploying/world/vercel-world.mdx +1 -1
- package/docs/errors/start-invalid-workflow-function.mdx +1 -1
- package/docs/getting-started/index.mdx +8 -1
- package/docs/getting-started/meta.json +2 -1
- package/docs/getting-started/next.mdx +24 -0
- package/docs/getting-started/python.mdx +165 -0
- package/docs/how-it-works/code-transform.mdx +6 -5
- package/docs/meta.json +1 -0
- package/docs/migration-guides/index.mdx +34 -0
- package/docs/migration-guides/meta.json +9 -0
- package/docs/migration-guides/migrating-from-aws-step-functions.mdx +311 -0
- package/docs/migration-guides/migrating-from-inngest.mdx +282 -0
- package/docs/migration-guides/migrating-from-temporal.mdx +284 -0
- package/docs/migration-guides/migrating-from-trigger-dev.mdx +296 -0
- package/package.json +13 -14
- package/dist/internal/private.d.ts +0 -6
- package/dist/internal/private.d.ts.map +0 -1
- package/dist/internal/private.js +0 -6
|
@@ -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,128 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Sandbox
|
|
3
|
+
description: Orchestrate Vercel Sandbox lifecycle -- creation, code execution, snapshotting -- inside durable workflows.
|
|
4
|
+
type: guide
|
|
5
|
+
summary: Use workflow steps to provision sandboxes, run code, and manage sandbox lifecycle with automatic cleanup on failure.
|
|
6
|
+
related:
|
|
7
|
+
- /docs/ai/defining-tools
|
|
8
|
+
- /docs/foundations/errors-and-retries
|
|
9
|
+
- /docs/api-reference/workflow-ai/durable-agent
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
[Vercel Sandbox](https://vercel.com/docs/sandbox) provides isolated code execution environments. The `@vercel/sandbox` package implements first-class support for the Workflow SDK -- the `Sandbox` class is serializable, and its methods (`create`, `runCommand`, `destroy`, etc.) implicitly run as steps. This means you can interact with sandboxes directly inside workflow functions without wrapping each operation in a separate `"use step"` function.
|
|
13
|
+
|
|
14
|
+
## What It Enables
|
|
15
|
+
|
|
16
|
+
- **Durable sandbox sessions** -- Sandbox provisioning and teardown survive cold starts
|
|
17
|
+
- **Automatic cleanup** -- Saga-style compensation ensures sandboxes are destroyed on failure
|
|
18
|
+
- **Multi-step code execution** -- Run a sequence of commands in the same sandbox with each step logged
|
|
19
|
+
- **Agent-driven sandboxes** -- Give your DurableAgent a tool that spins up sandboxes on demand
|
|
20
|
+
|
|
21
|
+
## When to Use
|
|
22
|
+
|
|
23
|
+
Use this integration when your workflow needs to:
|
|
24
|
+
|
|
25
|
+
- Execute user-provided or AI-generated code safely
|
|
26
|
+
- Run multi-step build/test pipelines in isolated environments
|
|
27
|
+
- Provision temporary environments for interactive sessions
|
|
28
|
+
- Snapshot sandbox state between steps for reproducibility
|
|
29
|
+
|
|
30
|
+
## Sandbox Lifecycle in a Workflow
|
|
31
|
+
|
|
32
|
+
Because `@vercel/sandbox` methods are implicit steps, each call is automatically persisted to the event log. If a failure occurs partway through, the workflow replays from where it left off.
|
|
33
|
+
|
|
34
|
+
```typescript title="workflows/sandbox-pipeline.ts" lineNumbers
|
|
35
|
+
import { Sandbox } from "@vercel/sandbox";
|
|
36
|
+
|
|
37
|
+
export async function sandboxPipeline(input: {
|
|
38
|
+
template: string;
|
|
39
|
+
commands: string[];
|
|
40
|
+
}) {
|
|
41
|
+
"use workflow";
|
|
42
|
+
|
|
43
|
+
const sandbox = await Sandbox.create({ template: input.template }); // [!code highlight]
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const results = [];
|
|
47
|
+
for (const command of input.commands) {
|
|
48
|
+
const result = await sandbox.runCommand(command); // [!code highlight]
|
|
49
|
+
results.push(result);
|
|
50
|
+
}
|
|
51
|
+
return { status: "completed", results };
|
|
52
|
+
} catch (error) {
|
|
53
|
+
await sandbox.destroy(); // [!code highlight]
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Sandbox as an Agent Tool
|
|
60
|
+
|
|
61
|
+
Give a DurableAgent the ability to create and use sandboxes. The agent decides when to spin up a sandbox, what code to run, and when to tear it down. Since sandbox methods are implicit steps, the tool execute functions can call them directly.
|
|
62
|
+
|
|
63
|
+
```typescript title="workflows/code-agent.ts" lineNumbers
|
|
64
|
+
import { Sandbox } from "@vercel/sandbox";
|
|
65
|
+
import { DurableAgent } from "@workflow/ai/agent";
|
|
66
|
+
import { convertToModelMessages, type UIMessage, type UIMessageChunk } from "ai";
|
|
67
|
+
import { getWritable } from "workflow";
|
|
68
|
+
import z from "zod/v4";
|
|
69
|
+
|
|
70
|
+
export async function codeAgent(messages: UIMessage[]) {
|
|
71
|
+
"use workflow";
|
|
72
|
+
|
|
73
|
+
let activeSandbox: Sandbox | null = null;
|
|
74
|
+
|
|
75
|
+
const agent = new DurableAgent({
|
|
76
|
+
model: "anthropic/claude-sonnet-4-20250514",
|
|
77
|
+
instructions:
|
|
78
|
+
"You are a coding assistant. You can create sandboxes to run code. " +
|
|
79
|
+
"Always create a sandbox first, then execute code in it. " +
|
|
80
|
+
"Clean up the sandbox when you are done.",
|
|
81
|
+
tools: {
|
|
82
|
+
createSandbox: {
|
|
83
|
+
description: "Create an isolated sandbox environment for running code",
|
|
84
|
+
inputSchema: z.object({
|
|
85
|
+
template: z.string().describe("The sandbox template (e.g., 'node', 'python')"),
|
|
86
|
+
}),
|
|
87
|
+
execute: async ({ template }) => {
|
|
88
|
+
activeSandbox = await Sandbox.create({ template }); // [!code highlight]
|
|
89
|
+
return { sandboxId: activeSandbox.id };
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
executeCode: {
|
|
93
|
+
description: "Execute a command in the active sandbox",
|
|
94
|
+
inputSchema: z.object({
|
|
95
|
+
command: z.string().describe("The command to execute"),
|
|
96
|
+
}),
|
|
97
|
+
execute: async ({ command }) => {
|
|
98
|
+
if (!activeSandbox) throw new Error("No active sandbox");
|
|
99
|
+
return activeSandbox.runCommand(command); // [!code highlight]
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
cleanupSandbox: {
|
|
103
|
+
description: "Destroy the active sandbox when finished",
|
|
104
|
+
inputSchema: z.object({}),
|
|
105
|
+
execute: async () => {
|
|
106
|
+
if (!activeSandbox) throw new Error("No active sandbox");
|
|
107
|
+
await activeSandbox.destroy(); // [!code highlight]
|
|
108
|
+
activeSandbox = null;
|
|
109
|
+
return { cleaned: true };
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const result = await agent.stream({
|
|
116
|
+
messages: await convertToModelMessages(messages),
|
|
117
|
+
writable: getWritable<UIMessageChunk>(),
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
return { messages: result.messages };
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Saga Pattern for Cleanup
|
|
125
|
+
|
|
126
|
+
Combine sandbox orchestration with the [saga pattern](/docs/cookbook/common-patterns/saga) to ensure sandboxes are always cleaned up, even when a step in the middle of your pipeline fails.
|
|
127
|
+
|
|
128
|
+
The example above uses a try/catch around the command execution loop. For more complex pipelines with multiple resources (sandbox + database + external API), push compensation functions onto a stack as shown in the [saga recipe](/docs/cookbook/common-patterns/saga).
|
|
@@ -166,54 +166,56 @@ The Streamer interface enables real-time data streaming:
|
|
|
166
166
|
{/* @skip-typecheck - interface definition, not runnable code */}
|
|
167
167
|
```typescript
|
|
168
168
|
interface Streamer {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
169
|
+
streamFlushIntervalMs?: number;
|
|
170
|
+
|
|
171
|
+
streams: {
|
|
172
|
+
write(
|
|
173
|
+
runId: string,
|
|
174
|
+
name: string,
|
|
175
|
+
chunk: string | Uint8Array
|
|
176
|
+
): Promise<void>;
|
|
177
|
+
|
|
178
|
+
writeMulti?(
|
|
179
|
+
runId: string,
|
|
180
|
+
name: string,
|
|
181
|
+
chunks: (string | Uint8Array)[]
|
|
182
|
+
): Promise<void>;
|
|
183
|
+
|
|
184
|
+
close(runId: string, name: string): Promise<void>;
|
|
185
|
+
|
|
186
|
+
get(
|
|
187
|
+
runId: string,
|
|
188
|
+
name: string,
|
|
189
|
+
startIndex?: number
|
|
190
|
+
): Promise<ReadableStream<Uint8Array>>;
|
|
191
|
+
|
|
192
|
+
list(runId: string): Promise<string[]>;
|
|
193
|
+
|
|
194
|
+
/** Paginated snapshot of stream chunks. */
|
|
195
|
+
getChunks(
|
|
196
|
+
runId: string,
|
|
197
|
+
name: string,
|
|
198
|
+
options?: { limit?: number; cursor?: string }
|
|
199
|
+
): Promise<{
|
|
200
|
+
data: { index: number; data: Uint8Array }[];
|
|
201
|
+
cursor: string | null;
|
|
202
|
+
hasMore: boolean;
|
|
203
|
+
done: boolean;
|
|
204
|
+
}>;
|
|
205
|
+
|
|
206
|
+
/** Lightweight metadata: tail index and completion flag. */
|
|
207
|
+
getInfo(
|
|
208
|
+
runId: string,
|
|
209
|
+
name: string
|
|
210
|
+
): Promise<{ tailIndex: number; done: boolean }>;
|
|
211
|
+
};
|
|
210
212
|
}
|
|
211
213
|
```
|
|
212
214
|
|
|
213
215
|
Streams are identified by a combination of `runId` and `name`. Each workflow run can have multiple named streams.
|
|
214
|
-
`
|
|
216
|
+
`writeMulti()` is an optional optimization for batching multiple writes.
|
|
215
217
|
|
|
216
|
-
`
|
|
218
|
+
`getChunks` returns a paginated snapshot of currently available chunks (unlike `get` which returns a live `ReadableStream` that waits for new chunks). `getInfo` returns the tail index (last chunk index, 0-based, or `-1` when empty) and whether the stream is complete — useful for resolving negative `startIndex` values into absolute positions.
|
|
217
219
|
|
|
218
220
|
## Reference Implementations
|
|
219
221
|
|