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:
capabilities:
scheduler: trueThis 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
| Type | Field | Behavior |
|---|---|---|
| Cron | cron: "0 12 * * *" | Recurring via cron expression, reschedules after each fire |
| Interval | type: 'recurring', interval: 60000 | Reschedules at now + interval after each fire |
| One-shot | type: '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:
| Format | Example | Description |
|---|---|---|
| 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 intervalCleanup
The scheduler is automatically stopped during runtime.cleanup() when using RuntimeBuilder.
process.on('SIGINT', async () => {
scheduler.stop();
await gateway.stop();
process.exit(0);
});