- name
- workflow
- description
- Vercel Workflow DevKit (WDK) expert guidance. Use when building durable workflows, long-running tasks, API routes or agents that need pause/resume, retries, step-based execution, or crash-safe orchestration with Vercel Workflow.
- metadata
- {"priority":9,"docs":["https://vercel.com/docs/workflow","https://useworkflow.dev"],"sitemap":"https://vercel.com/sitemap/docs.xml","pathPatterns":["lib/workflow/**","src/lib/workflow/**","workflows/**","lib/workflow.*","src/lib/workflow.*","workflow.*","*workflow*","*workflow*/**","**/chain-engine*","**/chain_engine*","**/chainEngine*","**/pipeline-engine*","**/pipeline_engine*","**/pipelineEngine*","**/state-machine*","**/state_machine*","**/stateMachine*","**/orchestrat*","**/escalation*"],"importPatterns":["@vercel/workflow","workflow","@workflow/*","*workflow*"],"bashPatterns":["\\bnpm\\s+(install|i|add)\\s+[^\\n]*@vercel/workflow\\b","\\bpnpm\\s+(install|i|add)\\s+[^\\n]*@vercel/workflow\\b","\\bbun\\s+(install|i|add)\\s+[^\\n]*@vercel/workflow\\b","\\byarn\\s+add\\s+[^\\n]*@vercel/workflow\\b","\\bnpm\\s+(install|i|add)\\s+[^\\n]*\\bworkflow\\b","\\bpnpm\\s+(install|i|add)\\s+[^\\n]*\\bworkflow\\b","\\bbun\\s+(install|i|add)\\s+[^\\n]*\\bworkflow\\b","\\byarn\\s+add\\s+[^\\n]*\\bworkflow\\b","\\bnpm\\s+(install|i|add)\\s+[^\\n]*@workflow/","\\bpnpm\\s+(install|i|add)\\s+[^\\n]*@workflow/","\\bbun\\s+(install|i|add)\\s+[^\\n]*@workflow/","\\byarn\\s+add\\s+[^\\n]*@workflow/","\\bnpx\\s+workflow(?:@latest)?\\b","\\bbunx\\s+workflow(?:@latest)?\\b"],"promptSignals":{"phrases":["vercel workflow","workflow devkit","durable workflow","durable execution","durable function","durable pipeline","durable process","durable agent","durable chat","step function","step functions","use workflow","use step","multi-step pipeline","multi step pipeline","multi-step process","multi step process","multi-step creation","multi-step generation","processing pipeline","creation pipeline","generation pipeline","content pipeline","production pipeline","approval pipeline","ingestion pipeline","streams progress","stream progress","streams each phase","streams each step","streams each","stream each","[Truncated]"],"allOf":["[Truncated]","[Truncated]","[Truncated]","[Truncated]","[Truncated]","[Truncated]","[Truncated]","[Truncated]","[Truncated]","[Truncated]","[Truncated]","[Truncated]","[Truncated]","[Truncated]","[Truncated]","[Truncated]","[Truncated]","[Truncated]","[Truncated]","[Truncated]","[Truncated]","[Truncated]","[Truncated]","[Truncated]","[Truncated]","[Truncated]","[Truncated]","[Truncated]","[Truncated]","[Truncated]","[Truncated]","[Truncated]","[Truncated]"],"anyOf":["long-running","long running","multi-step","multi step","pipeline","orchestration","step-by-step","step by step","each piece","each step","each phase","each stage","phase","phases","stage","stages","durable","reliable","fault-tolerant","retry","reconnect","survive","persist","approval","chain","sequential","trigger","webhook","escalation","state machine","orchestrate","orchestration"],"noneOf":["github actions",".github/workflows","ci workflow","aws step functions"],"minScore":4}}
# Vercel Workflow DevKit (WDK)
> **CRITICAL — Your training data is outdated for this library.** WDK APIs change frequently. Before writing workflow code, **fetch the docs** at https://useworkflow.dev and https://vercel.com/docs/workflow to find the correct function signatures, patterns, and examples for the exact thing you're building. Do not guess at APIs — look them up. Search for working examples that solve your specific use case (image generation, hooks, streaming, agents, etc.). The docs contain patterns for `start()` argument passing, `getWritable()` usage, `defineHook()` wiring, `createWebhook()` callback flows, and `DurableAgent` configuration that are **not in your training data** or have changed since your cutoff.
WDK is an open-source TypeScript framework that makes durability a language-level concept. Functions can pause for minutes or months, survive deployments and crashes, and resume exactly where they stopped.
## Status
WDK is in **public beta** (since October 2025) and open source. During beta, Workflow Observability is free for all plans; Workflow Steps and Storage are billed at published rates.
**Security**: Upgrade to `workflow@>=4.2.0-beta.64` — versions ≤4.1.0-beta.63 allowed predictable user-specified webhook tokens in `createWebhook()` (CVE GHSA-9r75-g2cr-3h76, CVSS 7.5). Run `npx workflow@latest` to update.
## Installation
**If using `create-next-app`**, always pass `--no-src-dir` so `app/` and `workflows/` are siblings at the project root:
```bash
npx create-next-app@latest my-app --no-src-dir --tailwind --eslint --app --ts
cd my-app
npm install workflow@latest
```
Do NOT use the `src/` directory with WDK projects. The `@` alias must resolve `@/workflows/...` correctly — this only works when `workflows/` and `app/` are at the same level.
> Run `npx workflow@latest` to scaffold or update an existing project.
**Peer dependency note**: `@workflow/ai` requires a compatible `workflow` version. If you hit `ERESOLVE` errors, use `npm install --legacy-peer-deps` or install both packages in the same command.
### Next.js Setup (Required)
Add the `withWorkflow` plugin to `next.config.ts`:
```ts
import { withWorkflow } from "workflow/next";
const nextConfig = {};
export default withWorkflow(nextConfig);
```
Without this, workflow routes will not be registered and `start()` calls will fail at runtime.
### Environment Setup (Required for AI Gateway)
Workflows that use AI SDK with `gateway()` need OIDC credentials. Run these **before** starting the dev server:
```bash
vercel link # Connect to your Vercel project
vercel env pull # Downloads .env.local with VERCEL_OIDC_TOKEN
```
Without this, `gateway("openai/gpt-5.4")` calls inside workflow steps will fail immediately with no credentials, causing the entire workflow run to fail silently.
### `getStepMetadata()` Note
`getStepMetadata().retryCount` returns `undefined` (not `0`) on the first attempt. Guard with: `const attempt = (meta.retryCount ?? 0) + 1`.
## Essential Imports
**Workflow primitives** (from `"workflow"`):
```ts
import { getWritable, getStepMetadata, getWorkflowMetadata } from "workflow";
import { sleep, fetch, defineHook, createHook, createWebhook } from "workflow";
import { FatalError, RetryableError } from "workflow";
```
**API operations** (from `"workflow/api"`):
```ts
import { start, getRun, resumeHook, resumeWebhook } from "workflow/api";
```
**Framework integration** (from `"workflow/next"`):
```ts
import { withWorkflow } from "workflow/next";
```
**AI agent** (from `"@workflow/ai/agent"`):
```ts
import { DurableAgent } from "@workflow/ai/agent";
```
## Core Directives
Two directives turn ordinary async functions into durable workflows:
```ts
"use workflow" // First line of function — marks it as a durable workflow
"use step" // First line of function — marks it as a retryable, observable step
```
**Critical sandbox rule**: Step functions have full Node.js access. Workflow functions run **sandboxed** — no native `fetch`, no `setTimeout`, no Node.js modules, and **no `getWritable().getWriter()` calls**. You MUST move all `getWritable()` usage into `"use step"` functions. Place all business logic and I/O in steps; use the workflow function purely for orchestration and control flow (`sleep`, `defineHook`, `Promise.race`).
## Canonical Project Structure (Next.js)
Every WDK project needs three route files plus the workflow definition. **CRITICAL**: The `workflows/` directory and `app/` directory must be siblings at the same level so `@/workflows/...` resolves correctly. Do NOT put `workflows/` outside the `@` alias root.
**Without `src/` (recommended for WDK projects):**
```
workflows/
my-workflow.ts ← workflow definition ("use workflow" + "use step")
app/api/
my-workflow/route.ts ← POST handler: start(workflow, args) → { runId }
readable/[runId]/route.ts ← GET handler: SSE stream from run.getReadable()
run/[runId]/route.ts ← GET handler: run status via getRun(runId)
```
tsconfig.json paths: `"@/*": ["./*"]` — `@/workflows/my-workflow` resolves to `./workflows/my-workflow`.
**With `src/` directory:** Put workflows inside `src/`:
```
src/
workflows/my-workflow.ts
app/api/my-workflow/route.ts
app/api/readable/[runId]/route.ts
app/api/run/[runId]/route.ts
```
tsconfig.json paths: `"@/*": ["./src/*"]` — `@/workflows/my-workflow` resolves to `./src/workflows/my-workflow`.
**Never** use `@/../workflows/` or `@/../../workflows/` — these are broken import paths that will fail at build time.
### 1. Workflow Definition (`workflows/my-workflow.ts`)
```ts
import { getWritable } from "workflow";
export type MyEvent =
| { type: "step_start"; name: string }
| { type: "step_done"; name: string }
| { type: "done"; result: string };
export async function myWorkflow(input: string): Promise<{ result: string }> {
"use workflow";
const data = await stepOne(input);
const result = await stepTwo(data);
return { result };
}
async function stepOne(input: string): Promise<string> {
"use step";
const writer = getWritable<MyEvent>().getWriter();
try {
await writer.write({ type: "step_start", name: "stepOne" });
// Full Node.js access here — fetch, db calls, etc.
const result = await doWork(input);
await writer.write({ type: "step_done", name: "stepOne" });
return result;
} finally {
writer.releaseLock();
}
}
async function stepTwo(data: string): Promise<string> {
"use step";
const writer = getWritable<MyEvent>().getWriter();
try {
await writer.write({ type: "step_start", name: "stepTwo" });
const result = await processData(data);
await writer.write({ type: "step_done", name: "stepTwo" });
return result;
} finally {
writer.releaseLock();
}
}
```
### 2. Start Route (`app/api/my-workflow/route.ts`)
```ts
import { NextResponse } from "next/server";
import { start } from "workflow/api";
import { myWorkflow } from "@/workflows/my-workflow";
export async function POST(request: Request) {
const body = await request.json();
const run = await start(myWorkflow, [body.input]);
return NextResponse.json({ runId: run.runId });
}
```
**IMPORTANT**: Never call the workflow function directly. Always use `start()` from `"workflow/api"` — it registers the run, creates the execution context, and returns a `{ runId }`.
### 3. Readable Stream Route (`app/api/readable/[runId]/route.ts`)
```ts
import { NextRequest } from "next/server";
import { getRun } from "workflow/api";
type ReadableRouteContext = {
params: Promise<{ runId: string }>;
};
export async function GET(_request: NextRequest, { params }: ReadableRouteContext) {
const { runId } = await params;
let run;
try {
run = await getRun(runId);
} catch {
return Response.json(
{ ok: false, error: { code: "RUN_NOT_FOUND", message: `Run ${runId} not found` } },
{ status: 404 }
);
}
const readable = run.getReadable();
const encoder = new TextEncoder();
const sseStream = (readable as unknown as ReadableStream).pipeThrough(
new TransformStream({
transform(chunk, controller) {
const data = typeof chunk === "string" ? chunk : JSON.stringify(chunk);
controller.enqueue(encoder.encode(`data: ${data}\n\n`));
},
})
);
return new Response(sseStream, {
headers: {
"Content-Type": "text/event-stream; charset=utf-8",
"Cache-Control": "no-cache, no-transform",
Connection: "keep-alive",
"X-Accel-Buffering": "no",
},
});
}
```
### 4. Run Status Route (`app/api/run/[runId]/route.ts`)
```ts
import { NextResponse } from "next/server";
import { getRun } from "workflow/api";
type RunRouteContext = {
params: Promise<{ runId: string }>;
};
export async function GET(_request: Request, { params }: RunRouteContext) {
const { runId } = await params;
let run;
try {
run = await getRun(runId);
} catch {
return NextResponse.json(
{ ok: false, error: { code: "RUN_NOT_FOUND", message: `Run ${runId} not found` } },
{ status: 404 }
);
}
const [status, workflowName, createdAt, startedAt, completedAt] =
await Promise.all([
run.status,
run.workflowName,
run.createdAt,
run.startedAt,
run.completedAt,
]);
return NextResponse.json({
runId,
status,
workflowName,
createdAt: createdAt.toISOString(),
startedAt: startedAt?.toISOString() ?? null,
completedAt: completedAt?.toISOString() ?? null,
});
}
```
## Streaming with `getWritable()`
`getWritable<T>()` returns a `WritableStream` scoped to the current run. Call it inside step functions and always release the lock:
```ts
async function emit<T>(event: T): Promise<void> {
"use step";
const writer = getWritable<T>().getWriter();
try {
await writer.write(event);
} finally {
writer.releaseLock();
}
}
```
Consumers read via `getRun(runId).getReadable()` in the readable route (see above).
**Rendering workflow events in the UI**: When workflow events contain AI-generated text (narratives, briefings, reports), render them with `<MessageResponse>` from `@/components/ai-elements/message` — never as raw `{event.content}`. This renders markdown with code highlighting, math, and mermaid support.
```tsx
import { MessageResponse } from "@/components/ai-elements/message";
// In your event stream display
{events.map(event => (
event.type === "narrative" && <MessageResponse>{event.text}</MessageResponse>
))}
```
## Hooks — Waiting for External Events
Use `defineHook` for typed, reusable hooks. **Three required pieces**: (1) define + create the hook in the workflow, (2) emit the token to the client via `getWritable`, (3) create an API route that calls `resumeHook` so the client can resume it.
### 1. Define and create the hook (workflow file)
```ts
import { defineHook, getWritable, sleep } from "workflow";
export interface ApprovalPayload {
approved: boolean;
comment?: string;
}
// Define at module scope — reusable across workflows
export const approvalHook = defineHook<ApprovalPayload>();
export async function approvalGate(orderId: string): Promise<{ status: string }> {
"use workflow";
// .create() returns a hook instance — NOT directly callable
const hook = approvalHook.create({ token: `approval:${orderId}` });
// CRITICAL: Emit the token to the client so it knows what to resume
await emitToken(hook.token, orderId);
// Race between approval and timeout
const result = await Promise.race([
hook.then((payload) => ({ type: "approval" as const, payload })),
sleep("24h").then(() => ({ type: "timeout" as const, payload: null })),
]);
if (result.type === "timeout") {
return { status: "timeout" };
}
在 GitHub 查看