workflow 5.0.0-beta.0 → 5.0.0-beta.10
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 +2 -4
- package/dist/api-workflow.d.ts.map +1 -1
- package/dist/api-workflow.js +3 -7
- package/dist/api.d.ts +5 -1
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +14 -2
- package/dist/astro.js +1 -1
- package/dist/index.js +1 -1
- package/dist/internal/builtins.d.ts +17 -0
- package/dist/internal/builtins.d.ts.map +1 -1
- package/dist/internal/builtins.js +65 -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 +2 -2
- package/dist/observability.js +3 -3
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +2 -2
- 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/index.mdx +6 -5
- package/docs/ai/resumable-streams.mdx +1 -1
- package/docs/api-reference/vitest/index.mdx +28 -1
- package/docs/api-reference/workflow/create-webhook.mdx +37 -18
- package/docs/api-reference/workflow/experimental-set-attributes.mdx +63 -0
- package/docs/api-reference/workflow/get-workflow-metadata.mdx +61 -0
- package/docs/api-reference/workflow/index.mdx +3 -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/start.mdx +5 -4
- 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/api-reference/workflow-errors/workflow-run-failed-error.mdx +16 -6
- package/docs/api-reference/workflow-next/with-workflow.mdx +34 -2
- package/docs/changelog/attributes-mvp.mdx +368 -0
- package/docs/changelog/eager-processing.mdx +595 -0
- package/docs/changelog/index.mdx +2 -1
- package/docs/cookbook/advanced/child-workflows.mdx +313 -0
- package/docs/cookbook/advanced/meta.json +9 -0
- package/docs/cookbook/advanced/publishing-libraries.mdx +336 -0
- package/docs/cookbook/advanced/serializable-steps.mdx +147 -0
- package/docs/cookbook/advanced/upgrading-workflows.mdx +195 -0
- package/docs/cookbook/agent-patterns/agent-cancellation.mdx +156 -0
- package/docs/cookbook/agent-patterns/durable-agent.mdx +154 -0
- package/docs/cookbook/agent-patterns/human-in-the-loop.mdx +255 -0
- package/docs/cookbook/agent-patterns/meta.json +4 -0
- package/docs/cookbook/common-patterns/batching.mdx +105 -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 +247 -0
- package/docs/cookbook/common-patterns/scheduling.mdx +129 -0
- package/docs/cookbook/common-patterns/sequential-and-parallel.mdx +155 -0
- package/docs/cookbook/common-patterns/timeouts.mdx +99 -0
- package/docs/cookbook/common-patterns/webhooks.mdx +185 -0
- package/docs/cookbook/common-patterns/workflow-composition.mdx +111 -0
- package/docs/cookbook/index.mdx +38 -0
- package/docs/cookbook/integrations/ai-sdk.mdx +383 -0
- package/docs/cookbook/integrations/chat-sdk.mdx +307 -0
- package/docs/cookbook/integrations/meta.json +4 -0
- package/docs/cookbook/integrations/sandbox.mdx +520 -0
- package/docs/cookbook/meta.json +5 -0
- package/docs/deploying/building-a-world.mdx +46 -44
- package/docs/deploying/world/local-world.mdx +1 -1
- package/docs/deploying/world/postgres-world.mdx +15 -8
- package/docs/deploying/world/vercel-world.mdx +3 -1
- package/docs/errors/abort-signal-timeout-in-workflow.mdx +80 -0
- package/docs/errors/hook-conflict.mdx +56 -4
- package/docs/errors/index.mdx +3 -0
- package/docs/errors/runtime-decryption-failed.mdx +77 -0
- package/docs/errors/start-invalid-workflow-function.mdx +1 -1
- package/docs/foundations/cancellation.mdx +460 -0
- package/docs/foundations/errors-and-retries.mdx +7 -3
- package/docs/foundations/index.mdx +3 -3
- package/docs/foundations/meta.json +3 -2
- package/docs/foundations/serialization.mdx +78 -42
- package/docs/foundations/starting-workflows.mdx +6 -2
- package/docs/foundations/versioning.mdx +263 -0
- package/docs/getting-started/astro.mdx +6 -0
- package/docs/getting-started/index.mdx +13 -7
- package/docs/getting-started/meta.json +3 -1
- package/docs/getting-started/nestjs.mdx +8 -0
- package/docs/getting-started/next.mdx +29 -3
- package/docs/getting-started/nitro.mdx +22 -0
- package/docs/getting-started/python.mdx +165 -0
- package/docs/getting-started/sveltekit.mdx +6 -0
- package/docs/getting-started/tanstack-start.mdx +241 -0
- package/docs/how-it-works/cancellation.mdx +287 -0
- package/docs/how-it-works/code-transform.mdx +8 -7
- package/docs/how-it-works/event-sourcing.mdx +2 -2
- package/docs/how-it-works/meta.json +2 -1
- package/docs/internal/index.mdx +19 -0
- package/docs/internal/meta.json +5 -0
- package/docs/internal/serializable-abort-controller.mdx +148 -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 +358 -0
- package/docs/migration-guides/migrating-from-inngest.mdx +304 -0
- package/docs/migration-guides/migrating-from-temporal.mdx +313 -0
- package/docs/migration-guides/migrating-from-trigger-dev.mdx +328 -0
- package/docs/observability/attributes.mdx +66 -0
- package/docs/observability/index.mdx +9 -1
- package/docs/observability/meta.json +1 -1
- package/package.json +14 -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
- package/docs/foundations/common-patterns.mdx +0 -265
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Versioning
|
|
3
|
+
description: Understand how workflow runs are pinned to deployments, how to recover runs after a fix, and how to opt in to newer code explicitly.
|
|
4
|
+
type: guide
|
|
5
|
+
summary: Keep in-flight runs stable by default, then choose explicit upgrade boundaries when you need them.
|
|
6
|
+
prerequisites:
|
|
7
|
+
- /docs/foundations/starting-workflows
|
|
8
|
+
related:
|
|
9
|
+
- /docs/api-reference/workflow-api/start
|
|
10
|
+
- /docs/foundations/cancellation
|
|
11
|
+
- /cookbook/common-patterns/workflow-composition
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
Workflow runs are pinned to the deployment that starts them. When a run begins, Workflow SDK records the deployment for that run and continues executing the run on that same copy of your code.
|
|
15
|
+
|
|
16
|
+
That default is intentional. Durable workflows can pause for minutes, days, or months. If the code underneath a paused run changed every time you deployed, an in-flight run could resume into a different function body, different step names, or different input types than the ones it started with. That can make type safety fragile and can break long-running work in hard-to-debug ways.
|
|
17
|
+
|
|
18
|
+
With Workflow SDK, you can keep shipping. New runs use new deployments, while existing runs keep the version they already understand.
|
|
19
|
+
|
|
20
|
+
## Default behavior
|
|
21
|
+
|
|
22
|
+
Start a workflow normally:
|
|
23
|
+
|
|
24
|
+
```typescript title="app/api/orders/route.ts" lineNumbers
|
|
25
|
+
import { start } from "workflow/api";
|
|
26
|
+
import { fulfillOrder } from "@/workflows/fulfill-order";
|
|
27
|
+
|
|
28
|
+
export async function POST(request: Request) {
|
|
29
|
+
const { orderId } = await request.json();
|
|
30
|
+
|
|
31
|
+
const run = await start(fulfillOrder, [orderId]); // [!code highlight]
|
|
32
|
+
|
|
33
|
+
return Response.json({ runId: run.runId });
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The run is tied to the deployment that handled this request. If you deploy a new version while the workflow is [sleeping](/docs/api-reference/workflow/sleep), [waiting on a hook](/docs/foundations/hooks), [retrying a step](/docs/foundations/errors-and-retries), or processing later queue messages, that existing run still resumes on the original deployment.
|
|
38
|
+
|
|
39
|
+
```typescript title="workflows/fulfill-order.ts" lineNumbers
|
|
40
|
+
import { sleep } from "workflow";
|
|
41
|
+
|
|
42
|
+
export async function fulfillOrder(orderId: string) {
|
|
43
|
+
"use workflow";
|
|
44
|
+
|
|
45
|
+
await reserveInventory(orderId);
|
|
46
|
+
await sleep("2d");
|
|
47
|
+
await chargeCustomer(orderId);
|
|
48
|
+
await shipOrder(orderId);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function reserveInventory(orderId: string) {
|
|
52
|
+
"use step";
|
|
53
|
+
// ...
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function chargeCustomer(orderId: string) {
|
|
57
|
+
"use step";
|
|
58
|
+
// ...
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function shipOrder(orderId: string) {
|
|
62
|
+
"use step";
|
|
63
|
+
// ...
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
If you deploy a change to `chargeCustomer()` while a run is in the two-day sleep, the existing run does not suddenly resume into the new implementation. It continues on the deployment it started on. The next order starts on the latest deployment and uses the new code from the beginning.
|
|
68
|
+
|
|
69
|
+
## Fixing in-flight runs
|
|
70
|
+
|
|
71
|
+
Sometimes you deploy because the old code had a bug. The safest fix is usually explicit:
|
|
72
|
+
|
|
73
|
+
1. Deploy the fixed code.
|
|
74
|
+
2. Find the affected runs in [observability](/docs/observability) or with the CLI.
|
|
75
|
+
3. Cancel the old runs if they are still running.
|
|
76
|
+
4. Rerun them on the latest deployment with the same inputs.
|
|
77
|
+
|
|
78
|
+
This keeps the version boundary visible. The old run ends as cancelled or failed, and the replacement run starts fresh on the fixed deployment. This is a good fit for one-off, ad-hoc upgrades where you explicitly opt in to moving affected runs onto a new version.
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# Inspect affected runs and copy the exact workflowName value.
|
|
82
|
+
npx workflow inspect runs \
|
|
83
|
+
--backend vercel \
|
|
84
|
+
--status running
|
|
85
|
+
|
|
86
|
+
# Cancel one run.
|
|
87
|
+
npx workflow cancel <run-id> \
|
|
88
|
+
--backend vercel
|
|
89
|
+
|
|
90
|
+
# Or bulk-cancel matching running runs.
|
|
91
|
+
npx workflow cancel \
|
|
92
|
+
--status running \
|
|
93
|
+
--workflowName "workflow//./workflows/fulfill-order//fulfillOrder" \
|
|
94
|
+
--backend vercel
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The `--workflowName` filter expects the generated workflow ID, not only the exported function's short name. Use the `workflowName` value from `workflow inspect runs`, and use [`parseWorkflowName()`](/docs/api-reference/workflow-api/world/observability) when you need display-friendly names.
|
|
98
|
+
|
|
99
|
+
In the [observability UI](/docs/observability), use **Rerun on latest** to enqueue the workflow again with the same inputs against the latest deployment.
|
|
100
|
+
|
|
101
|
+
If you are writing your own recovery route, call `start()` with the same arguments and `deploymentId: "latest"`:
|
|
102
|
+
|
|
103
|
+
```typescript title="app/api/orders/rerun/route.ts" lineNumbers
|
|
104
|
+
import { start } from "workflow/api";
|
|
105
|
+
import { fulfillOrder } from "@/workflows/fulfill-order";
|
|
106
|
+
|
|
107
|
+
export async function POST(request: Request) {
|
|
108
|
+
const { orderId } = await request.json();
|
|
109
|
+
|
|
110
|
+
const run = await start(fulfillOrder, [orderId], {
|
|
111
|
+
deploymentId: "latest", // [!code highlight]
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
return Response.json({ runId: run.runId });
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
<Callout type="warn">
|
|
119
|
+
`deploymentId: "latest"` is currently a Vercel-specific feature. Other Worlds may implement this option differently to match their own deployment runtimes, and the World spec may rename it from `deploymentId` to `version` in a future SDK version. On Vercel, `"latest"` resolves to the most recent deployment matching your current environment. Because the caller and target deployment can be different, keep the [workflow function name and file path](/docs/errors/workflow-not-registered), arguments, and return value backward-compatible across the deployments you plan to bridge.
|
|
120
|
+
</Callout>
|
|
121
|
+
|
|
122
|
+
## Self upgrading workflows
|
|
123
|
+
|
|
124
|
+
Some workflows are expected to run for a very long time. Scheduled loops, recurring jobs, agents, and chat sessions often should not stay on one deployment forever.
|
|
125
|
+
|
|
126
|
+
Model those as a sequence of runs. Each run does a bounded piece of work, then starts the next run on the latest deployment and exits. This is similar to `continueAsNew` in other durable execution systems, but in Workflow SDK it is just [explicit recursion through `start()`](/cookbook/common-patterns/workflow-composition).
|
|
127
|
+
|
|
128
|
+
```typescript title="workflows/daily-digest.ts" lineNumbers
|
|
129
|
+
import { sleep } from "workflow";
|
|
130
|
+
import { start } from "workflow/api";
|
|
131
|
+
|
|
132
|
+
type DigestState = {
|
|
133
|
+
userId: string;
|
|
134
|
+
lastSentAt?: string;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
export async function dailyDigest(state: DigestState) {
|
|
138
|
+
"use workflow";
|
|
139
|
+
|
|
140
|
+
const sentAt = await sendDigest(state.userId);
|
|
141
|
+
await sleep("1d");
|
|
142
|
+
|
|
143
|
+
const run = await start(
|
|
144
|
+
dailyDigest,
|
|
145
|
+
[{ ...state, lastSentAt: sentAt }],
|
|
146
|
+
{
|
|
147
|
+
deploymentId: "latest", // [!code highlight]
|
|
148
|
+
}
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
return { continuedAs: run.runId };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function sendDigest(userId: string) {
|
|
155
|
+
"use step";
|
|
156
|
+
// ...
|
|
157
|
+
return new Date().toISOString();
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
This pattern gives every run a clear lifecycle:
|
|
162
|
+
|
|
163
|
+
- The current run stays on its original deployment.
|
|
164
|
+
- The next run starts on the latest deployment.
|
|
165
|
+
- The [serialized `state`](/docs/foundations/serialization) is the migration boundary between versions.
|
|
166
|
+
- Observability can link parent and child runs when a workflow starts another run.
|
|
167
|
+
|
|
168
|
+
## Carrying context forward
|
|
169
|
+
|
|
170
|
+
Anything that is [serializable by Workflow SDK](/docs/foundations/serialization) can be passed from one run to the next as an argument. That includes plain state objects, `ReadableStream`, `WritableStream`, `AbortSignal`, and other supported serialized values.
|
|
171
|
+
|
|
172
|
+
For example, a long export can register its [output stream](/docs/foundations/streaming) once, write progress from each run, and pass the same stream plus updated state into the next run:
|
|
173
|
+
|
|
174
|
+
```typescript title="workflows/export-report.ts" lineNumbers
|
|
175
|
+
import { getWritable } from "workflow";
|
|
176
|
+
import { start } from "workflow/api";
|
|
177
|
+
|
|
178
|
+
type ExportState = {
|
|
179
|
+
exportId: string;
|
|
180
|
+
page: number;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export async function exportReport(
|
|
184
|
+
state: ExportState,
|
|
185
|
+
progress?: WritableStream<string>
|
|
186
|
+
) {
|
|
187
|
+
"use workflow";
|
|
188
|
+
|
|
189
|
+
// Register the stream once. Continuation runs receive this same stream
|
|
190
|
+
// as an argument and keep writing to it.
|
|
191
|
+
const stream =
|
|
192
|
+
progress !== undefined ? progress : getWritable<string>();
|
|
193
|
+
|
|
194
|
+
const hasMore = await exportPage(state, stream);
|
|
195
|
+
|
|
196
|
+
if (!hasMore) {
|
|
197
|
+
await writeProgress(stream, { type: "done", totalPages: state.page });
|
|
198
|
+
return { totalPages: state.page };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const run = await start(exportReport, [
|
|
202
|
+
{ ...state, page: state.page + 1 },
|
|
203
|
+
stream,
|
|
204
|
+
], {
|
|
205
|
+
deploymentId: "latest", // [!code highlight]
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
return { continuedAs: run.runId };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function exportPage(
|
|
212
|
+
state: ExportState,
|
|
213
|
+
stream: WritableStream<string>
|
|
214
|
+
) {
|
|
215
|
+
"use step";
|
|
216
|
+
|
|
217
|
+
// Do work for this version boundary.
|
|
218
|
+
const hasMore = state.page < 10;
|
|
219
|
+
const writer = stream.getWriter();
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
await writer.write(
|
|
223
|
+
JSON.stringify({ type: "page", page: state.page }) + "\n"
|
|
224
|
+
);
|
|
225
|
+
return hasMore;
|
|
226
|
+
} finally {
|
|
227
|
+
writer.releaseLock();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function writeProgress(
|
|
232
|
+
stream: WritableStream<string>,
|
|
233
|
+
event: { type: "done"; totalPages: number }
|
|
234
|
+
) {
|
|
235
|
+
"use step";
|
|
236
|
+
|
|
237
|
+
const writer = stream.getWriter();
|
|
238
|
+
try {
|
|
239
|
+
await writer.write(JSON.stringify(event) + "\n");
|
|
240
|
+
} finally {
|
|
241
|
+
writer.releaseLock();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
```typescript title="app/api/export/route.ts" lineNumbers
|
|
247
|
+
import { start } from "workflow/api";
|
|
248
|
+
import { exportReport } from "@/workflows/export-report";
|
|
249
|
+
|
|
250
|
+
export async function POST(request: Request) {
|
|
251
|
+
const { exportId } = await request.json();
|
|
252
|
+
|
|
253
|
+
const run = await start(exportReport, [{ exportId, page: 1 }]);
|
|
254
|
+
|
|
255
|
+
// Linked continuation runs keep writing to the stream registered by
|
|
256
|
+
// the parent run, because that stream is passed forward as an argument.
|
|
257
|
+
return new Response(run.readable, {
|
|
258
|
+
headers: { "Content-Type": "application/jsonl" },
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Each run still has one clear version boundary: the current run stays on its original deployment, the next run starts on the latest deployment, and only the explicit state and stream handle are carried forward.
|
|
@@ -51,6 +51,12 @@ export default defineConfig({
|
|
|
51
51
|
});
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
+
`workflow()` accepts an options object:
|
|
55
|
+
|
|
56
|
+
| Option | Type | Default | Description |
|
|
57
|
+
| --- | --- | --- | --- |
|
|
58
|
+
| `sourcemap` | `boolean \| 'inline' \| 'linked' \| 'external' \| 'both'` | `'inline'` | Controls source maps on generated workflow bundles. Accepts the same values as esbuild's `sourcemap` option. Set to `false` for smaller function bundles (useful for staying under the Vercel 250MB function size limit) at the cost of stack traces pointing at generated code. Can also be set via the `WORKFLOW_SOURCEMAP` environment variable. |
|
|
59
|
+
|
|
54
60
|
<Accordion type="single" collapsible>
|
|
55
61
|
<AccordionItem value="typescript-intellisense" className="[&_h3]:my-0">
|
|
56
62
|
<AccordionTrigger className="text-sm">
|
|
@@ -8,7 +8,7 @@ related:
|
|
|
8
8
|
- /docs/foundations/workflows-and-steps
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
-
import { Next, Nitro, SvelteKit, Nuxt, Hono, Bun, AstroDark, AstroLight, TanStack, Vite, Express, Nest, Fastify } from "@/app/[lang]/(home)/components/frameworks";
|
|
11
|
+
import { Next, Nitro, SvelteKit, Nuxt, Hono, Bun, AstroDark, AstroLight, TanStack, Vite, Express, Nest, Fastify, Python } from "@/app/[lang]/(home)/components/frameworks";
|
|
12
12
|
|
|
13
13
|
<Cards>
|
|
14
14
|
<Card href="/docs/getting-started/next">
|
|
@@ -63,17 +63,23 @@ import { Next, Nitro, SvelteKit, Nuxt, Hono, Bun, AstroDark, AstroLight, TanStac
|
|
|
63
63
|
<span className="font-medium">SvelteKit</span>
|
|
64
64
|
</div>
|
|
65
65
|
</Card>
|
|
66
|
-
<Card
|
|
66
|
+
<Card href="/docs/getting-started/tanstack-start" >
|
|
67
|
+
<div className="flex flex-col items-center justify-center gap-2">
|
|
68
|
+
<TanStack className="size-16 dark:invert" />
|
|
69
|
+
<span className="font-medium">TanStack Start</span>
|
|
70
|
+
</div>
|
|
71
|
+
</Card>
|
|
72
|
+
<Card href="/docs/getting-started/python">
|
|
67
73
|
<div className="flex flex-col items-center justify-center gap-2">
|
|
68
|
-
<
|
|
69
|
-
<span className="font-medium">
|
|
70
|
-
<Badge variant="secondary">
|
|
74
|
+
<Python className="size-16" />
|
|
75
|
+
<span className="font-medium">Python</span>
|
|
76
|
+
<Badge variant="secondary">Beta</Badge>
|
|
71
77
|
</div>
|
|
72
78
|
</Card>
|
|
73
79
|
<Card className="opacity-50">
|
|
74
80
|
<div className="flex flex-col items-center justify-center gap-2">
|
|
75
|
-
<
|
|
76
|
-
<span className="font-medium">
|
|
81
|
+
<Nest className="size-16 dark:invert grayscale" />
|
|
82
|
+
<span className="font-medium">NestJS</span>
|
|
77
83
|
<Badge variant="secondary">Coming soon</Badge>
|
|
78
84
|
</div>
|
|
79
85
|
</Card>
|
|
@@ -386,6 +386,14 @@ WorkflowModule.forRoot({
|
|
|
386
386
|
// Only used when moduleType is 'commonjs'
|
|
387
387
|
// Should match the outDir in your tsconfig.json
|
|
388
388
|
distDir: 'dist',
|
|
389
|
+
|
|
390
|
+
// Source maps on generated workflow bundles (default: 'inline').
|
|
391
|
+
// Accepts the same values as esbuild's sourcemap option: true, false,
|
|
392
|
+
// 'inline', 'linked', 'external', 'both'. Set to false for smaller
|
|
393
|
+
// function bundles (useful for staying under the Vercel 250MB function
|
|
394
|
+
// size limit) at the cost of stack traces pointing at generated code.
|
|
395
|
+
// Can also be set via the WORKFLOW_SOURCEMAP environment variable.
|
|
396
|
+
sourcemap: 'inline',
|
|
389
397
|
});
|
|
390
398
|
```
|
|
391
399
|
|
|
@@ -75,9 +75,9 @@ To enable helpful hints in your IDE, setup the workflow plugin in `tsconfig.json
|
|
|
75
75
|
</Accordion>
|
|
76
76
|
|
|
77
77
|
<Accordion type="single" collapsible>
|
|
78
|
-
<AccordionItem value="
|
|
78
|
+
<AccordionItem value="configure-proxy-handler" className="[&_h3]:my-0">
|
|
79
79
|
<AccordionTrigger className="text-sm">
|
|
80
|
-
|
|
80
|
+
<h3 id="configure-proxy-handler">Configure Proxy Handler (if applicable)</h3>
|
|
81
81
|
</AccordionTrigger>
|
|
82
82
|
<AccordionContent className="[&_p]:my-2">
|
|
83
83
|
|
|
@@ -85,7 +85,9 @@ If your Next.js app has a [proxy handler](https://nextjs.org/docs/app/api-refere
|
|
|
85
85
|
(formerly known as "middleware"), you'll need to update the matcher pattern to exclude Workflow's
|
|
86
86
|
internal paths to prevent the proxy handler from running on them.
|
|
87
87
|
|
|
88
|
-
|
|
88
|
+
If you see `[local world] Queue operation failed` with `Cannot perform ArrayBuffer.prototype.slice on a detached ArrayBuffer`, your proxy matcher is still intercepting Workflow's internal `POST /.well-known/workflow/v1/flow` request. This is especially easy to miss in Next.js 16, where `proxy.ts` replaced `middleware.ts`.
|
|
89
|
+
|
|
90
|
+
Add `.well-known/workflow/*` to your matcher exclusion list:
|
|
89
91
|
|
|
90
92
|
```typescript title="proxy.ts" lineNumbers
|
|
91
93
|
import { NextResponse } from "next/server";
|
|
@@ -281,6 +283,30 @@ Upgrade to `workflow@4.0.1-beta.26` or later:
|
|
|
281
283
|
workflow@latest
|
|
282
284
|
```
|
|
283
285
|
|
|
286
|
+
### Turborepo caching
|
|
287
|
+
|
|
288
|
+
If you're using [Turborepo](https://turbo.build/repo) in a monorepo, you need to include the generated Workflow routes in your cache outputs. The Workflow SDK generates route handlers at `app/.well-known/workflow/` (or `src/app/.well-known/workflow/` if your project uses the `src` directory) during the build process, and these files must be cached alongside your Next.js build output.
|
|
289
|
+
|
|
290
|
+
Add the following to your `turbo.json`:
|
|
291
|
+
|
|
292
|
+
```jsonc title="turbo.json"
|
|
293
|
+
{
|
|
294
|
+
"tasks": {
|
|
295
|
+
"build": {
|
|
296
|
+
"outputs": [
|
|
297
|
+
".next/**",
|
|
298
|
+
"!.next/cache/**",
|
|
299
|
+
// Include whichever path matches your project layout
|
|
300
|
+
"app/.well-known/workflow/**",
|
|
301
|
+
"src/app/.well-known/workflow/**"
|
|
302
|
+
]
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
Without this configuration, you may experience intermittent issues where workflows fail to register properly on cache hits, while working correctly on cache misses.
|
|
309
|
+
|
|
284
310
|
### `start()` says it received an invalid workflow function
|
|
285
311
|
|
|
286
312
|
If you see this error:
|
|
@@ -46,6 +46,28 @@ export default defineConfig({
|
|
|
46
46
|
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
+
### Module options
|
|
50
|
+
|
|
51
|
+
The `workflow/nitro` module reads its options from `workflow` on your Nitro config.
|
|
52
|
+
|
|
53
|
+
```typescript title="nitro.config.ts" lineNumbers
|
|
54
|
+
import { defineConfig } from "nitro";
|
|
55
|
+
|
|
56
|
+
export default defineConfig({
|
|
57
|
+
modules: ["workflow/nitro"],
|
|
58
|
+
workflow: {
|
|
59
|
+
runtime: "nodejs22.x",
|
|
60
|
+
sourcemap: "inline",
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
| Option | Type | Default | Description |
|
|
66
|
+
| --- | --- | --- | --- |
|
|
67
|
+
| `dirs` | `string[]` | — | Directories to scan for workflows and steps. By default, `workflows/` is scanned from the project root and all layer source directories. |
|
|
68
|
+
| `runtime` | `string` | `'nodejs22.x'` | Node.js runtime version for Vercel Functions (e.g. `'nodejs22.x'`, `'nodejs24.x'`). |
|
|
69
|
+
| `sourcemap` | `boolean \| 'inline' \| 'linked' \| 'external' \| 'both'` | `'inline'` | Controls source maps on generated workflow bundles. Accepts the same values as esbuild's `sourcemap` option. Set to `false` for smaller function bundles (useful for staying under the Vercel 250MB function size limit) at the cost of stack traces pointing at generated code. Can also be set via the `WORKFLOW_SOURCEMAP` environment variable. |
|
|
70
|
+
|
|
49
71
|
<Accordion type="single" collapsible>
|
|
50
72
|
<AccordionItem value="typescript-intellisense" className="[&_h3]:my-0">
|
|
51
73
|
<AccordionTrigger className="[&_p]:my-0 text-lg [&_p]:text-foreground">
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Python
|
|
3
|
+
description: Build durable workflows and AI agents in Python with the Vercel SDK.
|
|
4
|
+
type: guide
|
|
5
|
+
summary: Set up the Workflow Python SDK in your Python application.
|
|
6
|
+
prerequisites:
|
|
7
|
+
- /docs/getting-started
|
|
8
|
+
related:
|
|
9
|
+
- /docs/foundations
|
|
10
|
+
- /docs/foundations/workflows-and-steps
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
<Callout type="warn">
|
|
14
|
+
The Python SDK is currently in **beta**. APIs and behavior may change. For the latest documentation and updates, see the [official Vercel Workflow Python documentation](https://vercel.com/docs/workflow/python?language=py).
|
|
15
|
+
</Callout>
|
|
16
|
+
|
|
17
|
+
You can build durable workflows in Python using the [`vercel` Python SDK](https://pypi.org/project/vercel/). Your workflow code can pause, resume, and maintain state, just like the JavaScript and TypeScript Workflow SDK.
|
|
18
|
+
|
|
19
|
+
## Getting Started
|
|
20
|
+
|
|
21
|
+
Install the `vercel` package:
|
|
22
|
+
|
|
23
|
+
```bash filename="Terminal"
|
|
24
|
+
pip install vercel
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Configure `experimentalServices` in your `vercel.json`:
|
|
28
|
+
|
|
29
|
+
```json filename="vercel.json"
|
|
30
|
+
{
|
|
31
|
+
"experimentalServices": {
|
|
32
|
+
"ai_content_workflow": {
|
|
33
|
+
"type": "worker",
|
|
34
|
+
"entrypoint": "app/workflows/ai_content_workflow.py",
|
|
35
|
+
"topics": ["__wkf_*"]
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Workflows
|
|
42
|
+
|
|
43
|
+
A workflow is a stateful function that coordinates multi-step logic over time. Create a `Workflows` instance and use the `@wf.workflow` decorator to mark a function as durable:
|
|
44
|
+
|
|
45
|
+
```python filename="app/workflow.py" {3}
|
|
46
|
+
from vercel import workflow
|
|
47
|
+
|
|
48
|
+
wf = workflow.Workflows()
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
```python filename="app/workflows/ai_content_workflow.py" {3}
|
|
52
|
+
from app.workflow import wf
|
|
53
|
+
|
|
54
|
+
@wf.workflow
|
|
55
|
+
async def ai_content_workflow(*, topic: str):
|
|
56
|
+
draft = await generate_draft(topic=topic)
|
|
57
|
+
summary = await summarize_draft(draft=draft)
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
"draft": draft,
|
|
61
|
+
"summary": summary,
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Under the hood, the workflow compiles into a route that orchestrates execution. All inputs and outputs are recorded in an event log. If a deploy or crash happens, the system replays execution deterministically from where it stopped.
|
|
66
|
+
|
|
67
|
+
## Steps
|
|
68
|
+
|
|
69
|
+
A step is a stateless function that runs a unit of durable work inside a workflow. Use `@wf.step` to mark a function as a step:
|
|
70
|
+
|
|
71
|
+
```python filename="app/steps/generate_draft.py" {4,8}
|
|
72
|
+
import random
|
|
73
|
+
from app.workflow import wf
|
|
74
|
+
|
|
75
|
+
@wf.step
|
|
76
|
+
async def generate_draft(*, topic: str):
|
|
77
|
+
return await ai_generate(prompt=f"Write a blog post about {topic}")
|
|
78
|
+
|
|
79
|
+
@wf.step
|
|
80
|
+
async def summarize_draft(*, draft: str):
|
|
81
|
+
summary = await ai_summarize(text=draft)
|
|
82
|
+
|
|
83
|
+
# Simulate a transient error. The step automatically retries.
|
|
84
|
+
if random.random() < 0.3:
|
|
85
|
+
raise Exception("Transient AI provider error")
|
|
86
|
+
|
|
87
|
+
return summary
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Each step compiles into an isolated route. While the step executes, the workflow suspends without consuming resources. When the step completes, the workflow resumes automatically where it left off.
|
|
91
|
+
|
|
92
|
+
## Sleep
|
|
93
|
+
|
|
94
|
+
Sleep pauses a workflow for a specified duration without consuming compute resources:
|
|
95
|
+
|
|
96
|
+
```python filename="app/workflows/ai_refine.py" {7}
|
|
97
|
+
from vercel import workflow
|
|
98
|
+
|
|
99
|
+
@wf.workflow
|
|
100
|
+
async def ai_refine_workflow(*, draft_id: str):
|
|
101
|
+
draft = await fetch_draft(draft_id)
|
|
102
|
+
|
|
103
|
+
await workflow.sleep("7 days") # Wait 7 days to gather more signals.
|
|
104
|
+
|
|
105
|
+
refined = await refine_draft(draft)
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
"draft_id": draft_id,
|
|
109
|
+
"refined": refined,
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The sleep call pauses the workflow and consumes no resources. The workflow resumes automatically when the time expires.
|
|
114
|
+
|
|
115
|
+
## Hooks
|
|
116
|
+
|
|
117
|
+
A hook lets a workflow wait for external events such as user actions, webhooks, or third-party API responses.
|
|
118
|
+
|
|
119
|
+
Define a hook model with Pydantic and `workflow.BaseHook`:
|
|
120
|
+
|
|
121
|
+
```python filename="app/workflows/approval.py" {3,14}
|
|
122
|
+
from vercel import workflow
|
|
123
|
+
|
|
124
|
+
class Approval(BaseModel, workflow.BaseHook):
|
|
125
|
+
"""Human approval for AI-generated drafts"""
|
|
126
|
+
|
|
127
|
+
decision: Literal["approved", "changes"]
|
|
128
|
+
notes: str | None = None
|
|
129
|
+
|
|
130
|
+
@wf.workflow
|
|
131
|
+
async def ai_approval_workflow(*, topic: str):
|
|
132
|
+
draft = await generate_draft(topic=topic)
|
|
133
|
+
|
|
134
|
+
# Wait for human approval events
|
|
135
|
+
async for event in Approval.wait(token="draft-123"):
|
|
136
|
+
if event.decision == "approved":
|
|
137
|
+
await publish_draft(draft)
|
|
138
|
+
break
|
|
139
|
+
|
|
140
|
+
revised = await refine_draft(draft, event.notes)
|
|
141
|
+
await publish_draft(revised)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Resume the workflow when data arrives:
|
|
145
|
+
|
|
146
|
+
```python filename="app/api/resume.py" {5}
|
|
147
|
+
@app.post("/api/resume")
|
|
148
|
+
async def resume(approval: Approval):
|
|
149
|
+
"""Resume the workflow when an approval is received"""
|
|
150
|
+
|
|
151
|
+
await approval.resume("draft-123")
|
|
152
|
+
return {"ok": True}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
When a hook receives data, the workflow resumes automatically. You don't need polling, message queues, or manual state management.
|
|
156
|
+
|
|
157
|
+
## Learn More
|
|
158
|
+
|
|
159
|
+
For comprehensive documentation, examples, and the latest updates, visit the [official Vercel Workflow Python documentation](https://vercel.com/docs/workflow/python).
|
|
160
|
+
|
|
161
|
+
## Next Steps
|
|
162
|
+
|
|
163
|
+
- Learn more about the [Foundations](/docs/foundations).
|
|
164
|
+
- Check [Errors](/docs/errors) if you encounter issues.
|
|
165
|
+
- Explore the [API Reference](/docs/api-reference).
|
|
@@ -46,6 +46,12 @@ export default defineConfig({
|
|
|
46
46
|
});
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
+
`workflowPlugin()` accepts an options object:
|
|
50
|
+
|
|
51
|
+
| Option | Type | Default | Description |
|
|
52
|
+
| --- | --- | --- | --- |
|
|
53
|
+
| `sourcemap` | `boolean \| 'inline' \| 'linked' \| 'external' \| 'both'` | `'inline'` | Controls source maps on generated workflow bundles. Accepts the same values as esbuild's `sourcemap` option. Set to `false` for smaller function bundles (useful for staying under the Vercel 250MB function size limit) at the cost of stack traces pointing at generated code. Can also be set via the `WORKFLOW_SOURCEMAP` environment variable. |
|
|
54
|
+
|
|
49
55
|
<Accordion type="single" collapsible>
|
|
50
56
|
<AccordionItem value="typescript-intellisense" className="[&_h3]:my-0">
|
|
51
57
|
<AccordionTrigger className="text-sm">
|