Skip to main content
AI agents sometimes need to pause execution to schedule recurring or future actions, wait before retrying an operation (for rate limiting), or wait for external state to be available. Workflow DevKit’s sleep function enables agents to pause execution without consuming resources, and resume at a specified time, after a specified duration, or in response to an external event. Workflows that suspend survive restarts, new deploys, and infrastructure changes, whether the suspension takes seconds or months.
See the sleep() API Reference for the full list of supported duration formats and detailed API documentation.

Adding a Sleep Tool

Sleep is a built-in function in Workflow DevKit, so exposing it as a tool is as simple as wrapping it in a tool definition:
1

Define the Tool

Add a sleep tool to your tool definitions:
workflows/chat/steps/tools.ts
import { sleep } from "workflow";
import { z } from "zod";

async function executeSleep({ durationMs }: { durationMs: number }) {
  // Note: No "use step" here - sleep is a workflow-level function
  await sleep(durationMs);
  return { message: `Slept for ${durationMs}ms` };
}

export const tools = {
  // ... existing tool definitions ...
  sleep: {
    description: "Pause execution for a specified duration",
    inputSchema: z.object({
      durationMs: z.number().describe("Duration to sleep in milliseconds"),
    }),
    execute: executeSleep,
  },
};
The sleep() function must be called from within a workflow context, not from within a step. This is why executeSleep does not have "use step" - it runs in the workflow context where sleep() is available.
2

Show Tool Status in the UI

Extend your chat UI to display the sleep tool call:
app/page.tsx
export default function ChatPage() {
  const { messages } = useChat();

  return (
    <div>
      {messages.map((message) => (
        <div key={message.id}>
          {message.parts.map((part, partIndex) => {
            // ... other rendering logic ...

            if (part.type === "tool-sleep") {
              return (
                <div key={partIndex} className="border rounded-lg p-4">
                  <p className="text-sm font-medium">
                    {part.output
                      ? `Slept for ${part.input.durationMs}ms`
                      : `Sleeping for ${part.input.durationMs}ms...`}
                  </p>
                </div>
              );
            }

            return null;
          })}
        </div>
      ))}
    </div>
  );
}
Now you can ask the agent to sleep, and the workflow will pause without consuming resources until the duration expires.

Duration Formats

sleep() supports multiple duration formats:
lineNumbers
// Milliseconds
await sleep(5000);

// Human-readable strings
await sleep("5s");   // 5 seconds
await sleep("2m");   // 2 minutes
await sleep("1h");   // 1 hour
await sleep("1d");   // 1 day
await sleep("1w");   // 1 week

// Date object (absolute time)
await sleep(new Date("2024-12-31T23:59:59Z"));

// ISO 8601 duration
await sleep("PT5M");  // 5 minutes

Use Cases

Rate Limiting

When hitting API rate limits, use RetryableError with a delay:
workflows/chat/steps/tools.ts
import { RetryableError } from "workflow";

async function callRateLimitedAPI({ endpoint }: { endpoint: string }) {
  "use step";

  const response = await fetch(endpoint);

  if (response.status === 429) {
    const retryAfter = response.headers.get("Retry-After");
    throw new RetryableError("Rate limited", {
      retryAfter: retryAfter ? parseInt(retryAfter) * 1000 : "1m",
    });
  }

  return response.json();
}
Learn more in Errors and Retries.

Scheduled Tasks

Schedule future actions within your agent workflow:
lineNumbers
import { sleep } from "workflow";

async function scheduleReminder({
  message,
  delayMs,
}: {
  message: string;
  delayMs: number;
}) {
  // No "use step" - this is workflow-level
  
  await sleep(delayMs);
  
  // Send the reminder
  await sendNotification(message);
  
  return { sent: true, message };
}

async function sendNotification(message: string) {
  "use step";
  
  await fetch("https://api.notifications.com/send", {
    method: "POST",
    body: JSON.stringify({ message }),
  });
}

Polling External State

Poll an external service until a condition is met:
lineNumbers
import { sleep } from "workflow";

async function waitForJobCompletion({ jobId }: { jobId: string }) {
  // No "use step" - orchestration logic
  
  let attempts = 0;
  const maxAttempts = 60; // 5 minutes total
  
  while (attempts < maxAttempts) {
    const status = await checkJobStatus(jobId);
    
    if (status === "completed") {
      return { status: "success", jobId };
    }
    
    if (status === "failed") {
      return { status: "failed", jobId };
    }
    
    // Wait 5 seconds before checking again
    await sleep("5s");
    attempts++;
  }
  
  throw new Error("Job timed out after 5 minutes");
}

async function checkJobStatus(jobId: string) {
  "use step";
  
  const response = await fetch(`https://api.jobs.com/${jobId}/status`);
  const data = await response.json();
  return data.status;
}

Exponential Backoff

