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.
- package/README.md +4 -4
- package/dist/api-workflow.js +1 -1
- 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 +1 -1
- package/dist/nitro.js +1 -1
- package/dist/nuxt.js +1 -1
- package/dist/observability.js +1 -1
- 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 +34 -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/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/world/local-world.mdx +1 -1
- package/docs/deploying/world/postgres-world.mdx +1 -1
- 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/python.mdx +165 -0
- 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 -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.
|