Schedule tasks to run in the future — whether that’s seconds from now, at a specific date/time, or on a recurring cron schedule. Scheduled tasks survive agent restarts and are persisted to SQLite.The scheduling system supports four modes:
Mode
Syntax
Use Case
Delayed
this.schedule(60, ...)
Run in 60 seconds
Scheduled
this.schedule(new Date(...), ...)
Run at specific time
Cron
this.schedule("0 8 * * *", ...)
Run on recurring schedule
Interval
this.scheduleEvery(30, ...)
Run every 30 seconds
Under the hood, scheduling uses Durable Object alarms to wake the agent at the right time. Tasks are stored in a SQLite table and executed in order.
Pass a Date object to schedule a task at a specific time:
// Run tomorrow at noonconst tomorrow = new Date();tomorrow.setDate(tomorrow.getDate() + 1);tomorrow.setHours(12, 0, 0, 0);await this.schedule(tomorrow, "sendReminder", { message: "Meeting time!" });// Run at a specific timestampawait this.schedule(new Date("2025-06-15T14:30:00Z"), "triggerEvent", { eventId: "conference-2025"});// Run in 2 hours using Date mathconst twoHoursFromNow = new Date(Date.now() + 2 * 60 * 60 * 1000);await this.schedule(twoHoursFromNow, "checkIn", {});
Pass a cron expression string for recurring schedules:
// Every day at 8:00 AMawait this.schedule("0 8 * * *", "dailyReport", {});// Every hourawait this.schedule("0 * * * *", "hourlyCheck", {});// Every Monday at 9:00 AMawait this.schedule("0 9 * * 1", "weeklySync", {});// Every 15 minutesawait this.schedule("*/15 * * * *", "pollForUpdates", {});// First day of every month at midnightawait this.schedule("0 0 1 * *", "monthlyCleanup", {});
scheduleEvery() is idempotent on the combination of callback name, interval, and payload — calling it multiple times with the same arguments does not create duplicate schedules. This makes it safe to call in onStart(), which runs on every Durable Object wake:
class MyAgent extends Agent { async onStart() { // Safe: only one schedule is created, no matter how many times the DO wakes await this.scheduleEvery(30, "tick"); } async tick() { console.log("tick", new Date().toISOString()); }}
Calling scheduleEvery() with a different interval or payload creates a separate schedule, even for the same callback:
// First call creates one scheduleawait this.scheduleEvery(30, "poll");// Second call with a different interval creates a second scheduleawait this.scheduleEvery(60, "poll");// Two "poll" schedules exist: one every 30s and one every 60s// Third call with the same arguments as the first is a no-opawait this.scheduleEvery(30, "poll");// Still two schedules
Different callbacks also get their own independent schedules:
// These create two separate schedules (different callbacks)await this.scheduleEvery(30, "poll");await this.scheduleEvery(30, "healthCheck");
If a callback takes longer than the interval, the next execution is skipped (not queued). This prevents runaway resource usage:
class PollingAgent extends Agent { async poll() { // If this takes 45 seconds and interval is 30 seconds, // the next poll is skipped (with a warning logged) const data = await slowExternalApi(); await this.processData(data); }}// Set up 30-second intervalawait this.scheduleEvery(30, "poll", {});
When a skip occurs, you’ll see a warning in logs:
Skipping interval schedule abc123: previous execution still running
If the callback throws an error, the interval continues — only that execution fails:
async syncData() { // Even if this throws, the interval keeps running const response = await fetch("https://api.example.com/data"); if (!response.ok) throw new Error("Sync failed"); // ...}
Use cases:
Sub-minute polling (every 10, 30, 45 seconds)
Intervals that don’t map to cron (every 90 seconds, every 7 minutes)
Durable Objects are evicted after a period of inactivity (typically 70-140 seconds with no incoming requests, WebSocket messages, or alarms). During long-running operations — streaming LLM responses, waiting on external APIs, running multi-step computations — the agent can be evicted mid-flight.keepAlive() prevents this by creating a 30-second heartbeat schedule that keeps the agent active until you are done:
const dispose = await this.keepAlive();try { // Long-running work that must not be interrupted const result = await longRunningComputation(); await sendResults(result);} finally { dispose();}
The returned disposer function cancels the heartbeat. Always call it when the work is done — otherwise the heartbeat continues indefinitely.
keepAlive() calls scheduleEvery(30, "_cf_keepAliveHeartbeat") under the hood. The internal _cf_keepAliveHeartbeat callback is a no-op — the alarm firing itself is what resets the inactivity timer. Because it uses the scheduling system:
The heartbeat does not conflict with your own schedules (the scheduling system multiplexes through a single alarm slot)
The heartbeat shows up in getSchedules() if you need to inspect it
Multiple concurrent keepAlive() calls each get their own schedule, so they do not interfere with each other
// Get all scheduled tasksconst allSchedules = this.getSchedules();// Get only cron jobsconst cronJobs = this.getSchedules({ type: "cron" });// Get tasks in the next hourconst upcoming = this.getSchedules({ timeRange: { start: new Date(), end: new Date(Date.now() + 60 * 60 * 1000) }});// Get a specific task by IDconst specific = this.getSchedules({ id: "abc123" });// Combine filtersconst upcomingCronJobs = this.getSchedules({ type: "cron", timeRange: { start: new Date(), end: new Date(Date.now() + 24 * 60 * 60 * 1000) }});
The SDK includes utilities for parsing natural language scheduling requests with AI.getSchedulePrompt()Returns a system prompt for parsing natural language into scheduling parameters:
When using scheduleSchema with OpenAI models via the AI SDK, you must pass providerOptions: { openai: { strictJsonSchema: false } } to generateObject. This is because the schema uses a discriminated union which is not compatible with OpenAI’s strict structured outputs mode.
Schedule a task to run repeatedly at a fixed interval.Parameters:
intervalSeconds - Number of seconds between executions (must be > 0)
callback - Name of the method to call
payload - Data to pass to the callback (must be JSON-serializable)
options.retry - Optional retry configuration. See Retries for details.
Returns: A Schedule object with type: "interval"Behavior:
Idempotent on (callback, interval, payload) — calling with the same callback, interval, and payload returns the existing schedule instead of creating a duplicate. A different interval or payload creates a new, independent schedule.
First execution occurs after intervalSeconds (not immediately)
If callback is still running when next execution is due, it’s skipped (overlap prevention)
If callback throws an error, the interval continues
Cancel with cancelSchedule(id) to stop the entire interval
Create a 30-second heartbeat schedule that prevents the Durable Object from being evicted due to inactivity. Returns a disposer function that cancels the heartbeat when called. The disposer is idempotent — calling it multiple times is safe.
Run an async function while keeping the Durable Object alive. The heartbeat is automatically started before the function runs and stopped when it completes (whether it succeeds or throws). Returns the value returned by the function.This is the recommended way to use keepAlive — it guarantees cleanup.