Implement custom retry logic with exponential backoff:
lineNumbers
import { sleep } from "workflow";

async function retryWithBackoff({ url }: { url: string }) {
  const maxRetries = 5;
  let attempt = 0;
  
  while (attempt < maxRetries) {
    try {
      return await fetchData(url);
    } catch (error) {
      attempt++;
      
      if (attempt >= maxRetries) {
        throw error;
      }
      
      // Exponential backoff: 1s, 2s, 4s, 8s, 16s
      const delay = Math.pow(2, attempt) * 1000;
      await sleep(delay);
    }
  }
}

async function fetchData(url: string) {
  "use step";
  
  const response = await fetch(url);
  if (!response.ok) throw new Error("Fetch failed");
  return response.json();
}

Daily Reports

Schedule recurring daily tasks:
lineNumbers
import { sleep } from "workflow";

async function dailyReport() {
  "use workflow";
  
  while (true) {
    // Generate and send report
    const report = await generateReport();
    await sendReport(report);
    
    // Sleep until next day at 9 AM
    const now = new Date();
    const tomorrow9AM = new Date(now);
    tomorrow9AM.setDate(tomorrow9AM.getDate() + 1);
    tomorrow9AM.setHours(9, 0, 0, 0);
    
    await sleep(tomorrow9AM);
  }
}

async function generateReport() {
  "use step";
  
  const response = await fetch("https://api.analytics.com/daily-report");
  return response.json();
}

async function sendReport(report: any) {
  "use step";
  
  await fetch("https://api.email.com/send", {
    method: "POST",
    body: JSON.stringify({
      to: "[email protected]",
      subject: "Daily Report",
      body: JSON.stringify(report),
    }),
  });
}

Combining Sleep with Other Patterns

Sleep + Human-in-the-Loop

Implement timeouts for human approval:
lineNumbers
import { sleep } from "workflow";
import { bookingApprovalHook } from "@/workflows/hooks/booking-approval";

async function executeBookingWithTimeout(
  { flightNumber, price }: { flightNumber: string; price: number },
  { toolCallId }: { toolCallId: string }
) {
  const hook = bookingApprovalHook.create({ token: toolCallId });
  
  // Race between approval and timeout
  const result = await Promise.race([
    hook.then((data) => ({ type: "approval" as const, data })),
    sleep("1h").then(() => ({ type: "timeout" as const })),
  ]);
  
  if (result.type === "timeout") {
    return "Booking request timed out after 1 hour. Auto-rejected.";
  }
  
  const { approved } = result.data;
  return approved ? "Booking approved" : "Booking rejected";
}

Sleep + Streaming Updates

Stream progress updates during a long sleep:
lineNumbers
import { sleep, getWritable } from "workflow";
import type { UIMessageChunk } from "ai";

async function executeLongTask(
  { taskId }: { taskId: string },
  { toolCallId }: { toolCallId: string }
) {
  const totalDuration = 60000; // 60 seconds
  const updateInterval = 10000; // 10 seconds
  const steps = totalDuration / updateInterval;
  
  for (let i = 0; i < steps; i++) {
    await writeProgress(toolCallId, i + 1, steps);
    await sleep(updateInterval);
  }
  
  return { completed: true, taskId };
}

async function writeProgress(
  toolCallId: string,
  current: number,
  total: number
) {
  "use step";
  
  const writable = getWritable<UIMessageChunk>();
  const writer = writable.getWriter();
  try {
    await writer.write({
      type: "data-progress",
      id: toolCallId,
      data: { current, total, percentage: (current / total) * 100 },
    });
  } finally {
    writer.releaseLock();
  }
}

Best Practices

Don’t Sleep in Steps

Always call sleep() from workflow-level code, not steps:
lineNumbers
// ❌ Don't do this
async function badExample() {
  "use step";
  await sleep(1000); // Error: sleep not available in steps
}

// ✅ Do this instead
async function goodExample() {
  // No "use step" - workflow-level
  await sleep(1000);
  await performStepAction();
}

async function performStepAction() {
  "use step";
  // Step logic here
}

Combine with Steps for I/O

Use steps for I/O operations around sleep:
lineNumbers
async function scheduledJob() {
  while (true) {
    // Step: I/O operation
    await performJob();
    
    // Workflow: sleep
    await sleep("1h");
  }
}

async function performJob() {
  "use step";
  // Automatically retried on failure
  await fetch("https://api.example.com/job");
}

Handle Sleep Interruptions

Workflows can be cancelled during sleep:
lineNumbers
import { sleep, getWorkflowMetadata } from "workflow";

async function interruptibleSleep({ durationMs }: { durationMs: number }) {
  try {
    await sleep(durationMs);
    return { completed: true };
  } catch (error) {
    // Workflow was cancelled during sleep
    return { completed: false, interrupted: true };
  }
}

Build docs developers (and LLMs) love