vercel-workflow
Vercel Workflow DevKit
Build durable, resumable workflows that survive restarts, deployments, and failures using TypeScript directives.
Quick Start (Next.js)
npm i workflow
// next.config.ts
import { withWorkflow } from "workflow/next";
export default withWorkflow({});
// workflows/signup.ts
import { sleep, FatalError } from "workflow";
export async function signupWorkflow(email: string) {
"use workflow";
const user = await createUser(email);
await sendWelcome(user.id, email);
await sleep("3d");
await sendFollowUp(user.id);
return { success: true };
}
async function createUser(email: string) {
"use step";
return { id: crypto.randomUUID(), email };
}
async function sendWelcome(userId: string, email: string) {
"use step";
const res = await fetch("https://api.email.com/send", {
method: "POST",
body: JSON.stringify({ to: email, template: "welcome" })
});
if (!res.ok) throw new Error("Failed"); // Auto-retried
}
async function sendFollowUp(userId: string) {
"use step";
// FatalError = no retry
if (!userId) throw new FatalError("Missing userId");
// ... send email
}
// app/api/signup/route.ts
import { start } from "workflow/api";
import { signupWorkflow } from "@/workflows/signup";
export async function POST(request: Request) {
const { email } = await request.json();
const run = await start(signupWorkflow, [email]);
return Response.json({ runId: run.id });
}
Core Concepts
The Two Directives
| Directive | Purpose | Rules |
|---|---|---|
"use workflow" |
Orchestrates steps, sleeps, suspends | Deterministic: No side effects, no Node.js modules, no global fetch |
"use step" |
Contains business logic, I/O, side effects | Runs on separate request: Auto-retries on failure |
Why These Restrictions?
Workflow DevKit uses event sourcing - state changes are stored as events and replayed to reconstruct state. Workflows must be deterministic so replays produce identical step sequences. Steps run on separate requests, so data passes by value (mutations don't affect workflow variables).
Data Flow: Pass-by-Value
// WRONG - mutations lost
async function workflow() {
"use workflow";
const user = { name: "Alice" };
await updateUser(user);
console.log(user.name); // Still "Alice"!
}
// CORRECT - return modified data
async function workflow() {
"use workflow";
let user = { name: "Alice" };
user = await updateUser(user);
console.log(user.name); // "Bob"
}
async function updateUser(user: User) {
"use step";
user.name = "Bob";
return user; // Must return!
}
API Quick Reference
workflow package
import {
sleep, // Suspend for duration/until date
fetch, // HTTP with auto-retry (use in workflows)
FatalError, // Non-retryable error
RetryableError, // Explicit retry with delay
createHook, // Receive external payloads
createWebhook, // Receive HTTP requests
defineHook, // Type-safe hooks with validation
getWritable, // Stream output
getWorkflowMetadata,// { workflowRunId, workflowStartedAt, url }
getStepMetadata // { stepId } - use for idempotency keys
} from "workflow";
workflow/api package
import {
start, // Start workflow run
getRun, // Get run status
resumeHook, // Resume via hook token
resumeWebhook, // Resume via webhook token
getHookByToken // Get hook details
} from "workflow/api";
Sleep & Scheduling
await sleep("10s"); // seconds
await sleep("5m"); // minutes
await sleep("2h"); // hours
await sleep("1d"); // days
await sleep("2w"); // weeks
await sleep(5000); // milliseconds
await sleep(new Date("2025-12-25")); // until date
Error Handling
import { FatalError, RetryableError } from "workflow";
async function processPayment(amount: number) {
"use step";
try {
return await paymentAPI.charge(amount);
} catch (error) {
// Don't retry invalid requests
if (error.code === "INVALID_CARD") {
throw new FatalError("Invalid card");
}
// Explicit retry with delay
if (error.code === "RATE_LIMITED") {
throw new RetryableError("Rate limited", { retryAfter: "5m" });
}
// Other errors auto-retry with default backoff
throw error;
}
}
Hooks (External Events)
import { createHook, defineHook } from "workflow";
import { resumeHook } from "workflow/api";
import { z } from "zod";
// In workflow - create hook and wait
export async function approvalWorkflow(orderId: string) {
"use workflow";
const hook = createHook<{ approved: boolean; comment: string }>({
token: `approval:${orderId}` // Custom token for deterministic recovery
});
await notifyReviewer(orderId, hook.token);
const result = await hook; // Suspends until resumed
if (result.approved) await processOrder(orderId);
}
// In API route - resume with payload
export async function POST(request: Request) {
const { token, approved, comment } = await request.json();
await resumeHook(token, { approved, comment });
return Response.json({ success: true });
}
// Type-safe hooks with validation
const approvalHook = defineHook(z.object({
approved: z.boolean(),
comment: z.string().max(500)
}));
// Use: approvalHook.create(), approvalHook.resume(token, payload)
Idempotency
Use stepId for idempotency keys - stable across retries, unique per step:
import { getStepMetadata } from "workflow";
async function chargeCustomer(customerId: string, amount: number) {
"use step";
const { stepId } = getStepMetadata();
return stripe.charges.create({
customer: customerId,
amount,
idempotency_key: `charge:${stepId}`
});
}
Streaming
import { getWritable } from "workflow";
export async function streamingWorkflow() {
"use workflow";
const writable = getWritable<{ progress: number }>();
await streamProgress(writable);
await closeStream(writable);
}
// IMPORTANT: All stream operations must happen in steps
async function streamProgress(writable: WritableStream) {
"use step";
const writer = writable.getWriter();
try {
for (let i = 0; i <= 100; i += 10) {
await writer.write({ progress: i });
}
} finally {
writer.releaseLock(); // Always release!
}
}
async function closeStream(writable: WritableStream) {
"use step";
await writable.close();
}
// Consume in API route
export async function GET() {
const run = await start(streamingWorkflow);
return new Response(run.readable);
}
// Namespaced streams
const dataStream = getWritable({ namespace: "data" });
const logsStream = getWritable({ namespace: "logs" });
AI Agents with @workflow/ai
import { DurableAgent } from "@workflow/ai";
import { fetch, getWritable } from "workflow";
import { z } from "zod";
export async function chatWorkflow(messages: UIMessage[]) {
"use workflow";
// CRITICAL: Enable fetch for AI SDK
globalThis.fetch = fetch;
const writable = getWritable<UIMessageChunk>();
const agent = new DurableAgent({
model: "anthropic/claude-haiku-4.5",
system: "You are a helpful assistant.",
tools: {
searchWeb: {
description: "Search the web",
parameters: z.object({ query: z.string() }),
execute: async ({ query }) => {
"use step";
return await searchAPI(query);
}
}
}
});
const result = await agent.stream({
messages,
writable,
maxSteps: 10
});
return result.messages;
}
Observability & Debugging
npx workflow web # Visual web UI
npx workflow inspect runs # List recent runs
npx workflow inspect run <id> # Detailed run info
npx workflow inspect steps <id> # List steps
# Production (Vercel)
npx workflow inspect runs --backend vercel --env production
Deployment
Vercel (recommended): vercel deploy - no config needed
Other backends: Set WORKFLOW_TARGET_WORLD:
@workflow/world-local(default dev)@workflow-worlds/postgres@workflow-worlds/turso@workflow-worlds/mongodb@workflow-worlds/redis
Common Patterns
See references/patterns.md for:
- Parallel execution (Promise.all)
- Saga pattern (compensating transactions)
- Circuit breaker
- Scheduled/recurring workflows
- Human-in-the-loop
- Single vs multi-turn chat sessions
Debugging & Errors
See references/debugging.md for:
- All error types and solutions
- Logging best practices
- Testing strategies
- Performance tips
- Middleware configuration
More from jakerains/agentskills
shot-list
Generate professional shot lists from screenplays and scripts. Use when user uploads a screenplay (.fountain, .fdx, .txt, .pdf, .docx) or describes scenes for production planning. Parses scripts to extract scenes, helps determine camera setups, shot types, framing, and movement through collaborative discussion, then generates beautifully formatted PDF shot lists for production. Triggers include requests to create shot lists, plan shots, break down scripts for filming, or organize camera coverage.
27nextjs-pwa
Build Progressive Web Apps with Next.js: service workers, offline support, caching strategies, push notifications, install prompts, and web app manifest. Use when creating PWAs, adding offline capability, configuring service workers, implementing push notifications, handling install prompts, or optimizing PWA performance. Triggers: PWA, progressive web app, service worker, offline, cache strategy, web manifest, push notification, installable app, Serwist, next-pwa, workbox, background sync.
9elevenlabs
Complete ElevenLabs AI audio platform: text-to-speech (TTS), speech-to-text (STT/Scribe), voice cloning, voice design, sound effects, music generation, dubbing, voice changer, voice isolator, and conversational voice agents. Use when working with audio generation, voice synthesis, transcription, audio processing, or building voice-enabled applications. Triggers: generate speech, clone voice, transcribe audio, create sound effects, compose music, dub video, change voice, isolate vocals, build voice agent, ElevenLabs API/SDK/CLI/MCP.
9onnx-webgpu-converter
Convert HuggingFace transformer models to ONNX format for browser inference with Transformers.js and WebGPU. Use when given a HuggingFace model link to convert to ONNX, when setting up optimum-cli for ONNX export, when quantizing models (fp16, q8, q4) for web deployment, when configuring Transformers.js with WebGPU acceleration, or when troubleshooting ONNX conversion errors. Triggers on mentions of ONNX conversion, Transformers.js, WebGPU inference, optimum export, model quantization for browser, or running ML models in the browser.
8skill-seekers
Convert documentation websites, GitHub repositories, and PDFs into Claude AI skills. Use when creating Claude skills from docs, scraping documentation, packaging websites into skills, or converting repos/PDFs to Claude knowledge.
7apple-foundation-models
Build Apple Intelligence features with Foundation Models and Image Playground on iOS 26+, iPadOS 26+, macOS 26+, Mac Catalyst 26+, and visionOS 26+. Use when implementing SystemLanguageModel, LanguageModelSession, guided generation with @Generable/@Guide, tool calling, streaming responses, prompt design, safety and guardrail handling, model availability checks, content tagging, context-window limits, local on-device inference, routing to larger-model paths, adapters, and ImagePlayground/ImageCreator APIs. Covers model capabilities and limitations, structured output, error handling, and SwiftUI integration patterns.
7