Skip to main content

Quickstart Guide

This guide will walk you through creating your first durable workflow with Workflow DevKit. You’ll learn how to:
  • Install and configure Workflow DevKit
  • Write a simple workflow with steps
  • Handle errors and retries
  • Start and monitor workflow runs
This guide uses Next.js as an example, but Workflow DevKit works with SvelteKit, Nuxt, Astro, and other frameworks. See Framework Guides for other options.

Installation

1

Install the package

Install Workflow DevKit in your project:
npm install workflow
2

Configure your framework

Add the Workflow plugin to your framework configuration.
Update your next.config.ts (or next.config.js):
next.config.ts
import type { NextConfig } from 'next';
import { withWorkflow } from 'workflow/next';

const nextConfig: NextConfig = {
  // Your existing Next.js config
};

export default withWorkflow(nextConfig);
3

Initialize the runtime (Next.js only)

For Next.js, create an instrumentation.ts file in your project root:
instrumentation.ts
export async function register() {
  if (process.env.NEXT_RUNTIME === 'nodejs') {
    const { getWorld } = await import('workflow/runtime');
    await getWorld().start?.();
  }
}
Make sure instrumentationHook is enabled in your next.config.ts:
const nextConfig: NextConfig = {
  experimental: {
    instrumentationHook: true,
  },
};

Create Your First Workflow

Let’s build a simple user signup workflow that creates a user, sends a welcome email, and then sends a follow-up email after a delay.
1

Create a workflow file

Create a new file workflows/user-signup.ts:
workflows/user-signup.ts
import { sleep } from 'workflow';

export async function handleUserSignup(email: string) {
  'use workflow';

  // Create the user account
  const user = await createUser(email);
  console.log('User created:', user.id);

  // Send welcome email
  await sendWelcomeEmail(user);
  console.log('Welcome email sent');

  // Wait 5 seconds before follow-up
  await sleep('5s');

  // Send follow-up email
  await sendFollowUpEmail(user);
  console.log('Follow-up email sent');

  return { userId: user.id, status: 'onboarded' };
}

async function createUser(email: string) {
  'use step';

  // Simulate API call that might fail
  if (Math.random() < 0.3) {
    throw new Error('Database temporarily unavailable');
  }

  console.log(`Creating user: ${email}`);
  return {
    id: crypto.randomUUID(),
    email,
    createdAt: new Date().toISOString(),
  };
}

async function sendWelcomeEmail(user: { id: string; email: string }) {
  'use step';

  console.log(`Sending welcome email to: ${user.email}`);

  // Simulate email service API call
  await new Promise(resolve => setTimeout(resolve, 100));
}

async function sendFollowUpEmail(user: { id: string; email: string }) {
  'use step';

  console.log(`Sending follow-up email to: ${user.email}`);

  // Simulate email service API call
  await new Promise(resolve => setTimeout(resolve, 100));
}
Key concepts:
  • 'use workflow' marks the main workflow function
  • 'use step' marks individual steps that are automatically retried
  • Each step’s result is persisted before moving to the next step
  • sleep() pauses the workflow for a specified duration
2

Create an API route to start workflows

Create an API route to trigger your workflow:
Create app/api/signup/route.ts:
app/api/signup/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { start } from 'workflow/api';
import { handleUserSignup } from '@/workflows/user-signup';

export async function POST(request: NextRequest) {
  const { email } = await request.json();

  if (!email) {
    return NextResponse.json(
      { error: 'Email is required' },
      { status: 400 }
    );
  }

  // Start the workflow
  const run = await start(handleUserSignup, [email]);

  return NextResponse.json({
    runId: run.runId,
    message: 'Signup workflow started',
  });
}
3

Start your development server

Run your development server:
npm run dev
4

Test your workflow

Trigger your workflow with a POST request:
cURL
curl -X POST http://localhost:3000/api/signup \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]"}'
You should receive a response like:
{
  "runId": "run_abc123xyz",
  "message": "Signup workflow started"
}
Check your server logs to see the workflow executing:
Creating user: [email protected]
User created: 550e8400-e29b-41d4-a716-446655440000
Welcome email sent
Sending welcome email to: [email protected]
Sending follow-up email to: [email protected]
Follow-up email sent

Understanding Steps and Workflows

Workflows vs Steps

  • Workflows ('use workflow') are the main orchestration functions
  • Steps ('use step') are individual units of work that can fail and retry
export async function processOrder(orderId: string) {
  'use workflow';  // Main workflow - orchestrates steps

  const order = await fetchOrder(orderId);     // Step 1
  const payment = await processPayment(order); // Step 2
  await fulfillOrder(order);                   // Step 3
  await sendConfirmation(order);               // Step 4

  return { orderId, status: 'complete' };
}

