Vercel Logo

Your First Workflow

Your serverless function has the lifespan of a mayfly. Request comes in, response goes out, function dies. waitUntil() from @vercel/functions buys you some extra seconds, like a mayfly that found a really good energy drink. But what happens when the weather API is down for 30 seconds? Or when Vercel redeploys your app mid-evaluation? The mayfly is dead, and so is your work.

The Workflow DevKit fixes this. A workflow is a function that can pause, retry, and resume across server restarts. Each step runs independently with automatic retries. If the function dies mid-step, the platform picks up where it left off. No retry loops, no dead-letter queues, no crossed fingers.

Outcome

Install the Workflow DevKit, create a durable workflow that evaluates ski alerts against live weather data, and trigger it from a route handler.

Fast Track

  1. Install workflow and add workflowPlugin() to your Vite config
  2. Create a workflow file with "use workflow" and "use step" directives
  3. Trigger it from a route handler with start()

Workflows vs Steps

Two directives, two roles:

"use workflow"                      "use step"
┌─────────────────────┐            ┌─────────────────────┐
│ Orchestrator         │            │ Worker               │
│ Sandboxed            │            │ Full Node.js         │
│ Deterministic        │            │ Side effects OK      │
│ Controls flow        │            │ Auto-retries (3x)    │
│ Calls steps          │            │ Does the real work   │
└─────────────────────┘            └─────────────────────┘

The workflow function decides what to do: loop, branch, run steps in parallel. The step functions do the work: fetch data, query APIs, read files. If a step fails, the Workflow DevKit retries it automatically (3 times by default) without re-running the entire workflow.

You could put the fetchWeather call directly in the workflow function. But workflow functions are sandboxed for determinism. No network access, no file system. That's the whole point. They're coordinators, not workers.

Hands-on exercise 3.1

Set up the Workflow DevKit and create your first workflow:

Requirements:

  1. Install the workflow package
  2. Add workflowPlugin() to vite.config.ts
  3. Create workflows/evaluate-alerts.ts with a workflow function and a step function
  4. Complete the route handler at src/routes/api/workflow/+server.ts to start the workflow
  5. The step should group alerts by resort, fetch weather for each, and evaluate conditions

Implementation hints:

  • The workflow file goes at the project root in a workflows/ directory (Workflow DevKit convention)
  • Import workflowPlugin from workflow/sveltekit and add it to your Vite plugins array
  • The workflow function uses "use workflow" as the first line. The step function uses "use step"
  • Inside a step, use dynamic imports for $lib modules: const { getResort } = await import('$lib/data/resorts')
  • Use start() from workflow/api in the route handler. It returns a run object immediately without waiting for the workflow to complete
  • Object.groupBy() handles alert grouping (Node 24 supports it natively)
Fluid compute should be enabled

The Workflow DevKit is designed for Vercel's Fluid compute. Without it, each workflow resumption triggers a cold start. Your project already uses Fluid compute (the default for new projects), so you're good.

Try It

  1. Install and configure:

    npm install workflow

    Restart your dev server after updating vite.config.ts.

  2. Trigger the workflow:

    $ curl -X POST http://localhost:5173/api/workflow \
      -H "Content-Type: application/json" \
      -d '{"alerts": [{"id": "test-1", "resortId": "mammoth", "condition": {"type": "conditions", "match": "powder"}, "originalQuery": "test", "createdAt": "2025-01-01", "triggered": false}]}'

    Expected response:

    {
      "runId": "wf_abc123...",
      "status": "started"
    }

    The workflow runs in the background. The route handler returns immediately.

  3. Check server logs:

    [Workflow] Complete { evaluated: 1, triggered: 0 }
    
  4. Inspect in the Workflow dashboard:

    npx workflow web

    Open the dashboard and you'll see your workflow run with its step, inputs, outputs, and timing.

Commit

git add -A
git commit -m "feat(workflow): add Workflow DevKit with alert evaluation"
git push

