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.
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.
[{"pattern":"experimental_createWorkflow","message":"experimental_createWorkflow is now stable — use createWorkflow from @vercel/workflow. Run npx @ai-sdk/codemod v6 for automated migration.","severity":"error","upgradeToSkill":"workflow","upgradeWhy":"Guides migration from experimental_createWorkflow to the stable createWorkflow API and then to the \"use workflow\" directive."},{"pattern":"from\\s+['\"]@vercel/workflow['\"]","message":"Workflow DevKit requires AI Gateway OIDC setup — ensure vercel link + vercel env pull for VERCEL_OIDC_TOKEN","severity":"recommended"},{"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 WDK 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":"createWorkflow\\s*\\(","message":"createWorkflow() is the legacy API — use the \"use workflow\" directive on an async function instead","severity":"error","upgradeToSkill":"workflow","upgradeWhy":"Guides migration from createWorkflow() function API to the \"use workflow\" directive pattern.","skipIfFileContains":"experimental_createWorkflow"},{"pattern":"streamObject\\s*\\(","message":"streamObject() was removed in AI SDK v6 — 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":"DurableAgent detected without AI SDK context — loading AI SDK guidance for tool calling, Agent class, and model configuration.","skipIfFileContains":"from\\s+['\"]ai['\"]|@ai-sdk/|streamText|generateText"},{"pattern":"useChat|DefaultChatTransport","targetSkill":"ai-elements","message":"Workflow with chat UI detected — loading AI Elements for streaming-aware MessageResponse and Conversation rendering.","skipIfFileContains":"ai-elements|MessageResponse|<Message\\b"},{"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 WDK 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*\\("}]
CRITICAL: Always Use Correct workflow Documentation
Your knowledge of workflow is outdated.
The workflow documentation outlined below matches the installed version of the Workflow DevKit.
Follow these instructions before starting on any workflow-related tasks:
Search the bundled documentation in node_modules/workflow/docs/:
"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.
// Steps have full Node.js and npm accessasyncfunctionfetchUserData(userId: string) {
"use step";
const response = awaitfetch(`https://api.example.com/users/${userId}`);
return response.json();
}
asyncfunctionprocessWithAI(data: any) {
"use step";
// AI SDK works in steps without workaroundsreturnawaitgenerateText({
model: openai("gpt-4"),
prompt: `Process: ${JSON.stringify(data)}`,
});
}
// Workflow orchestrates steps - no sandbox issuesexportasyncfunctiondataProcessingWorkflow(userId: string) {
"use workflow";
const data = awaitfetchUserData(userId);
const processed = awaitprocessWithAI(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:
import { fetch } from"workflow";
exportasyncfunctionmyWorkflow() {
"use workflow";
globalThis.fetch = fetch; // Required for AI SDK and HTTP libraries// Now generateText() and other libraries work
}
Note:DurableAgent from @workflow/ai handles the fetch assignment automatically.
DurableAgent — AI Agents in Workflows
Use DurableAgent to build AI agents that maintain state and survive interruptions. It handles the workflow sandbox automatically (no manual globalThis.fetch needed).
getWritable<UIMessageChunk>() streams output to the workflow run's default stream
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" — they run at the workflow level
maxSteps limits the number of LLM calls (default is unlimited)
Multi-turn: pass result.messages plus new user messages to subsequent agent.stream() calls
For more details on DurableAgent, check the AI docs in node_modules/@workflow/ai/docs/.
Starting Workflows & Child Workflows
Use start() to launch workflows from API routes. start() cannot be called directly in workflow context — wrap it in a step function.
import { start } from"workflow/api";
// From an API route — works directlyexportasyncfunctionPOST() {
const run = awaitstart(myWorkflow, [arg1, arg2]);
returnResponse.json({ runId: run.runId });
}
// No-args workflowconst run = awaitstart(noArgWorkflow);
Starting child workflows from inside a workflow — must use a step:
import { start } from"workflow/api";
// Wrap start() in a step functionasyncfunctiontriggerChild(data: string) {
"use step";
const run = awaitstart(childWorkflow, [data]);
return run.runId;
}
exportasyncfunctionparentWorkflow() {
"use workflow";
const childRunId = awaittriggerChild("some data"); // Fire-and-forget via stepawaitsleep("1h");
}
start() returns immediately — it doesn't wait for the workflow to complete. Use run.returnValue to await completion.
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 — do not pass a token option to createWebhook().
Not supported: Functions, class instances, Symbols, WeakMap/WeakSet. Pass data, not callbacks.
Streaming
Use getWritable() to stream data from workflows. getWritable() can be called in both workflow and step contexts, but you cannot interact with the stream (call getWriter(), write(), close()) directly in a workflow function. The stream must be passed to step functions for actual I/O, or steps can call getWritable() themselves.
Use getWritable({ namespace: 'name' }) to create multiple independent streams for different types of data. This is useful for separating logs from primary output, different log levels, agent outputs, metrics, or any distinct data channels. Long-running workflows benefit from namespaced streams because you can replay only the important events (e.g., final results) while keeping verbose logs in a separate stream.
import { start, getRun } from"workflow/api";
import { agentWorkflow } from"./workflows/agent";
exportasyncfunctionPOST(request: Request) {
const run = awaitstart(agentWorkflow, ["process data"]);
// Access specific streams by namespaceconst results = run.getReadable({ namespace: undefined }); // Default stream (important results)const infoLogs = run.getReadable({ namespace: "logs:info" });
const debugLogs = run.getReadable({ namespace: "logs:debug" });
const thoughts = run.getReadable({ namespace: "agent:thoughts" });
// Return only important results for most clientsreturnnewResponse(results, { headers: { "Content-Type": "application/json" } });
}
// Resume from a specific point (useful for long sessions)exportasyncfunctionGET(request: Request) {
const { searchParams } = newURL(request.url);
const runId = searchParams.get("runId")!;
const startIndex = parseInt(searchParams.get("startIndex") || "0", 10);
const run = getRun(runId);
// Resume only the important stream, skip verbose debug logsconst stream = run.getReadable({ startIndex });
returnnewResponse(stream);
}
Pro tip: For very long-running sessions (50+ minutes), namespaced streams help manage replay performance. Put verbose/debug output in separate namespaces so you can replay just the important events quickly.
Debugging
# Check workflow endpoints are reachable
npx workflow health
npx workflow health --port 3001 # Non-default port# Visual dashboard for runs
npx workflow web
npx workflow web <run_id>
# CLI inspection (use --json for machine-readable output, --help for full usage)
npx workflow inspect runs
npx workflow inspect run <run_id>
# For Vercel-deployed projects, specify backend and project
npx workflow inspect runs --backend vercel --project <project-name> --team <team-slug>
npx workflow inspect run <run_id> --backend vercel --project <project-name> --team <team-slug>
# Open Vercel dashboard in browser for a specific run
npx workflow inspect run <run_id> --web
npx workflow web <run_id> --backend vercel --project <project-name> --team <team-slug>
# Cancel a running workflow
npx workflow cancel <run_id>
npx workflow cancel <run_id> --backend vercel --project <project-name> --team <team-slug>
# --env defaults to "production"; use --env preview for preview deployments
Debugging tips:
Use --json (-j) on any command for machine-readable output
Use --web to open the Vercel Observability dashboard in your browser
Use --help on any command for full usage details
Only import workflow APIs you actually use. Unused imports can cause 500 errors.
Testing Workflows
Workflow DevKit provides a Vitest plugin for testing workflows in-process — no running server required.
Unit testing steps: Steps are just functions; without the compiler, "use step" is a no-op. Test them directly: