---
title: "Parallel Steps"
description: "Refactor the workflow to process resorts as independent parallel steps and use sleep to schedule delayed re-evaluation."
canonical_url: "https://vercel.com/academy/svelte-on-vercel/multi-step-workflows"
md_url: "https://vercel.com/academy/svelte-on-vercel/multi-step-workflows.md"
docset_id: "vercel-academy"
doc_version: "1.0"
last_updated: "2026-04-11T13:24:42.627Z"
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>

# Parallel Steps

# Parallel Steps and Sleep

Imagine checking conditions at five ski resorts by calling each one on the phone. You call Mammoth. Wait. Call Palisades. Wait. By the time you get to Mt. Bachelor, Mammoth's conditions have changed. You wouldn't do this in real life. You'd call all five at once and deal with the answers as they come in.

The workflow from lesson 3.1 has one step that handles everything sequentially. If Mammoth's weather API takes 500ms and you have 5 resorts, that's 2.5 seconds of serial waiting. Let's split the work: one step per resort, all running in parallel. And after evaluation finishes, we'll use `sleep()` to schedule a re-check without cron jobs or external schedulers.

## Outcome

Refactor the workflow to run one step per resort in parallel and use `sleep()` to schedule automatic re-evaluation.

## Fast Track

1. Extract a `evaluateResort` step function that handles a single resort
2. Use `Promise.all` in the workflow to run all resort steps concurrently
3. Add `sleep()` to pause the workflow and re-check later if no alerts triggered

## One Step per Resort

In 3.1, one big step did all the work. The problem: if the weather API fails for Mammoth, the entire step retries, including the successful fetches for the other 4 resorts.

```
Lesson 3.1 (one step):
  [evaluateAllAlerts] → mammoth → palisades → ... → mt-bachelor
  If palisades fails → entire step retries from mammoth

Lesson 3.2 (one step per resort):
  [evaluateResort: mammoth]    ┐
  [evaluateResort: palisades]  ├→ parallel, independent retries
  [evaluateResort: steamboat]  │
  [evaluateResort: targhee]    │
  [evaluateResort: bachelor]   ┘
  If palisades fails → only palisades retries
```

Each step is independently retryable. If Palisades times out, only Palisades retries. The other 4 results are already recorded.

## Hands-on exercise 3.2

Refactor the workflow to use parallel steps and sleep:

**Requirements:**

1. Extract `evaluateResort(resortId, alerts)` as its own `"use step"` function
2. In the workflow function, group alerts by resort and dispatch parallel steps with `Promise.all`
3. After evaluation, if no alerts triggered and we haven't rechecked 3 times, `sleep('30m')` and re-evaluate
4. Return the final results with the number of rounds completed

**Implementation hints:**

- `Promise.all` in a workflow function dispatches steps concurrently. The platform runs them in parallel
- `sleep('30m')` suspends the workflow for 30 minutes without consuming resources. No server running, no memory used. After 30 minutes, it resumes
- For local testing, use `sleep('10s')` instead of `sleep('30m')` so you don't wait half an hour
- The workflow function can call itself recursively for re-checks by returning the result of another `evaluateAlerts` call with an incremented counter
- Data between workflow and step functions is serialized (passed by value). Return modified data from steps rather than mutating shared state

## Try It

1. **Trigger the workflow with alerts for multiple resorts:**
   ```bash
   $ curl -X POST http://localhost:5173/api/workflow \
     -H "Content-Type: application/json" \
     -d '{"alerts": [{"id": "a1", "resortId": "mammoth", "condition": {"type": "conditions", "match": "powder"}, "originalQuery": "test", "createdAt": "2025-01-01", "triggered": false}, {"id": "a2", "resortId": "grand-targhee", "condition": {"type": "temperature", "operator": "lt", "value": 20, "unit": "fahrenheit"}, "originalQuery": "test", "createdAt": "2025-01-01", "triggered": false}, {"id": "a3", "resortId": "steamboat", "condition": {"type": "snowfall", "operator": "gt", "value": 6, "unit": "inches"}, "originalQuery": "test", "createdAt": "2025-01-01", "triggered": false}]}'
   ```

2. **Open the Workflow dashboard:**

   ```bash
   npx workflow web
   ```

   You should see three parallel `evaluateResort` steps, one for each resort. They start at roughly the same time instead of sequentially.

3. **Check server logs:**
   ```
   [Workflow] Round complete { round: 1, evaluated: 3, triggered: 0 }
   ```

4. **Observe the sleep state:**

   If no alerts triggered, the workflow enters a sleep state. In `npx workflow web`, you'll see the workflow paused, waiting to resume. For local testing with `sleep('10s')`, it resumes after 10 seconds and runs round 2.

