---
title: Sleep, Scheduling & Timed Workflows
description: Use durable sleep to schedule actions minutes, hours, days, or weeks into the future.
type: guide
summary: Schedule future actions with durable sleep that survives cold starts, and race sleeps against hooks to let external events cancel the workflow early.
---

# Sleep, Scheduling & Timed Workflows



Workflow's `sleep()` is durable — it survives cold starts, restarts, and deployments. Combined with `defineHook()` and `Promise.race()`, it becomes the foundation for interruptible scheduled workflows like drip campaigns, reminders, and timed sequences.

## When to use this

* Sending emails on a schedule (drip campaigns, onboarding sequences, reminders)
* Waiting for a deadline but allowing early cancellation
* Any pattern where "do X, wait N hours, then do Y" needs to be both reliable and interruptible

## Drip campaign with cancellation

A drip campaign sends emails at intervals, sleeping between each. Each sleep races against a cancellation hook — if an external event fires the hook (e.g. user converts, unsubscribes), the campaign stops immediately.

```typescript
import { defineHook, sleep } from "workflow";

// Hook that any API route can fire to cancel the drip
export const cancelDrip = defineHook<{ reason?: string }>(); // [!code highlight]

async function sendEmail(email: string, template: string): Promise<void> {
  "use step";
  await fetch("https://api.sendgrid.com/v3/mail/send", {
    method: "POST",
    headers: { Authorization: `Bearer ${process.env.SENDGRID_KEY}` },
    body: JSON.stringify({ to: [{ email }], template_id: template }),
  });
}

export async function emailSequence(email: string) {
  "use workflow";

  await sendEmail(email, "welcome");

  // Race durable sleep against the cancellation hook
  const hook = cancelDrip.create({ token: `cancel-drip:${email}` }); // [!code highlight]
  const cancelled = await Promise.race([ // [!code highlight]
    sleep("2d").then(() => false), // [!code highlight]
    hook.then(() => true), // [!code highlight]
  ]); // [!code highlight]
  if (cancelled) return { status: "cancelled", email };

  await sendEmail(email, "getting-started-tips");

  // Create a fresh hook for the next sleep window
  const hook2 = cancelDrip.create({ token: `cancel-drip:${email}` }); // [!code highlight]
  const cancelled2 = await Promise.race([ // [!code highlight]
    sleep("2d").then(() => false), // [!code highlight]
    hook2.then(() => true), // [!code highlight]
  ]); // [!code highlight]
  if (cancelled2) return { status: "cancelled", email };

  await sendEmail(email, "feature-highlights");

  return { status: "drip-complete", email };
}
```

### Cancelling from an API route

Any server-side code can fire the hook by calling `.resume()` with the same token:

```typescript
import { cancelDrip } from "@/workflows/email-sequence";

export async function POST(req: Request) {
  const { email, reason } = await req.json();

  if (!email) {
    return Response.json({ error: "email is required" }, { status: 400 });
  }

  try {
    await cancelDrip.resume(`cancel-drip:${email}`, { // [!code highlight]
      reason: reason ?? "User completed action", // [!code highlight]
    }); // [!code highlight]
  } catch (error) {
    const msg = error instanceof Error ? error.message.toLowerCase() : "";
    if (msg.includes("not found") || msg.includes("expired")) {
      return Response.json({
        success: true,
        email,
        note: "No active drip found (already completed or cancelled)",
      });
    }
    throw error;
  }

  return Response.json({ success: true, email });
}
```

## How it works

1. **Durable sleep** — `sleep("2d")` persists through restarts at zero compute cost. The workflow resumes precisely when the timer fires.
2. **Hook creation** — `cancelDrip.create({ token })` registers a hook that resolves when any external system calls `.resume()` with the same token.
3. **Race** — `Promise.race([sleep(...), hook])` blocks until either the timer fires or the hook is resumed, whichever comes first.
4. **Fresh hooks per window** — after a sleep completes normally, the previous hook instance is consumed. A new `.create()` call registers a fresh hook for the next sleep window, reusing the same token.

## Adapting to your use case

* **Change durations** — replace `"2d"` with any duration string (`"1h"`, `"7d"`, `"30m"`) or a `Date` object for absolute times.
* **Add more steps** — the pattern scales to any number of email-then-sleep pairs.
* **Snooze instead of cancel** — resolve the hook with a `snooze` payload and sleep again: `sleep(new Date(Date.now() + payload.snoozeMs))`.
* **Timeout any operation** — the same `Promise.race(sleep, work)` pattern works for adding deadlines to slow steps.
* **Real providers** — swap the `sendEmail` step body for Resend, Postmark, or any HTTP API. The `"use step"` function has full Node.js access.

## Tips

* **`sleep()` accepts** duration strings (`"1d"`, `"2h"`, `"30s"`), milliseconds, or `Date` objects for sleeping until a specific time.
* **Durable means durable.** A `sleep("7d")` workflow costs nothing while sleeping — no compute, no memory.
* **Use `sleep()` in workflow context only.** Step functions cannot call `sleep()` directly. If a step needs a delay, use `setTimeout` inside the step.

## Key APIs

* [`"use workflow"`](/docs/foundations/workflows-and-steps) — marks the orchestrator function
* [`"use step"`](/docs/foundations/workflows-and-steps) — marks functions that run with full Node.js access
* [`sleep()`](/docs/api-reference/workflow/sleep) — durable wait (survives restarts, zero compute cost)
* [`defineHook()`](/docs/api-reference/workflow/define-hook) — creates a typed hook that external systems can fire
* [`Promise.race()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race) — races sleep against hooks for interruptible waits


## Sitemap
[Overview of all docs pages](/sitemap.md)
