Cogitator
Channels

Scheduler

Schedule tasks, reminders, and recurring jobs with HeartbeatScheduler.

Overview

The HeartbeatScheduler enables your assistant to schedule tasks, set reminders, and run recurring jobs. It polls a TimerStore at regular intervals and fires overdue entries as messages through the gateway.

Setup via Config

Enable the scheduler in cogitator.yml:

cogitator.yml
capabilities:
  scheduler: true

This adds three tools to the agent automatically:

  • schedule_task — Schedule a one-off or recurring task
  • list_tasks — List all pending scheduled tasks
  • cancel_task — Cancel a task by ID

Programmatic Setup

import { HeartbeatScheduler, SimpleTimerStore } from '@cogitator-ai/channels';

const store = new SimpleTimerStore({ persistPath: '.cogitator/timers.json' });

const scheduler = new HeartbeatScheduler(store, {
  onFire: (msg) => gateway.injectMessage(msg),
  pollInterval: 30_000,
  maxRetries: 5,
  staggerMs: 5000,
  onRunComplete: (entry, status, error, durationMs) => {
    console.log(`[scheduler] ${entry.id}: ${status} (${durationMs}ms)`);
    if (error) console.error(`  error: ${error}`);
  },
});

scheduler.start();

HeartbeatConfig

interface HeartbeatConfig {
  onFire: (msg: ChannelMessage) => Promise<void> | void;
  pollInterval?: number;        // default: 30000 (30s)
  getNextCronMs?: (cron: string) => number;
  maxRetries?: number;          // default: 5 — skip after N consecutive errors
  staggerMs?: number;           // random startup delay (0-N ms) to prevent thundering herd
  onRunComplete?: (
    entry: TimerEntry,
    status: 'ok' | 'error',
    error?: string,
    durationMs?: number
  ) => void;
}

Schedule Types

TypeFieldBehavior
Croncron: "0 12 * * *"Recurring via cron expression, reschedules after each fire
Intervaltype: 'recurring', interval: 60000Reschedules at now + interval after each fire
One-shottype: 'fixed'Fires once, not rescheduled

Tool Usage

Once enabled, the agent schedules tasks naturally in conversation:

User: "Remind me to check the deployment in 30 minutes"

{
  "description": "Check the deployment",
  "delay": "30m"
}

User: "Every weekday at 9am, send me a standup reminder"

{
  "description": "Daily standup reminder",
  "cron": "0 9 * * 1-5"
}

Scheduling Formats

The schedule_task tool accepts three formats:

FormatExampleDescription
Delay"20m", "2h", "1d"Relative delay from now
Cron"0 12 * * *"Standard cron expression (recurring)
ISO datetime"2025-03-15T10:00:00Z"Absolute time (one-off)

Job Management

The scheduler exposes methods for runtime job control:

const jobs = await scheduler.listJobs();

await scheduler.disableJob(jobId);   // soft-disable, skipped on poll
await scheduler.enableJob(jobId);    // re-enable + reset error count
await scheduler.cancelJob(jobId);    // permanent cancel
const job = await scheduler.getJob(jobId);

Error Tracking

Each job tracks its run history:

interface TimerEntry {
  // ...
  lastRunAt?: number;
  lastRunStatus?: 'ok' | 'error' | 'skipped';
  lastError?: string;
  consecutiveErrors?: number;
  enabled?: boolean;
}

After maxRetries consecutive errors (default: 5), the job is automatically skipped with status 'skipped'. Re-enable it with scheduler.enableJob(id) to reset the error count.

Persistence

SimpleTimerStore persists timers to a JSON file with atomic writes (write to temp file, then rename). Timers survive restarts.

const store = new SimpleTimerStore({
  persistPath: '.cogitator/timers.json',
});

Lifecycle

scheduler.start()
  → optional stagger delay (random 0-staggerMs)
  → poll loop every pollInterval:
    → get overdue entries from store
    → skip disabled entries
    → skip entries with consecutiveErrors >= maxRetries
    → fire each as ChannelMessage via onFire
    → update run tracking (lastRunAt, lastRunStatus, consecutiveErrors)
    → mark fired + call onRunComplete
    → reschedule cron/interval entries

scheduler.stop()
  → clear the poll interval

Cleanup

The scheduler is automatically stopped during runtime.cleanup() when using RuntimeBuilder.

process.on('SIGINT', async () => {
  scheduler.stop();
  await gateway.stop();
  process.exit(0);
});

On this page