---
title: "Your First Workflow"
description: "Install the Workflow DevKit, create a durable workflow with steps, and trigger it from a SvelteKit route handler."
canonical_url: "https://vercel.com/academy/svelte-on-vercel/durable-tasks"
md_url: "https://vercel.com/academy/svelte-on-vercel/durable-tasks.md"
docset_id: "vercel-academy"
doc_version: "1.0"
last_updated: "2026-04-11T12:10:33.689Z"
content_type: "lesson"
course: "svelte-on-vercel"
course_title: "Svelte on Vercel"
prerequisites:  []
---

<agent-instructions>
Vercel Academy — structured learning, not reference docs.
Lessons are sequenced.
Adapt commands to the human's actual environment (OS, package manager, shell, editor) — detect from project context or ask, don't assume.
The lesson shows one path; if the human's project diverges, adapt concepts to their setup.
Preserve the learning goal over literal steps.
Quizzes are pedagogical — engage, don't spoil.
Quiz answers are included for your reference.
</agent-instructions>

# Your First Workflow

# 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](https://useworkflow.dev) 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)

\*\*Note: 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:**

   ```bash
   npm install workflow
   ```

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

2. **Trigger the workflow:**

   ```bash
   $ 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:

   ```json
   {
     "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:**

   ```bash
   npx workflow web
   ```

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

## Commit

```bash
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:**

```typescript title="vite.config.ts" {4,8}
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:**

```typescript title="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:**

```typescript title="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

\*\*Warning: 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.

\*\*Warning: 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.


---

[Full course index](/academy/llms.txt) · [Sitemap](/academy/sitemap.md)