Done-When

  • workflow package is installed and workflowPlugin() is in vite.config.ts
  • workflows/evaluate-alerts.ts exists with "use workflow" and "use step" directives
  • /api/workflow route handler starts the workflow and returns a run ID
  • Workflow evaluates alerts against live weather data
  • npx workflow web shows the completed workflow run

Solution

1. Vite config:

vite.config.ts
import tailwindcss from '@tailwindcss/vite';
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import { workflowPlugin } from 'workflow/sveltekit';
 
export default defineConfig({
  plugins: [
    tailwindcss(), workflowPlugin(), sveltekit()
  ]
});

2. Workflow file:

workflows/evaluate-alerts.ts
import type { Alert } from '$lib/schemas/alert';
 
interface EvaluateInput {
  alerts: Alert[];
}
 
export default async function evaluateAlerts({ alerts }: EvaluateInput) {
  "use workflow";
 
  const results = await evaluateAllAlerts(alerts);
 
  console.log('[Workflow] Complete', {
    evaluated: results.length,
    triggered: results.filter((r) => r.triggered).length
  });
 
  return results;
}
 
async function evaluateAllAlerts(alerts: Alert[]) {
  "use step";
 
  const { getResort } = await import('$lib/data/resorts');
  const { fetchWeather } = await import('$lib/services/weather');
  const { evaluateCondition } = await import('$lib/services/alerts');
 
  const alertsByResort = Object.groupBy(alerts, (a) => a.resortId);
  const results = [];
 
  for (const [resortId, resortAlerts] of Object.entries(alertsByResort)) {
    const resort = getResort(resortId);
    if (!resort) continue;
 
    const weather = await fetchWeather(resort);
 
    for (const alert of resortAlerts!) {
      const triggered = evaluateCondition(alert.condition, weather);
      results.push({
        alertId: alert.id,
        resortId,
        triggered
      });
    }
  }
 
  return results;
}

3. Route handler:

src/routes/api/workflow/+server.ts
import { json } from '@sveltejs/kit';
import { start } from 'workflow/api';
import evaluateAlerts from '../../../../workflows/evaluate-alerts';
import type { RequestHandler } from './$types';
 
export const POST: RequestHandler = async ({ request }) => {
  const { alerts } = await request.json();
 
  if (!alerts || !Array.isArray(alerts)) {
    return json({ error: 'alerts array required' }, { status: 400 });
  }
 
  const run = await start(evaluateAlerts, [{ alerts }]);
 
  return json({
    runId: run.runId,
    status: 'started'
  });
};

The "use workflow" directive marks evaluateAlerts as the orchestrator. It calls evaluateAllAlerts, which is a step function (marked with "use step") that does the actual work: fetching weather data and evaluating conditions. If the step fails, the platform retries it up to 3 times automatically.

start() enqueues the workflow and returns immediately. It doesn't block the route handler. The workflow runs in the background with its own lifecycle: it can pause, retry failed steps, and survive function restarts.

Troubleshooting

Module not found errors for $lib imports

Step functions run in their own context. If $lib path aliases don't resolve, use the full relative path instead: await import('../../src/lib/data/resorts'). The workflowPlugin() should handle SvelteKit aliases, but check that the plugin is loaded before sveltekit() in your Vite config.

Workflow starts but step never completes

Check the dev server logs for errors inside the step function. Step failures are retried silently by default. Run npx workflow web to see the step status and any error messages. If the step failed 3 times and exhausted retries, you'll see it marked as failed in the dashboard.

Advanced: How Durable Execution Works

When the Workflow DevKit runs your code, it records every step's input and output to an event log. If the function restarts mid-execution, the DevKit replays the event log to reconstruct the workflow's state without re-running completed steps. That's why workflow functions must be deterministic: the replay needs to make the same decisions every time.

First run:
  evaluateAllAlerts(alerts) → runs step → records result ✓

Function restarts mid-workflow:
  evaluateAllAlerts(alerts) → replays recorded result (skip!) ✓
  ...continues with next steps

This is also why Math.random() and Date.now() are fixed during workflow replay. The DevKit intercepts them to ensure determinism.