async function fetchOrder(orderId: string) {
  'use step';  // This step will retry if it fails
  const response = await fetch(`/api/orders/${orderId}`);
  return response.json();
}

Automatic Retries

Steps automatically retry when they throw an error. By default, retries happen immediately, but you can customize this:
import { RetryableError, FatalError } from 'workflow';

async function chargeCustomer(amount: number) {
  'use step';

  try {
    const result = await stripe.charges.create({ amount });
    return result;
  } catch (error) {
    if (error.type === 'card_error') {
      // Don't retry - this is a permanent error
      throw new FatalError('Invalid card: ' + error.message);
    }

    // Retry after 30 seconds for network errors
    throw new RetryableError('Stripe API unavailable', {
      retryAfter: '30s'
    });
  }
}
Error Types:
  • Regular Error: Retried immediately (default behavior)
  • RetryableError: Retried after a specified delay
  • FatalError: Not retried - workflow fails permanently

Monitoring Workflow Runs

You can check the status and result of a workflow run:
app/api/runs/[runId]/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { getRun } from 'workflow/api';

export async function GET(
  request: NextRequest,
  { params }: { params: { runId: string } }
) {
  const run = await getRun(params.runId);

  if (!run) {
    return NextResponse.json(
      { error: 'Run not found' },
      { status: 404 }
    );
  }

  // Wait for the workflow to complete (if still running)
  const result = await run.returnValue;

  return NextResponse.json({
    runId: run.runId,
    status: run.status,
    result,
  });
}

Using sleep() for Delays

The sleep() function pauses workflow execution without blocking your server:
import { sleep } from 'workflow';

export async function scheduleReminder(taskId: string) {
  'use workflow';

  await createTask(taskId);

  // Wait 1 day
  await sleep('24h');
  await sendReminder(taskId);

  // Wait 7 days
  await sleep('7d');
  await sendFinalReminder(taskId);
}
Supported duration formats:
  • Milliseconds: sleep(5000) or sleep('5000ms')
  • Seconds: sleep('30s')
  • Minutes: sleep('5m')
  • Hours: sleep('2h')
  • Days: sleep('7d')
  • Date objects: sleep(new Date('2026-12-31'))

Next Steps

Now that you’ve built your first workflow, explore more advanced features:

Webhooks & Hooks

Pause workflows and resume them via external callbacks

AI Workflows

Build durable AI agents with tool calling

Error Handling

Advanced retry strategies and error recovery

Streaming

Stream real-time updates from long-running workflows

Common Patterns

Pattern 1: Multi-Step API Orchestration

export async function syncUserData(userId: string) {
  'use workflow';

  // Each external API call is a step
  const user = await fetchFromCRM(userId);
  const profile = await fetchFromAuth0(userId);
  const analytics = await fetchFromMixpanel(userId);

  // Combine and save
  await saveToDatabase({
    ...user,
    ...profile,
    analytics,
  });
}

Pattern 2: Human-in-the-Loop

import { createWebhook } from 'workflow';

export async function contentApproval(contentId: string) {
  'use workflow';

  const content = await fetchContent(contentId);

  // Create webhook for approval
  const webhook = createWebhook();
  await sendApprovalRequest(content, webhook.url);

  // Wait for approval (webhook will be called)
  const approval = await webhook;

  if (approval.approved) {
    await publishContent(contentId);
  }
}

Pattern 3: Scheduled Tasks

import { sleep } from 'workflow';

export async function trialExpiration(userId: string) {
  'use workflow';

  await sendTrialStartEmail(userId);

  // Wait 7 days
  await sleep('7d');
  await sendTrialEndingEmail(userId);

  // Wait 1 more day
  await sleep('1d');
  await sendTrialExpiredEmail(userId);
  await downgradeAccount(userId);
}

Troubleshooting

Make sure you’ve:
  1. Added the framework plugin to your config
  2. Created instrumentation.ts (Next.js only)
  3. Enabled instrumentationHook in next.config (Next.js only)
  4. Restarted your development server
Steps only retry when they throw an error. Check that:
  • Your step function has 'use step' directive
  • The function is actually throwing an error (not returning it)
  • You’re not catching and suppressing errors
If you see “Cannot find module ‘workflow’”:
  1. Make sure you’ve installed the package: npm install workflow
  2. Restart your TypeScript server in your editor
  3. Clear your framework’s build cache and restart

Need Help?

Join the discussion on GitHub for community support

Build docs developers (and LLMs) love