AI Agents

Sleep, Suspense, and Scheduling

AI agents sometimes need to pause execution in order to schedule recurring or future actions, wait before retrying an operation (e.g. 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. Workflow operation that suspend will survive restarts, new deploys, and infrastructure changes, independent of whether the suspense takes seconds or months.

See the sleep() API Reference for the full list of supported duration formats and detailed API documentation, and see the hooks documentation for more information on how to resume in response to external events.

Adding a Sleep Tool

We can expose the sleep tool directly to the Agent by wrapping it in a tool.

Define the Tool

ai/tools/sleep.ts
import { tool } from 'ai';
import { getWritable, sleep } from 'workflow'; 
import { z } from 'zod';
import type { UIMessageChunk } from 'ai';

const inputSchema = z.object({
  durationMs: z.number().describe('Duration to sleep in milliseconds'),
});

async function executeSleep(
  { durationMs }: z.infer<typeof inputSchema>,
  { toolCallId }: { toolCallId: string }
) {
  // Note: No "use step" here - sleep is a workflow-level function

  await sleep(durationMs); 

  return `Slept for ${durationMs}ms`;
}

export const sleepTool = tool({
  description: 'Pause execution for a specified duration',
  inputSchema,
  execute: executeSleep,
});

Note that 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.

Emitting status updates

We want the UI to react to the tool call status, so we emit a new chunk type data-wait, that the UI can then consume.

// This is just a helper function to emit the sleep status to the UI
async function emitSleepStatus(
  { durationMs }: { durationMs: number },
  { toolCallId }: { toolCallId: string }
) {
  'use step';

  const writable = getWritable<UIMessageChunk>();
  const writer = writable.getWriter();

  const seconds = Math.ceil(durationMs / 1000);

  await writer.write({
    id: toolCallId,
    type: 'data-wait',
    data: { text: `Sleeping for ${seconds} seconds` },
  });

  writer.releaseLock();
}

Then modify the tool execution to emit the status update:

async function executeSleep(
  { durationMs }: z.infer<typeof inputSchema>,
  { toolCallId }: { toolCallId: string }
) {
  await emitSleepStatus({ durationMs }, { toolCallId }); 
  await sleep(durationMs);
}

Show the tool status in the UI

// In your message rendering logic:
{message.parts.map((part, i) => {
  if (part.type === 'text') {
    return <span key={i}>{part.text}</span>;
  }
  if (part.type === 'data-wait') { 
    return <div key={i}><WaitIcon />{part.data.text}...</div>; 
  } 
  return null;
})}

Use Cases

Aside from providing sleep() as a tool, there are other use cases for Agents that commonly call for suspension and resumption.

Rate Limiting

When hitting API rate limits, use RetryableError with a delay:

async function callRateLimitedAPI(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();
}

Polling with Backoff

Poll for a result with increasing delays:

export async function pollForResult(jobId: string) {
  'use workflow';

  let attempt = 0;
  const maxAttempts = 10;

  while (attempt < maxAttempts) {
    const result = await checkJobStatus(jobId);

    if (result.status === 'complete') {
      return result.data;
    }

    attempt++;
    await sleep(Math.min(1000 * 2 ** attempt, 60000)); // Exponential backoff, max 1 minute
  }

  throw new Error('Job did not complete in time');
}

async function checkJobStatus(jobId: string) {
  'use step';
  // Check job status...
}