vercel-workflow

Best practices for using Vercel Workflow DevKit. Use when creating, modifying, or debugging workflows, steps, hooks, webhooks, or any durable function using the Workflow DevKit. Ensures proper usage of directives, error handling, serialization, streaming, and workflow patterns.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "vercel-workflow" with this command: npx skills add victor-teles/skills/victor-teles-skills-vercel-workflow

Workflow Best Practices

Enforces proper usage of Vercel Workflow DevKit patterns for durable, resumable workflows.

Core Concepts

Directives

Two fundamental directives define execution context:

"use workflow" - Marks orchestration functions that coordinate steps:

export async function processOrder(orderId: string) {
  'use workflow';
  
  const order = await fetchOrder(orderId);  // Step
  await sleep('1h');                         // Suspend
  return await chargePayment(order);        // Step
}

"use step" - Marks atomic operations with full runtime access:

async function fetchOrder(orderId: string) {
  'use step';
  
  // Full Node.js access: database, APIs, file I/O
  return await db.orders.findUnique({ where: { id: orderId } });
}

Critical Rules:

  • Workflow functions must be deterministic - same inputs always produce same outputs
  • Workflows run in a sandboxed environment without Node.js API access
  • Steps have full runtime access and automatic retries on failure
  • All parameters must be serializable (no functions, class instances, closures)

Execution Model

Workflows suspend and resume through:

  1. Step calls - Workflow yields while step executes
  2. sleep() - Pause for duration without consuming resources
  3. Hooks/Webhooks - Wait for external events

During replay, workflows re-execute using cached step results from the event log.

Structure Patterns

Organization

workflows/{feature-name}/
├── index.ts           # Workflow orchestration
├── steps/             # Step functions
│   ├── {action}.ts
│   └── ...
└── hooks/             # Hook definitions
    └── {event}.ts

Workflow Function

import { sleep, createHook } from 'workflow';
import { processData } from './steps/process-data';
import { sendEmail } from './steps/send-email';

export async function myWorkflow(userId: string) {
  'use workflow';
  
  const result = await processData(userId);
  
  await sleep('5m');
  
  await sendEmail({ userId, result });
  
  return { status: 'completed', result };
}

Rules:

  • ONE workflow export per file (main entry point)
  • Orchestrate steps - don't do work directly
  • Use language primitives: Promise.all, for...of, try/catch
  • NO Node.js APIs: fs, http, crypto, process
  • NO side effects: database calls, API requests, mutations
  • Parameters and returns must be serializable

Step Functions

type ProcessDataArgs = {
  userId: string;
  options?: { retry?: boolean };
};

export async function processData(params: ProcessDataArgs) {
  'use step';
  
  // Full Node.js access
  const user = await db.users.findUnique({ where: { id: params.userId } });
  const result = await externalApi.process(user);
  
  return { processed: true, data: result };
}

Rules:

  • ONE step per file is preferred for clarity
  • Use typed parameters (object or single value)
  • Return serializable values only
  • Mutations happen here - not in workflows
  • Can throw errors for automatic retry
  • Use getStepMetadata() for idempotency keys

Error Handling

Automatic Retries

Steps retry automatically (default: 3 attempts):

async function fetchData(url: string) {
  'use step';
  
  // Throws Error - will retry
  const response = await fetch(url);
  if (!response.ok) throw new Error('Fetch failed');
  
  return response.json();
}

Fatal Errors (No Retry)

import { FatalError } from 'workflow';

async function validateUser(userId: string) {
  'use step';
  
  if (!userId) {
    // Don't retry invalid input
    throw new FatalError('User ID is required');
  }
  
  return await db.users.findUnique({ where: { id: userId } });
}

Retryable with Delay

import { RetryableError } from 'workflow';

async function callRateLimitedApi() {
  'use step';
  
  const response = await fetch('https://api.example.com');
  
  if (response.status === 429) {
    // Retry after 10 seconds
    throw new RetryableError('Rate limited', { delay: '10s' });
  }
  
  return response.json();
}

Workflow Error Handling

export async function resilientWorkflow(orderId: string) {
  'use workflow';
  
  try {
    const order = await fetchOrder(orderId);
    await processPayment(order);
  } catch (error) {
    // Log and handle at workflow level
    await logError({ orderId, error: String(error) });
    throw error; // Workflow will fail
  }
}

Serialization

Allowed Types

Primitives, objects, arrays, Date, URL, Headers, Request, Response, ReadableStream, WritableStream.

Pass-by-Value

Parameters are copied, not referenced:

// ❌ WRONG - mutations not visible
export async function badWorkflow() {
  'use workflow';
  
  let counter = 0;
  await updateCounter(counter);
  console.log(counter); // Still 0!
}

async function updateCounter(count: number) {
  'use step';
  count++; // Only mutates the copy
}
// ✅ CORRECT - return modified values
export async function goodWorkflow() {
  'use workflow';
  
  let counter = 0;
  counter = await updateCounter(counter);
  console.log(counter); // 1
}

async function updateCounter(count: number) {
  'use step';
  return count + 1;
}

Forbidden Types

NO functions, class instances, symbols, WeakMaps, closures:

// ❌ WRONG
async function badStep(callback: () => void) {
  'use step';
  callback(); // ERROR: Cannot serialize functions
}

// ✅ CORRECT - use configuration
type Config = { shouldLog: boolean };

async function goodStep(config: Config) {
  'use step';
  if (config.shouldLog) console.log('Done');
}

Hooks & Webhooks

Type-Safe Hooks

import { defineHook } from 'workflow';
import { z } from 'zod';

const approvalHook = defineHook({
  schema: z.object({
    approved: z.boolean(),
    approvedBy: z.string(),
    comment: z.string(),
  }),
});

export async function documentWorkflow(docId: string) {
  'use workflow';
  
  const hook = approvalHook.create({
    token: `approval:${docId}`,
  });
  
  const result = await hook;
  
  return result.approved ? 'approved' : 'rejected';
}

Iterating Over Events

import { createHook } from 'workflow';

export async function monitoringWorkflow(channelId: string) {
  'use workflow';
  
  const hook = createHook<{ message: string }>({
    token: `messages:${channelId}`,
  });
  
  for await (const event of hook) {
    await processMessage(event.message);
    
    if (event.message === 'stop') break;
  }
}

Webhooks

import { createWebhook } from 'workflow';

export async function paymentWorkflow(orderId: string) {
  'use workflow';
  
  const webhook = createWebhook({
    respondWith: new Response('Payment received', { status: 200 }),
  });
  
  await sendPaymentLink({ orderId, webhookUrl: webhook.url });
  
  const request = await webhook;
  const payload = await request.json();
  
  return { paid: true, transactionId: payload.id };
}

Streaming

Writing to Streams (Steps Only)

import { getWritable } from 'workflow';

export async function progressWorkflow() {
  'use workflow';
  
  const writable = getWritable<{ progress: number }>();
  
  await processWithProgress(writable);
  await finalizeStream(writable);
}

async function processWithProgress(writable: WritableStream) {
  'use step';
  
  const writer = writable.getWriter();
  
  try {
    for (let i = 0; i <= 100; i += 10) {
      await writer.write({ progress: i });
      await new Promise(resolve => setTimeout(resolve, 100));
    }
  } finally {
    writer.releaseLock();
  }
}

async function finalizeStream(writable: WritableStream) {
  'use step';
  await writable.close();
}

Critical: Workflows can GET streams but NOT interact with them. Steps must do all writing/closing.

Namespaced Streams

export async function multiStreamWorkflow() {
  'use workflow';
  
  const defaultStream = getWritable();
  const logStream = getWritable({ namespace: 'logs' });
  
  await writeToStreams(defaultStream, logStream);
}

async function writeToStreams(
  defaultStream: WritableStream,
  logStream: WritableStream
) {
  'use step';
  
  const writer1 = defaultStream.getWriter();
  const writer2 = logStream.getWriter();
  
  try {
    await writer1.write({ data: 'main' });
    await writer2.write({ log: 'processing' });
  } finally {
    writer1.releaseLock();
    writer2.releaseLock();
  }
}

Common Patterns

Sequential Steps

export async function sequentialWorkflow(data: unknown) {
  'use workflow';
  
  const validated = await validateData(data);
  const processed = await processData(validated);
  const stored = await storeData(processed);
  
  return stored;
}

Parallel Steps

export async function parallelWorkflow(userId: string) {
  'use workflow';
  
  const [user, orders, payments] = await Promise.all([
    fetchUser(userId),
    fetchOrders(userId),
    fetchPayments(userId),
  ]);
  
  return { user, orders, payments };
}

Conditional Steps

export async function conditionalWorkflow(orderId: string) {
  'use workflow';
  
  const order = await fetchOrder(orderId);
  
  if (order.isPaid) {
    await fulfillOrder(order);
  } else {
    await sendPaymentReminder(order);
  }
}

Loops with Steps

