惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Y
Y Combinator Blog
The GitHub Blog
The GitHub Blog
云风的 BLOG
云风的 BLOG
Engineering at Meta
Engineering at Meta
Google DeepMind News
Google DeepMind News
aimingoo的专栏
aimingoo的专栏
Recent Announcements
Recent Announcements
A
About on SuperTechFans
U
Unit 42
MyScale Blog
MyScale Blog
J
Java Code Geeks
博客园_首页
Blog — PlanetScale
Blog — PlanetScale
D
Docker
Microsoft Azure Blog
Microsoft Azure Blog
博客园 - 司徒正美
量子位
月光博客
月光博客
G
Google Developers Blog
V
V2EX
博客园 - 聂微东
宝玉的分享
宝玉的分享
IT之家
IT之家
Vercel News
Vercel News

Workflow SDK Documentation

Patterns for Defining Tools Human-in-the-Loop Building Durable AI Agents Queueing User Messages Resumable Streams Sleep, Suspense, and Scheduling Streaming Updates from Tools API Reference Workflow Globals Changelog Resilient run start Cookbook Building a World Deploying Astro Express Fastify Hono Getting Started NestJS Next.js Nitro Nuxt Python SvelteKit Vite corrupted-event-log fetch-in-workflow hook-conflict Errors
Sleep, Scheduling & Timed Workflows
2026-05-31 · via Workflow SDK Documentation

Use durable sleep to schedule actions minutes, hours, days, or weeks into the future.

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.

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 for the explicit deploymentId: "latest" continuation pattern.

  • 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

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.

import { defineHook, sleep } from "workflow";

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

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}` }); 
  const cancelled = await Promise.race([ 
    sleep("2d").then(() => false), 
    hook.then(() => true), 
  ]); 
  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}` }); 
  const cancelled2 = await Promise.race([ 
    sleep("2d").then(() => false), 
    hook2.then(() => true), 
  ]); 
  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:

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}`, { 
      reason: reason ?? "User completed action", 
    }); 
  } 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 });
}
  1. Durable sleepsleep("2d") persists through restarts at zero compute cost. The workflow resumes precisely when the timer fires.
  2. Hook creationcancelDrip.create({ token }) registers a hook that resolves when any external system calls .resume() with the same token.
  3. RacePromise.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.
  • 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.
  • 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.
  • "use workflow" — marks the orchestrator function
  • "use step" — marks functions that run with full Node.js access
  • sleep() — durable wait (survives restarts, zero compute cost)
  • defineHook() — creates a typed hook that external systems can fire
  • Promise.race() — races sleep against hooks for interruptible waits