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
- Extract a
evaluateResortstep function that handles a single resort - Use
Promise.allin the workflow to run all resort steps concurrently - 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:
- Extract
evaluateResort(resortId, alerts)as its own"use step"function - In the workflow function, group alerts by resort and dispatch parallel steps with
Promise.all - After evaluation, if no alerts triggered and we haven't rechecked 3 times,
sleep('30m')and re-evaluate - Return the final results with the number of rounds completed
Implementation hints:
Promise.allin a workflow function dispatches steps concurrently. The platform runs them in parallelsleep('30m')suspends the workflow for 30 minutes without keeping a function active, then resumes it- For local testing, use
sleep('10s')instead ofsleep('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
evaluateAlertscall 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
-
Trigger the workflow with alerts for multiple resorts:
$ 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}]}' -
Open the Workflow dashboard:
npx workflow webYou should see three parallel
evaluateResortsteps, one for each resort. They start at roughly the same time instead of sequentially. -
Check server logs:
[Workflow] Round complete { round: 1, evaluated: 3, triggered: 0 } -
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 withsleep('10s'), it resumes after 10 seconds and runs round 2.
Commit
git add -A
git commit -m "feat(workflow): parallel resort steps with sleep re-check"
git pushDone-When
- Each resort is processed by its own
"use step"function - Steps run in parallel via
Promise.allin the workflow sleep()pauses the workflow between evaluation rounds- Workflow rechecks up to 3 times if no alerts trigger
npx workflow webshows parallel steps and sleep states
Solution
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)
}));
}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.
Troubleshooting
Advanced: Racing Steps Against a Timeout
Promise.race lets you set a deadline on a group of steps:
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.
Was this helpful?