export async function batchWorkflow(items: string[]) {
  'use workflow';
  
  for (const item of items) {
    await processItem(item);
  }
  
  return { processed: items.length };
}

Timeout Pattern

import { sleep } from 'workflow';

export async function timeoutWorkflow(taskId: string) {
  'use workflow';
  
  const result = await Promise.race([
    processTask(taskId),
    sleep('30s').then(() => 'timeout' as const),
  ]);
  
  if (result === 'timeout') {
    throw new Error('Task timed out after 30 seconds');
  }
  
  return result;
}

Rollback Pattern

export async function rollbackWorkflow(orderId: string) {
  'use workflow';
  
  const rollbacks: Array<() => Promise<void>> = [];
  
  try {
    await reserveInventory(orderId);
    rollbacks.push(() => releaseInventory(orderId));
    
    await chargePayment(orderId);
    rollbacks.push(() => refundPayment(orderId));
    
    await fulfillOrder(orderId);
  } catch (error) {
    // Execute rollbacks in reverse order
    for (const rollback of rollbacks.reverse()) {
      await rollback();
    }
    throw error;
  }
}

Idempotency

Using Step IDs

import { getStepMetadata } from 'workflow';

async function chargeUser(userId: string, amount: number) {
  'use step';
  
  const { stepId } = getStepMetadata();
  
  return await stripe.charges.create(
    { amount, currency: 'usd', customer: userId },
    { idempotencyKey: `charge:${stepId}` }
  );
}

Rules:

  • Always use stepId for external API idempotency
  • stepId is stable across retries
  • Never use attempt numbers or timestamps

Testing Workflows

import { start } from 'workflow/api';
import { myWorkflow } from './workflows/my-workflow';

// Start workflow
const run = await start(myWorkflow, ['arg1']);

// Check status
console.log(await run.status); // 'running' | 'completed' | 'failed'

// Wait for completion
const result = await run.returnValue;

// Stream output
const stream = run.readable;

Anti-Patterns

❌ Direct Node.js API in Workflow

export async function badWorkflow() {
  'use workflow';
  
  // ERROR: fs not available in workflow context
  const data = fs.readFileSync('file.txt');
}

❌ Non-Deterministic Logic

export async function badWorkflow() {
  'use workflow';
  
  // ERROR: Date.now() will change on replay
  if (Date.now() > someTimestamp) { /* ... */ }
  
  // ERROR: Math.random() will change on replay
  if (Math.random() > 0.5) { /* ... */ }
}

❌ Mutating Parameters

export async function badWorkflow(data: { count: number }) {
  'use workflow';
  
  await incrementCount(data);
  console.log(data.count); // Still original value!
}

async function incrementCount(data: { count: number }) {
  'use step';
  data.count++; // Only mutates the copy
}

❌ Stream Interaction in Workflow

export async function badWorkflow() {
  'use workflow';
  
  const writable = getWritable();
  const writer = writable.getWriter(); // ERROR!
  await writer.write('data'); // ERROR!
}

❌ Missing Directive

// ERROR: No "use step" - won't be retried
async function fetchData() {
  return await db.query('SELECT * FROM users');
}

Quick Reference

Workflow Functions:

  • Orchestrate steps
  • Must be deterministic
  • No Node.js APIs
  • Sandboxed environment

Step Functions:

  • Execute work
  • Full Node.js access
  • Automatic retries
  • Can throw errors

Serialization:

  • Pass-by-value (copy)
  • Return modified values
  • No functions/closures

Error Handling:

  • Error - Retry automatically
  • FatalError - No retry
  • RetryableError - Retry with delay

Streaming:

  • Get stream in workflow
  • Interact in steps only
  • Always release locks
  • Close when done

Hooks:

  • Use defineHook for type safety
  • Custom tokens for determinism
  • Iterate with for await...of

Idempotency:

  • Use stepId for keys
  • Apply to external APIs
  • Steps are already idempotent

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

frontend-design

Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, artifacts, posters, or applications (examples include websites, landing pages, dashboards, React components, HTML/CSS layouts, or when styling/beautifying any web UI). Generates creative, polished code and UI design that avoids generic AI aesthetics.

Repository SourceNeeds Review
94.2K160.3K
anthropics
Coding

remotion-best-practices

Use this skills whenever you are dealing with Remotion code to obtain the domain-specific knowledge.

Repository Source
2.1K147.9K
remotion-dev
Coding

azure-ai

Service Use When MCP Tools CLI

Repository SourceNeeds Review
155136.1K
microsoft