---
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



<CopyPrompt text="Create an interruptible scheduled workflow. Import `defineHook` and `sleep` from `workflow`. Define a cancellation hook such as `export const cancelDrip = defineHook<{ reason?: string }>()`. In an exported workflow function with &#x22;use workflow&#x22;, create the hook once, send the first message in a &#x22;use step&#x22; helper, then `await Promise.race([sleep(&#x22;2d&#x22;).then(() => false), cancelHook.then(() => true)])` before each delayed action. If the hook wins, return a cancelled status; if sleep wins, continue to the next step. Add an API route that calls `cancelDrip.resume(runIdOrToken, { reason })` or `resumeHook()` from `workflow/api`. Verify scheduled delivery, cancellation before sleep completes, and resume after server restart." />

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.

<Callout type="info">
  Scheduled workflows are still pinned to the deployment that started them. If you are building recurring or indefinitely running schedules that should adopt newer code over time, see [Versioning](/docs/foundations/versioning) for the explicit `deploymentId: "latest"` continuation pattern.
</Callout>

## 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.

<Callout type="info">
  Deterministic hook tokens can also serve as the idempotency point for scheduled runs. If duplicate schedule starts would send duplicate campaigns or reminders, create a hook with a token derived from the campaign key near the beginning of the workflow and route retries through that hook. If two scheduled starts race, the duplicate run can detect the conflict early with `await hook.getConflict()`, which resolves with the active owner so the duplicate can defer to it. See [Idempotency](/docs/foundations/idempotency).
</Callout>

## 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


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)