- name
- workflow
- description
- Vercel Workflow SDK 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/workflows","https://workflow-sdk.dev"],"sitemap":"https://vercel.com/sitemap.xml","pathPatterns":["lib/workflow/**","src/lib/workflow/**","lib/workflow.*","src/lib/workflow.*","workflow.*","*workflow*"],"importPatterns":["workflow","@workflow/*","*workflow*"],"bashPatterns":["\\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 sdk","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","[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}}
- validate
- [{"pattern":"setTimeout|setInterval","message":"setTimeout/setInterval are not available in workflow sandbox scope — use sleep() from \"workflow\" for delays","severity":"error","skipIfFileContains":"use step"},{"pattern":"context\\.run\\s*\\(","message":"context.run() is not a Workflow SDK pattern — use \"use step\" directive for retryable, observable steps","severity":"error","upgradeToSkill":"workflow","upgradeWhy":"Guides migration from context.run() to the \"use step\" directive for durable, retryable workflow steps."},{"pattern":"\\brequire\\s*\\(","message":"require() is not available in workflow sandbox scope — use ESM imports and move Node.js logic into \"use step\" functions","severity":"error","skipIfFileContains":"use step"},{"pattern":"getWritable\\(\\)","message":"getWritable() must only be called inside \"use step\" functions — workflow sandbox scope does not support it","severity":"recommended","skipIfFileContains":"use step"},{"pattern":"streamObject\\s*\\(","message":"streamObject() is deprecated since AI SDK 6 — use streamText() with output: Output.object() instead","severity":"error","upgradeToSkill":"ai-sdk","upgradeWhy":"Guides migration from streamObject to streamText + Output.object() with correct v6 streaming patterns."},{"pattern":"await\\s+\\w+Workflow\\s*\\(","message":"Do not call workflow functions directly — use start() from \"workflow/api\" to register the run and get a runId","severity":"recommended","skipIfFileContains":"use workflow"},{"pattern":"\\bfetch\\s*\\(","message":"Native fetch() is not available in workflow sandbox scope — import fetch from \"workflow\" or move the call into a \"use step\" function","severity":"recommended","skipIfFileContains":"use step"},{"pattern":"\"use step\"","message":"Workflow steps should include console.log or structured logging for observability — add logging at step entry/exit to debug hangs","severity":"warn","skipIfFileContains":"console\\.(log|warn|error|info)"},{"pattern":"\"use workflow\"","message":"Workflow files should import and use logging — add console.log or a logger at key execution points for debugging","severity":"warn","skipIfFileContains":"console\\.(log|warn|error|info)"}]
- chainTo
- [{"pattern":"DurableAgent|@workflow/ai","targetSkill":"ai-sdk","message":"Workflow 5 (workflow@latest) deprecates DurableAgent (@workflow/ai) in favor of WorkflowAgent from @ai-sdk/workflow 2.x, which requires Workflow 5; the Workflow 4 docs (workflow@4) use DurableAgent. Loading AI SDK guidance for tool calling, the Agent class, and model configuration.","skipIfFileContains":"from\\s+['\"]ai['\"]|@ai-sdk/|streamText|generateText"},{"pattern":"process\\.env\\.(OPENAI_API_KEY|ANTHROPIC_API_KEY)|from\\s+['\"]@ai-sdk/(anthropic|openai)['\"\"]","targetSkill":"ai-gateway","message":"Direct provider API key in workflow — loading AI Gateway guidance for OIDC auth (required for Workflow SDK AI steps).","skipIfFileContains":"gateway\\(|@ai-sdk/gateway|VERCEL_OIDC"},{"pattern":"setTimeout\\s*\\(|setInterval\\s*\\(","targetSkill":"vercel-functions","message":"Timer-based delay in workflow code — use sleep() from \"workflow\" instead of setTimeout/setInterval. Loading Vercel Functions guidance.","skipIfFileContains":"from\\s+['\"]workflow['\"].*sleep|sleep\\s*\\("}]
- retrieval
- {"aliases":["durable workflow","long running task","step function","orchestration"],"intents":["build workflow","add retry logic","create durable task","implement step function"],"entities":["Workflow SDK","Workflow DevKit","WDK","step","pause/resume","durable"]}
## *Critical*: Always use correct `workflow` documentation
Your knowledge of `workflow` is outdated.
The `workflow` documentation outlined below matches the installed version of the Workflow SDK.
Follow these instructions before starting on any `workflow`-related tasks:
Search the bundled documentation in `node_modules/workflow/docs/`:
1. **Find docs**: `glob "node_modules/workflow/docs/**/*.mdx"`
2. **Search content**: `grep "your query" node_modules/workflow/docs/`
Documentation structure in `node_modules/workflow/docs/`:
- `getting-started/` - Framework setup (next.mdx, express.mdx, hono.mdx, etc.)
- `foundations/` - Core concepts (workflows-and-steps.mdx, hooks.mdx, streaming.mdx, etc.)
- `api-reference/workflow/` - API docs (sleep.mdx, create-hook.mdx, fatal-error.mdx, etc.)
- `api-reference/workflow-api/` - Client API (start.mdx, get-run.mdx, resume-hook.mdx, etc.)
- `api-reference/workflow-runtime/` - Runtime API (get-world.mdx) and `world/` World SDK (storage.mdx, streams.mdx, queue.mdx)
- `api-reference/workflow-observability/` - Hydration and name parsing utilities (hydrate-resource-io.mdx, parse-workflow-name.mdx, etc.)
- `ai/`: AI SDK integration docs
- `errors/` - Error code documentation
- `worlds/` - Per-World behavior and limits (vercel.mdx, local.mdx, postgres.mdx). Other pages link these as `/worlds/<name>`.
Related packages also include bundled docs:
- `@ai-sdk/workflow`: `node_modules/ai/docs/` - WorkflowAgent and AI SDK integration
- `@workflow/ai`: `node_modules/@workflow/ai/docs/` - deprecated DurableAgent APIs for existing applications
- `@workflow/core`: `node_modules/@workflow/core/docs/` - Core runtime (foundations, how-it-works)
- `@workflow/next`: `node_modules/@workflow/next/docs/` - Next.js integration
**When in doubt, update to the latest version of the Workflow SDK.**
### Official resources
- **Website**: https://workflow-sdk.dev
- **GitHub**: https://github.com/vercel/workflow
### Quick reference
**Directives:**
```typescript
"use workflow"; // First line - makes async function durable
"use step"; // First line - makes function a cached, retryable unit
```
**Essential imports:**
```typescript
// Workflow primitives
import { sleep, fetch, createHook, createWebhook, getWritable } from "workflow";
import { FatalError, RetryableError } from "workflow";
import { getWorkflowMetadata, getStepMetadata } from "workflow";
// API operations
import { start, getRun, resumeHook, resumeWebhook } from "workflow/api";
// Observability & data hydration
import { hydrateResourceIO, observabilityRevivers, parseStepName, parseWorkflowName } from "workflow/observability";
// Framework integrations
import { withWorkflow } from "workflow/next";
import { workflow } from "workflow/vite";
import { workflow } from "workflow/astro";
// Or use modules: ["workflow/nitro"] for Nitro/Nuxt
// AI agent (Workflow 5)
import { WorkflowAgent, type ModelCallStreamPart } from "@ai-sdk/workflow";
```
## Prefer step functions to avoid sandbox errors
`"use workflow"` functions run in a sandboxed VM. `"use step"` functions have **full Node.js access**. Put your logic in steps and use the workflow function purely for orchestration.
```typescript
// Steps have full Node.js and npm access
async function fetchUserData(userId: string) {
"use step";
const response = await fetch(`https://api.example.com/users/${userId}`);
return response.json();
}
async function processWithAI(data: any) {
"use step";
// AI SDK works in steps without workarounds
return await generateText({
model: "spacexai/grok-4.6",
prompt: `Process: ${JSON.stringify(data)}`,
});
}
// Workflow orchestrates steps - no sandbox issues
export async function dataProcessingWorkflow(userId: string) {
"use workflow";
const data = await fetchUserData(userId);
const processed = await processWithAI(data);
return { success: true, processed };
}
```
**Benefits:** Steps have automatic retry, results are persisted for replay, and no sandbox restrictions.
## Workflow sandbox limitations
When you need logic directly in a workflow function (not in a step), these restrictions apply:
| Limitation | Workaround |
|------------|------------|
| No `fetch()` | `import { fetch } from "workflow"` then `globalThis.fetch = fetch` |
| No `setTimeout`/`setInterval` | Use `sleep("5s")` from `"workflow"` |
| No Node.js modules (fs, crypto, etc.) | Move to a step function |
**Example - Using fetch in workflow context:**
```typescript
import { fetch } from "workflow";
export async function myWorkflow() {
"use workflow";
globalThis.fetch = fetch; // Required for AI SDK and HTTP libraries
// Now generateText() and other libraries work
}
```
**Note:** Plain `"provider/model"` strings use Vercel AI Gateway. Do not construct a direct provider instance unless the user explicitly needs a provider-only feature.
## WorkflowAgent: AI agents in Workflow 5
Use AI SDK's `WorkflowAgent` for durable agents on Workflow 5. It replaces the deprecated `DurableAgent` API from `@workflow/ai` and checkpoints model calls and step-backed tools.
```typescript
import { WorkflowAgent, type ModelCallStreamPart } from "@ai-sdk/workflow";
import { isStepCount, tool } from "ai";
import { getWritable } from "workflow";
import { z } from "zod";
async function lookupData({ query }: { query: string }) {
"use step";
// Step functions have full Node.js access
return `Results for "${query}"`;
}
export async function myAgentWorkflow(userMessage: string) {
"use workflow";
const agent = new WorkflowAgent({
model: "spacexai/grok-4.6",
instructions: "You are a helpful assistant.",
tools: {
lookupData: tool({
description: "Search for information",
inputSchema: z.object({ query: z.string() }),
execute: lookupData,
}),
},
});
const result = await agent.stream({
messages: [{ role: "user", content: userMessage }],
writable: getWritable<ModelCallStreamPart>(),
stopWhen: isStepCount(10),
});
return result.messages;
}
```
**Key points:**
- A plain `"provider/model"` string routes through Vercel AI Gateway; `spacexai/grok-4.6` is the default model in Workflow examples
- `getWritable<ModelCallStreamPart>()` streams durable model-call output; convert it with `createModelCallToUIChunkTransform()` in an HTTP route
- Tool `execute` functions that need Node.js/npm access should use `"use step"`
- Tool `execute` functions that use workflow primitives (`sleep()`, `createHook()`) should **NOT** use `"use step"` because they run at the workflow level
- `stopWhen` limits the number of model calls; the default is to stop when the model stops calling tools
- Multi-turn: pass `result.messages` plus new user messages to subsequent `agent.stream()` calls
**For more details, check the WorkflowAgent docs in the installed AI SDK package or at https://ai-sdk.dev/v7/docs/agents/workflow-agent.**
## Starting workflows & child workflows
Use `start()` to launch workflows from API routes. In Workflow 5, `start()` can also be called directly from a workflow function to spawn a child run; it is step-backed and records a deterministic boundary in the parent's event log.
```typescript
import { start } from "workflow/api";
// From an API route; works directly
export async function POST() {
const run = await start(myWorkflow, [arg1, arg2]);
return Response.json({ runId: run.runId });
}
// No-args workflow
const run = await start(noArgWorkflow);
```
**Starting child workflows from inside a Workflow 5 workflow:**
```typescript
import { start } from "workflow/api";
export async function parentWorkflow() {
"use workflow";
const childRun = await start(childWorkflow, ["some data"]);
await sleep("1h");
return { childRunId: childRun.runId };
}
```
`start()` returns after creating the child run and doesn't wait for it to complete. Use `childRun.returnValue` only when the parent should wait for the child; each `Run` property access or method call inside a workflow is a step.
## Run size & concurrency: know when to split
Do NOT treat any number you remember as authoritative — the current values are published under [Workflow run limits](https://vercel.com/docs/workflows/pricing#workflow-run-limits).
**Events per run.** A run's event log is capped, and the run fails with `MAX_EVENTS_EXCEEDED` past the ceiling. Events are not steps: a step that succeeds on the first try records three (`step_created`, `step_started`, `step_completed`), a retry records one or two more, and hooks, sleeps, and webhooks each record their own. Split into child workflows well before the ceiling — the pricing page recommends that past **a few thousand events**, because replay slows down long before the run fails.
**Steps per run.** Capped implicitly through the event limit. Bundle several items into one step when a chain would otherwise reach five figures.
**Concurrency.** A wide fan-out is throttled rather than rejected: event creation is rate-limited per run per second, so a flat `Promise.all` over a few thousand items spends much of its time backing off. Batch or bundle instead — process the list in chunks, or handle several items per step, so fewer and larger units run concurrently. Spawning one child run per item does not by itself narrow the fan-out; it bounds each child's log and isolates failures, which is worth doing for those reasons, but it is not a substitute for chunking.
You cannot raise any of these yourself — `WORKFLOW_MAX_EVENTS_OVERRIDE` only clamps *down*, and on the Vercel World the ceilings are service-owned — but Vercel raises the per-run event and step limits on request, so a genuinely large run is a support question as well as a design one.
```typescript
const BATCH = 100;
async function processItem(item: string) {
"use step";
return item.toUpperCase();
}
// One step per item, all in flight at once, all in one log
export async function processAll(items: string[]) {
"use workflow";
await Promise.all(items.map((item) => processItem(item)));
}
// Chunked, so only BATCH steps are in flight at a time
export async function processBatched(items: string[]) {
"use workflow";
for (let i = 0; i < items.length; i += BATCH) {
await Promise.allSettled(items.slice(i, i + BATCH).map((item) => processItem(item)));
}
}
// Bundled, so one step covers many items and the log stays short
async function processChunk(chunk: string[]) {
"use step";
return chunk.map((item) => item.toUpperCase());
}
export async function processBundled(items: string[]) {
"use workflow";
for (let i = 0; i < items.length; i += BATCH) {
await processChunk(items.slice(i, i + BATCH));
}
}
```
`processAll` is the shape to avoid at scale. `processBatched` bounds concurrency but still records events for every item. `processBundled` bounds both, because one step covers `BATCH` items — that is the only one of the three whose event count shrinks as `BATCH` grows.
## Hooks: pause & resume with external events
Hooks let workflows wait for external data. Use `createHook()` inside a workflow and `resumeHook()` from API routes. Deterministic tokens are for `createHook()` + `resumeHook()` (server-side) only. `createWebhook()` always generates random tokens, so do not pass a `token` option to `createWebhook()`.
### Single event
```typescript
import { createHook } from "workflow";
export async function approvalWorkflow() {
"use workflow";
const hook = createHook<{ approved: boolean }>({
token: "approval-123", // deterministic token for external systems
});
const result = await hook; // Workflow suspends here
return result.approved;
}
```
### Multiple events (iterable hooks)
Hooks implement `AsyncIterable`. Use `for await...of` to receive multiple events:
```typescript
import { createHook } from "workflow";
export async function chatWorkflow(channelId: string) {
"use workflow";
const hook = createHook<{ text: string; done?: boolean }>({
token: `chat-${channelId}`,
});
for await (const event of hook) {
await processMessage(event.text);
if (event.done) break;
}
}
```
Each `resumeHook(token, payload)` call delivers the next value to the loop.
### Resuming from API routes
```typescript
import { resumeHook } from "workflow/api";
export async function POST(req: Request) {
const { token, data } = await req.json();
await resumeHook(token, data);
return new Response("ok");
}
```
## Error handling
Use `FatalError` for permanent failures (no retry), `RetryableError` for transient failures:
```typescript
import { FatalError, RetryableError } from "workflow";
if (res.status === 429) {
throw new RetryableError("Rate limited", { retryAfter: "5m" });
}
if (res.status >= 400 && res.status < 500) {
throw new FatalError(`Client error: ${res.status}`);
}
```
## Serialization
All data passed to/from workflows and steps must be serializable.
**Supported built-in types:** string, number, boolean, null, undefined, bigint, plain objects, arrays, Date, RegExp, URL, URLSearchParams, Map, Set, Headers, ArrayBuffer, typed arrays, Request, Response, ReadableStream, WritableStream.
**Not supported:** Functions, Symbols, WeakMap/WeakSet. Pass data, not callbacks.
### Custom class serialization
Class instances **can** be serialized across workflow/step boundaries by implementing the `@workflow/serde` protocol. This is essential when a class has instance methods with `"use step"` or when you want to pass class instances between steps.
**Install:** `@workflow/serde` must be a dependency of the package containing the class.
**Pattern:** Add two static methods inside the class body using computed property syntax:
```typescript
import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from "@workflow/serde";
export class Point {
x: number;
y: number;
constructor(x: number, y: number) {
GitHub에서 보기