---
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-10-02T17:46:31.506Z"
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

The workflow from lesson 3.1 checks resorts one at a time. That makes total latency the sum of every weather request and forces successful resorts to run again when one request fails.

If each weather request takes 500ms, checking 5 resorts serially takes about 2.5 seconds. Splitting the work into one parallel step per resort cuts that wait and isolates retries. After evaluation finishes, `sleep()` schedules a re-check without a cron job or external scheduler.

## Outcome

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

## Fast Track

1. Extract an `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 lesson 3.1, one step checks all resorts. If the weather API fails for Mammoth, the entire step retries, repeating 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 the workflow hasn'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 keeping a function active, then resumes it
- 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.ts';

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.ts');
  const { fetchWeather } = await import('#lib/services/weather.ts');
  const { evaluateCondition } = await import('#lib/services/alerts.ts');

  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)
  }));
}
```

The refactor changes two behaviors from lesson 3.1:

**Parallel steps.** `evaluateResort` is its own `"use step"` function. The workflow dispatches one per resort via `Promise.all`. The Workflow SDK 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 keeping a function active. After 30 minutes, the platform resumes the workflow from its recorded state. The recursive call to `evaluateAlerts` increments `recheckCount` and starts a new evaluation round. After three re-checks, the workflow returns its current results.

\*\*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 SDK 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 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)