## Commit

```bash
git add -A
git commit -m "feat(workflow): parallel resort steps with sleep re-check"
git push
```

## Done-When

- [ ] Each resort is processed by its own `"use step"` function
- [ ] Steps run in parallel via `Promise.all` in the workflow
- [ ] `sleep()` pauses the workflow between evaluation rounds
- [ ] Workflow rechecks up to 3 times if no alerts trigger
- [ ] `npx workflow web` shows parallel steps and sleep states

## Solution

```typescript title="workflows/evaluate-alerts.ts" {3,10-11,16-17,27-30,35-56}
import { sleep } from 'workflow';
import type { Alert } from '$lib/schemas/alert';

interface EvaluateInput {
  alerts: Alert[];
  recheckCount?: number;
}

interface AlertResult {
  alertId: string;
  resortId: string;
  triggered: boolean;
}

export default async function evaluateAlerts(
  { alerts, recheckCount = 0 }: EvaluateInput
) {
  "use workflow";

  const alertsByResort = Object.groupBy(alerts, (a) => a.resortId);
  const resortIds = Object.keys(alertsByResort);

  // One step per resort, all in parallel
  const results = await Promise.all(
    resortIds.map((resortId) =>
      evaluateResort(resortId, alertsByResort[resortId]!)
    )
  );

  const allResults = results.flat();
  const triggered = allResults.filter((r) => r.triggered);

  console.log('[Workflow] Round complete', {
    round: recheckCount + 1,
    evaluated: allResults.length,
    triggered: triggered.length
  });

  // If nothing triggered and we haven't hit the recheck limit, sleep and try again
  if (triggered.length === 0 && recheckCount < 3) {
    await sleep('30m');
    return evaluateAlerts({ alerts, recheckCount: recheckCount + 1 });
  }

  return {
    results: allResults,
    rounds: recheckCount + 1,
    triggered: triggered.length
  };
}

async function evaluateResort(
  resortId: string,
  alerts: Alert[]
): Promise<AlertResult[]> {
  "use step";

  const { getResort } = await import('$lib/data/resorts');
  const { fetchWeather } = await import('$lib/services/weather');
  const { evaluateCondition } = await import('$lib/services/alerts');

  const resort = getResort(resortId);
  if (!resort) return [];

  const weather = await fetchWeather(resort);

  return alerts.map((alert) => ({
    alertId: alert.id,
    resortId,
    triggered: evaluateCondition(alert.condition, weather)
  }));
}
```

Two big changes from 3.1:

**Parallel steps.** `evaluateResort` is its own `"use step"` function. The workflow dispatches one per resort via `Promise.all`. The Workflow DevKit runs them concurrently, and each has its own retry budget. If Mammoth's weather API times out, only Mammoth retries. Steamboat's result is already saved.

**Sleep.** `sleep('30m')` suspends the workflow without consuming any resources. No server running, no function billed. After 30 minutes, the platform wakes the workflow and it continues from where it left off. The recursive call to `evaluateAlerts` with an incremented `recheckCount` runs a fresh round of evaluation. Three re-checks max, then it returns whatever it has.

\*\*Note: Use a shorter sleep for local testing\*\*

While developing, change `sleep('30m')` to `sleep('10s')` so you can see re-check rounds without waiting half an hour. Switch back to `'30m'` before deploying.

\*\*Note: Data is serialized between workflow and steps\*\*

Arguments and return values are copied, not shared. If you modify an object inside a step, the workflow doesn't see the change. Always return the data you want the workflow to use.

## Troubleshooting

\*\*Warning: Steps run sequentially instead of in parallel\*\*

Make sure you're passing the step calls to `Promise.all`, not awaiting each one individually. `await evaluateResort(...)` inside a `for` loop runs them sequentially. `Promise.all(resortIds.map(...))` runs them in parallel.

\*\*Warning: Sleep doesn't seem to work locally\*\*

The local Workflow DevKit processes steps synchronously. Short sleeps like `sleep('5s')` should work, but the timing may not be precise. Deploy to Vercel to test production sleep behavior where the workflow truly suspends and resumes.

## Advanced: Racing Steps Against a Timeout

`Promise.race` lets you set a deadline on a group of steps:

```typescript
import { sleep } from 'workflow';

const results = await Promise.race([
  Promise.all(
    resortIds.map((id) => evaluateResort(id, alertsByResort[id]!))
  ),
  sleep('30s').then(() => 'timeout' as const)
]);

if (results === 'timeout') {
  console.warn('[Workflow] Evaluation timed out after 30s');
  return { results: [], timedOut: true };
}
```

The workflow returns whatever finishes first: the actual results or the timeout. Useful when you'd rather return partial data than wait indefinitely.


---

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