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 SDK handles work that outlives a request. A workflow can pause, retry, and resume across server restarts, and each step has its own retry lifecycle. If a function stops mid-step, the platform resumes the run from its recorded state.

Outcome

Install the Workflow SDK, 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 controls the sequence: loops, branches, and parallel work. Step functions handle side effects such as fetching data, calling APIs, and reading files. If a step fails, the Workflow SDK retries it automatically (3 times by default) without re-running completed steps.

Keep fetchWeather out of the workflow function because workflow functions are sandboxed for determinism and cannot access the network or file system. Use them to coordinate work, and put side effects in step functions.

Hands-on Exercise 3.1

Set up the Workflow SDK 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 SDK 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 SDK is designed to use Vercel's Fluid compute. Without it, each workflow resumption can trigger a cold start. New Vercel projects enable Fluid compute by default, but verify the setting before you deploy.

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 SDK 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 SDK runs your code, it records each step's input and output in an event log. If the function restarts mid-execution, the SDK replays that log to reconstruct state without re-running completed steps. Workflow functions must therefore be deterministic: every replay must make the same decisions.

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 SDK intercepts them to preserve determinism.

Was this helpful?

supported.