TypeScriptADK-TS

Agent Scheduler

Schedule recurring agent execution with cron expressions or fixed intervals

The AgentScheduler runs ADK-TS agents on a recurring schedule using cron expressions or fixed intervals. A single instance manages multiple jobs concurrently, each with its own schedule, callbacks, and execution tracking. It also handles overlap prevention, graceful shutdown, and real-time event streaming.

Use it when agents need to run on a timer: daily reports, periodic health checks, scheduled data processing, or recurring monitoring.

Prerequisites

AgentScheduler works with any agent built through AgentBuilder. You'll need a built runner instance to schedule jobs.

Getting Started

Build an agent with AgentBuilder, then pass its runner to the scheduler:

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

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

// 2. Create the scheduler and add a job
const scheduler = new AgentScheduler();

scheduler.schedule({
  id: "daily-report",
  cron: "0 9 * * *", // Every day at 9 AM
  runner,
  userId: "system",
  input: "Generate today's report",
});

// 3. Start the scheduler
scheduler.start();

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

The scheduler validates your cron expression at registration time and throws immediately if it's invalid. Jobs added while the scheduler is already running start automatically.

Scheduling Options

Every job requires either a cron expression or an intervalMs value, not both.

Cron Expressions

Cron expressions use the standard 5-field format. The scheduler uses croner under the hood.

scheduler.schedule({
  id: "weekday-standup",
  cron: "30 9 * * 1-5", // Mon-Fri at 9:30 AM
  runner,
  userId: "system",
  input: "Summarize yesterday's progress and today's priorities",
});

scheduler.schedule({
  id: "weekly-digest",
  cron: "0 17 * * 5", // Friday at 5 PM
  runner,
  userId: "system",
  input: "Generate the weekly team digest",
});

Common cron patterns:

ExpressionSchedule
* * * * *Every minute
0 * * * *Every hour
0 9 * * *Daily at 9 AM
0 9 * * 1-5Weekdays at 9 AM
0 0 1 * *First day of every month

Fixed Intervals

For simple recurring tasks where wall-clock alignment doesn't matter:

scheduler.schedule({
  id: "health-check",
  intervalMs: 5 * 60 * 1000, // Every 5 minutes
  runner,
  userId: "system",
  input: "Run a system health check and report any anomalies",
});

scheduler.schedule({
  id: "cache-refresh",
  intervalMs: 30 * 60 * 1000, // Every 30 minutes
  runner,
  userId: "system",
  input: "Refresh the data cache with latest entries",
});

Job Configuration

PropertyTypeRequiredDescription
idstringYesUnique job identifier
cronstringOne of cron / intervalMsCron expression (5-field)
intervalMsnumberOne of cron / intervalMsFixed interval in milliseconds
runnerEnhancedRunnerYesRunner from AgentBuilder.build()
userIdstringYesUser ID for the session
inputstring | ContentYesMessage sent on each execution
sessionIdstringNoPin a session across runs
enabledbooleanNoWhether the job starts enabled (default: true)
maxExecutionsnumberNoAuto-pause after N executions
onTrigger(jobId) => voidNoCalled when execution starts
onComplete(jobId, events) => voidNoCalled on successful completion
onError(jobId, error) => voidNoCalled on execution failure
onEvent(jobId, event) => voidNoCalled for each streamed event

Job Lifecycle

The scheduler provides methods for each lifecycle transition.

Scheduling and Removal

Schedule a new job with schedule(). Remove it entirely with unschedule():

// Add a job
scheduler.schedule({
  id: "my-job",
  cron: "0 */2 * * *",
  runner,
  userId: "system",
  input: "Do the thing",
});

// Remove a job entirely
scheduler.unschedule("my-job");

Pausing and Resuming

Pause a job to temporarily stop its timer without removing it. Resume picks up where it left off:

// Pause - stops the timer, keeps config
scheduler.pause("my-job");

// Resume - restarts the timer
scheduler.resume("my-job");

Manual Triggering

Execute a job immediately, outside its normal schedule:

// Collect all events after execution completes
const events = await scheduler.triggerNow("my-job");
console.log(`Got ${events.length} events`);

// Or stream events as they arrive
for await (const event of scheduler.triggerNowStream("my-job")) {
  console.log(event);
}

Monitoring Status

Get the current status of a job, including next run time and last error:

const status = scheduler.getJobStatus("my-job");
// {
//   enabled: true,
//   isRunning: false,
//   executionCount: 42,
//   lastRunTime: 1706000000000,
//   nextRunTime: 1706007200000,
//   lastError: undefined
// }

// List all registered job IDs
const jobIds = scheduler.getJobIds();

Execution Limits

Automatically pause a job after a fixed number of runs:

scheduler.schedule({
  id: "one-time-migration",
  intervalMs: 60_000,
  runner,
  userId: "system",
  input: "Run the data migration step",
  maxExecutions: 1, // Runs once, then auto-pauses
});

Sessions

By default, each execution creates a new session ID (scheduled-{jobId}-{timestamp}). Pin a session to persist conversation history across runs:

scheduler.schedule({
  id: "ongoing-monitor",
  cron: "*/30 * * * *",
  runner,
  userId: "system",
  sessionId: "monitor-persistent-session", // Same session every run
  input: "Check for new alerts since your last check",
});

Event Handling

The scheduler supports both global listeners and per-job callbacks.

Per-Job Callbacks

Register callbacks for key job events when scheduling:

