---
title: Scheduler
description: 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`:

```yaml title="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

```typescript
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

```typescript
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"

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

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

```json
{
  "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:

```typescript
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:

```typescript
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.

```typescript
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`.

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