Skip to main content
The AgentScheduler enables automated, recurring execution of agents on a schedule using cron expressions or fixed intervals.

Overview

AgentScheduler provides:
  • Cron-based scheduling - Run agents on complex schedules (“daily at 9 AM”, “every Monday”, etc.)
  • Interval-based scheduling - Run agents every N milliseconds
  • Lifecycle callbacks - React to job triggers, completions, and errors
  • Event streaming - Stream events as jobs execute
  • Execution tracking - Monitor runs, errors, and schedules
  • Pause/resume - Control job execution dynamically

Quick Start

import { AgentScheduler, AgentBuilder } from "@iqai/adk";

// Build an agent
const { runner } = await AgentBuilder
  .create("daily-reporter")
  .withModel("gemini-2.5-flash")
  .withInstruction("Generate a daily summary report")
  .build();

// Create scheduler
const scheduler = new AgentScheduler();

// Schedule the agent
scheduler.schedule({
  id: "daily-report",
  cron: "0 9 * * *", // Every day at 9:00 AM
  runner,
  userId: "system",
  input: "Generate today's report",
  onComplete: (id, events) => {
    console.log(`Report generated with ${events.length} events`);
  },
});

// Start the scheduler
scheduler.start();

// Later: stop the scheduler
await scheduler.stop();

Cron Expressions

ADK uses standard cron syntax with 5 fields:
┌───────────── minute (0 - 59)
│ ┌───────────── hour (0 - 23)
│ │ ┌───────────── day of month (1 - 31)
│ │ │ ┌───────────── month (1 - 12)
│ │ │ │ ┌───────────── day of week (0 - 7, 0 or 7 is Sunday)
│ │ │ │ │
│ │ │ │ │
* * * * *

Common Patterns

scheduler.schedule({
  id: "every-minute",
  cron: "* * * * *",
  // ...
});
ADK uses the croner library for cron parsing with robust error handling and timezone support.

Interval-based Scheduling

Alternatively, schedule jobs at fixed intervals:
scheduler.schedule({
  id: "every-30-seconds",
  intervalMs: 30_000, // 30 seconds
  runner,
  userId: "system",
  input: "Check for new messages",
});
You must provide either cron OR intervalMs, but not both.

Scheduled Job Configuration

interface ScheduledJob {
  /** Unique identifier for this job */
  id: string;
  
  /** Cron expression (e.g., "0 9 * * *" for daily at 9 AM) */
  cron?: string;
  
  /** Interval in milliseconds (alternative to cron) */
  intervalMs?: number;
  
  /** The runner to execute */
  runner: EnhancedRunner<any, any>;
  
  /** User ID for the session */
  userId: string;
  
  /** Session ID (optional - creates new session each run if not provided) */
  sessionId?: string;
  
  /** Input message to send on each scheduled run */
  input: string | Content;
  
  /** Whether the job is enabled (default: true) */
  enabled?: boolean;
  
  /** Maximum number of executions (undefined = unlimited) */
  maxExecutions?: number;
  
  /** Callback when job triggers */
  onTrigger?: (jobId: string) => void;
  
  /** Callback when execution completes */
  onComplete?: (jobId: string, events: Event[]) => void;
  
  /** Callback on execution error */
  onError?: (jobId: string, error: Error) => void;
  
  /** Callback for each event as it streams in during execution */
  onEvent?: (jobId: string, event: Event) => void;
}

Lifecycle Callbacks

React to job execution events:
scheduler.schedule({
  id: "monitored-job",
  cron: "0 * * * *",
  runner,
  userId: "system",
  input: "Run task",
  
  onTrigger: (jobId) => {
    console.log(`[${jobId}] Starting execution...`);
  },
  
  onEvent: (jobId, event) => {
    // Called for each event during execution
    if (event.isFinalResponse()) {
      console.log(`[${jobId}] Final response:`, event.text);
    }
  },
  
  onComplete: (jobId, events) => {
    console.log(`[${jobId}] Completed with ${events.length} events`);
    // Send notification, update database, etc.
  },
  
  onError: (jobId, error) => {
    console.error(`[${jobId}] Failed:`, error.message);
    // Alert on-call, retry logic, etc.
  },
});

Managing Jobs

Pause and Resume

// Pause a job
scheduler.pause("daily-report");

// Resume a paused job
scheduler.resume("daily-report");

Unschedule

// Remove a job completely
scheduler.unschedule("daily-report");

Trigger Manually

Run a scheduled job outside its normal schedule:
// Trigger and wait for completion
const events = await scheduler.triggerNow("daily-report");
console.log("Manual trigger completed:", events);

// Or stream events as they occur
for await (const event of scheduler.triggerNowStream("daily-report")) {
  console.log("Event:", event);
}

Job Status