scheduler.schedule({
  id: "report-job",
  cron: "0 9 * * *",
  runner,
  userId: "system",
  input: "Generate the report",
  onTrigger: jobId => {
    console.log(`Job ${jobId} starting`);
  },
  onEvent: (jobId, event) => {
    // Fires for every ADK-TS event during execution
    console.log(`[${jobId}]`, event);
  },
  onComplete: (jobId, events) => {
    console.log(`Job ${jobId} finished with ${events.length} events`);
  },
  onError: (jobId, error) => {
    console.error(`Job ${jobId} failed:`, error.message);
  },
});

Global Event Listeners

Listen to lifecycle events across all jobs:

scheduler.addEventListener(event => {
  // event.type: "schedule:triggered" | "schedule:completed"
  //           | "schedule:failed" | "schedule:paused" | "schedule:resumed"
  console.log(`[${event.type}] ${event.scheduleId} at ${event.timestamp}`);

  if (event.type === "schedule:failed") {
    alertOpsTeam(event.scheduleId, event.data?.error);
  }
});
Event TypeFired When
schedule:triggeredExecution begins
schedule:completedExecution finishes successfully
schedule:failedExecution throws an error
schedule:pausedJob is paused
schedule:resumedJob is resumed

Streaming Execution Events

triggerNowStream returns an AsyncGenerator<Event>, same pattern as runner.runAsync():

for await (const event of scheduler.triggerNowStream("my-job")) {
  if (event.content?.parts) {
    for (const part of event.content.parts) {
      if ("text" in part) {
        process.stdout.write(part.text);
      }
    }
  }
}

Available Methods

schedule(config: ScheduledJob): void

Register a new job. Throws if the ID already exists or if no cron/intervalMs is provided. Cron expressions are validated immediately.

unschedule(jobId: string): boolean

Remove a job and stop its timer. Returns false if the job doesn't exist.

pause(jobId: string): boolean

Pause a job's timer. The job stays registered. Emits schedule:paused.

resume(jobId: string): boolean

Resume a paused job. Emits schedule:resumed. Restarts the timer if the scheduler is running.

triggerNow(jobId: string): Promise<Event[]>

Execute a job immediately. Returns all events after completion.

triggerNowStream(jobId: string): AsyncGenerator<Event>

Execute a job immediately and yield events as they stream in.

getJobStatus(jobId: string): JobStatus | undefined

Return the current status of a job:

interface JobStatus {
  enabled: boolean;
  isRunning: boolean;
  executionCount: number;
  lastRunTime?: number;
  nextRunTime?: number;
  lastError?: string;
}

getJobIds(): string[]

Return all registered job IDs.

start(): void

Start the scheduler. All enabled jobs begin their timers.

stop(): Promise<void>

Stop the scheduler. Waits up to 30 seconds for running jobs to finish before resolving.

addEventListener(listener): void

Add a global event listener.

removeEventListener(listener): void

Remove a previously added listener.

Advanced Patterns

Error Handling with Retry Notifications

Combine onError with an external alerting system to handle failures:

scheduler.schedule({
  id: "critical-job",
  cron: "0 */1 * * *",
  runner,
  userId: "system",
  input: "Run the critical pipeline",
  onError: async (jobId, error) => {
    await sendSlackAlert(`Scheduled job ${jobId} failed: ${error.message}`);
  },
});

// Global listener for centralized logging
scheduler.addEventListener(event => {
  if (event.type === "schedule:failed") {
    metrics.increment("scheduler.failures", {
      job: event.scheduleId,
    });
  }
});

Overlap Prevention

The scheduler automatically skips an execution if the previous run is still in progress. This is built-in and requires no configuration:

// If this job takes 10 minutes but is scheduled every 5 minutes,
// overlapping runs are silently skipped with a warning log.
scheduler.schedule({
  id: "slow-job",
  intervalMs: 5 * 60 * 1000,
  runner,
  userId: "system",
  input: "Process the full dataset",
});

Multiple Agent Types

AgentScheduler works with any agent type since it operates on the runner returned by AgentBuilder.build():

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

// Schedule an LLM agent
const { runner: llmRunner } = await AgentBuilder.create("summarizer")
  .withModel("gemini-2.5-flash")
  .withInstruction("Summarize recent activity")
  .build();

// Schedule a sequential pipeline
const writer = new LlmAgent({
  name: "writer",
  model: "gemini-2.5-flash",
  instruction: "Draft the weekly report",
  outputKey: "draft",
});

const editor = new LlmAgent({
  name: "editor",
  model: "gemini-2.5-flash",
  instruction: "Edit the draft for clarity: {draft}",
  outputKey: "final_report",
});

const { runner: pipelineRunner } = await AgentBuilder.create(
  "scheduled-pipeline",
)
  .asSequential([writer, editor])
  .build();

const scheduler = new AgentScheduler();

scheduler.schedule({
  id: "quick-summary",
  intervalMs: 60 * 60 * 1000,
  runner: llmRunner,
  userId: "system",
  input: "Summarize the last hour",
});

scheduler.schedule({
  id: "weekly-report",
  cron: "0 17 * * 5",
  runner: pipelineRunner,
  userId: "system",
  input: "Generate the weekly report",
});

scheduler.start();

Graceful Shutdown

stop() waits for in-flight executions to complete before resolving, with a 30-second timeout:

const scheduler = new AgentScheduler();
// ... schedule jobs, start ...

// Handle process signals
process.on("SIGINT", async () => {
  console.log("Shutting down scheduler...");
  await scheduler.stop(); // Waits for running jobs
  process.exit(0);
});