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
- Install
workflowand addworkflowPlugin()to your Vite config - Create a workflow file with
"use workflow"and"use step"directives - 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:
- Install the
workflowpackage - Add
workflowPlugin()tovite.config.ts - Create
workflows/evaluate-alerts.tswith a workflow function and a step function - Complete the route handler at
src/routes/api/workflow/+server.tsto start the workflow - 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
workflowPluginfromworkflow/sveltekitand 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
$libmodules:const { getResort } = await import('$lib/data/resorts') - Use
start()fromworkflow/apiin 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)
Try It
-
Install and configure:
npm install workflowRestart your dev server after updating
vite.config.ts. -
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.
-
Check server logs:
[Workflow] Complete { evaluated: 1, triggered: 0 } -
Inspect in the Workflow dashboard:
npx workflow webOpen 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 pushDone-When
workflowpackage is installed andworkflowPlugin()is invite.config.tsworkflows/evaluate-alerts.tsexists with"use workflow"and"use step"directives/api/workflowroute handler starts the workflow and returns a run ID- Workflow evaluates alerts against live weather data
npx workflow webshows the completed workflow run
Solution
1. Vite config:
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:
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:
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
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?