const status = scheduler.getJobStatus("daily-report");
console.log(status);
// {
//   enabled: true,
//   isRunning: false,
//   executionCount: 42,
//   lastRunTime: 1234567890,
//   nextRunTime: 1234571490,
//   lastError: undefined
// }

// List all job IDs
const jobIds = scheduler.getJobIds();
console.log("Scheduled jobs:", jobIds);

Scheduler Events

Listen to scheduler-level events:
scheduler.addEventListener((event) => {
  console.log("Scheduler event:", event.type, event.scheduleId);
});

// Event types:
// - "schedule:triggered" - Job started
// - "schedule:completed" - Job finished successfully
// - "schedule:failed" - Job encountered an error
// - "schedule:paused" - Job was paused
// - "schedule:resumed" - Job was resumed

Advanced Examples

Limited Execution Count

Run a job a specific number of times:
scheduler.schedule({
  id: "onboarding-reminder",
  intervalMs: 24 * 60 * 60 * 1000, // Daily
  runner,
  userId: "new-user-123",
  input: "Send onboarding reminder",
  maxExecutions: 7, // Only run for 7 days
});
After 7 executions, the job automatically pauses.

Persistent Sessions

Maintain conversation state across scheduled runs:
const sessionId = "persistent-session-123";

scheduler.schedule({
  id: "conversational-job",
  cron: "0 */4 * * *", // Every 4 hours
  runner,
  userId: "system",
  sessionId, // Reuse same session
  input: "Continue the conversation",
});

Multiple Scheduled Agents

const scheduler = new AgentScheduler();

// Morning reports
scheduler.schedule({
  id: "morning-summary",
  cron: "0 8 * * 1-5", // Weekdays at 8 AM
  runner: morningReporter,
  userId: "system",
  input: "Generate morning summary",
});

// Afternoon updates
scheduler.schedule({
  id: "afternoon-update",
  cron: "0 14 * * 1-5", // Weekdays at 2 PM
  runner: afternoonReporter,
  userId: "system",
  input: "Generate afternoon update",
});

// Nightly cleanup
scheduler.schedule({
  id: "nightly-cleanup",
  cron: "0 2 * * *", // Daily at 2 AM
  runner: cleanupAgent,
  userId: "system",
  input: "Run cleanup tasks",
});

scheduler.start();

Dynamic Scheduling

Schedule jobs based on runtime conditions:
const scheduler = new AgentScheduler();
scheduler.start();

// Later in your application...
function scheduleUserReminder(userId: string, time: string) {
  scheduler.schedule({
    id: `reminder-${userId}`,
    cron: time, // e.g., "0 9 * * *"
    runner: reminderAgent,
    userId,
    input: "Send daily reminder",
    onComplete: (jobId, events) => {
      // Track completion in database
      db.recordReminder(userId, events);
    },
  });
}

// User signs up
scheduleUserReminder("user-123", "0 9 * * *");

// User unsubscribes
scheduler.unschedule("reminder-user-123");

Error Handling

Scheduler handles errors gracefully:
scheduler.schedule({
  id: "robust-job",
  cron: "0 * * * *",
  runner,
  userId: "system",
  input: "Run task",
  
  onError: async (jobId, error) => {
    console.error(`Job ${jobId} failed:`, error);
    
    // Send alert
    await notifyOncall({
      message: `Scheduled job ${jobId} failed`,
      error: error.message,
    });
    
    // Don't throw - job will retry on next schedule
  },
});
Failed jobs don’t stop the scheduler. The job will retry on its next scheduled time.

Preventing Overlapping Executions

Scheduler automatically prevents overlaps:
scheduler.schedule({
  id: "long-running-job",
  intervalMs: 60_000, // Every minute
  runner: slowAgent, // Takes 3 minutes to complete
  userId: "system",
  input: "Run slow task",
});

// If job is still running when next interval hits,
// the new execution is skipped (logged as warning)

Graceful Shutdown

process.on("SIGTERM", async () => {
  console.log("Shutting down scheduler...");
  
  // Stop accepting new executions
  await scheduler.stop();
  
  // Waits up to 30 seconds for running jobs to complete
  console.log("Scheduler stopped gracefully");
  process.exit(0);
});

Best Practices

Use unique job IDs - Job IDs must be unique. Consider prefixing with a namespace (e.g., user-${userId}-reminder).
Set reasonable intervals - Don’t schedule jobs too frequently. LLMs and tools have rate limits.
Handle errors gracefully - Always implement onError callbacks to handle failures.
Monitor execution times - Track job duration to ensure schedules don’t overlap.
Use persistent sessions carefully - Reusing sessions can grow memory over time. Consider ending sessions periodically.
Scheduled jobs run indefinitely until stopped. Ensure proper cleanup in long-running applications.
Cron schedules use the system’s local timezone. Consider timezone implications for distributed systems.

See Also

Build docs developers (and LLMs) love