Recipes
Multi-step with retries
step.do + step.sleep for durable, retryable, time-paced pipelines. (Beta)
Beta
Uses the Workflows lane atoms (step.do, step.sleep). Part of the
Functions/Workflows beta.
When you have a pipeline that:
- exceeds 30 s wall time, or
- needs to retry individual steps with backoff, or
- needs cooldown between batches,
…write step.* atoms. Harbor auto-routes the run to the durable lane
(Cloudflare Workflows under the hood). No flag, no separate command.
Example: re-deploy with rolling restarts
defineJob({
name: "rolling-deploy",
handler: async (ctx, input: { environments: string[] }) => {
const results: Array<{ env: string; ok: boolean }> = [];
for (const env of input.environments) {
// step.do checkpoints per attempt; transient failure replays only this step
const deploy = await step.do(\`deploy:\${env}\`, async () => {
return ctx.plugins.cloudflareMcp.deploy({ env });
});
// step.sleep parks the run cheaply between batches
await step.sleep("cool-down", "10m");
const health = await step.do(\`health:\${env}\`, async () => {
return ctx.plugins.cloudflareMcp.health({ env });
});
results.push({ env, ok: health.status === "ok" });
// Bail out at first bad environment
if (!health.ok) break;
}
return { results };
},
});What each atom buys you
| Atom | What it does | When to use |
|---|---|---|
step.do(name, fn) | Run fn with per-step checkpoint; on failure, only fn retries | Anything that hits a rate-limited or flaky API |
step.sleep(name, duration) | Park the run for duration without burning CPU | Cooldowns between batches, scheduled polling |
step.waitForEvent(name, opts) | Park the run until POST /runs/events arrives | OAuth approval, manual gate, async upstream event |
duration accepts '1m', '10m', '1h', '2d', etc. Maximum park
time is plan-bound — see Limits.
Idempotency rules
Inside step.do:
- Use only deterministic inputs. The engine may replay
fnafter a partial failure; randomness orDate.now()will produce different results. - Side effects must be idempotent. Idempotency keys, conditional inserts, "create if absent" patterns. Otherwise expect duplicate writes.
Outside step.do (top-level handler code) is not retried per step
— that's just the orchestrator. So:
const id = crypto.randomUUID(); // ✓ runs once
await step.do("create", () => orbit.db.tickets.insert({ id, ... })); // ✓ deterministic inputWhere to go next
- OAuth approval workflow — the
step.waitForEventpattern. - Concepts → Exec → Durable execution — the underlying mechanism.