> ## Documentation Index
> Fetch the complete documentation index at: https://comis-feature-skill-archive-import.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Scheduler

> Cron jobs, heartbeat monitoring, and task extraction

The scheduler lets your agents work on a schedule — sending periodic reports, running health checks, and even creating their own tasks from conversations. Think of it as a personal assistant's calendar that agents manage themselves.

## Three Scheduling Systems

Comis includes three complementary scheduling systems, each designed for a different kind of automated work.

### Cron Jobs

Cron jobs are recurring tasks that run on a schedule, like "check email every morning" or "send a daily summary at 5pm." Your agents create and manage cron jobs using the scheduling tools — you configure the limits, and they handle the details.

```yaml theme={}
# config.yaml
scheduler:
  cron:
    enabled: true
    maxConcurrentRuns: 3    # Maximum jobs running at the same time
    defaultTimezone: ""     # Empty = UTC
    maxJobs: 100            # Maximum number of scheduled jobs
```

| Setting             | Default    | What It Does                                        |
| ------------------- | ---------- | --------------------------------------------------- |
| `enabled`           | `true`     | Whether cron scheduling is active                   |
| `maxConcurrentRuns` | 3          | How many cron jobs can run simultaneously           |
| `defaultTimezone`   | `""` (UTC) | Default timezone for cron expressions               |
| `maxJobs`           | 100        | Maximum number of cron jobs allowed (0 = unlimited) |

<Tip>Agents can create their own cron jobs using the scheduling tools. You set the limits (how many jobs, how many run at once); they decide what to schedule and when.</Tip>

### Heartbeat Monitoring

The heartbeat system runs periodic health checks at regular intervals (every 5 minutes by default). This is how the [monitoring sources](/operations/monitoring) — disk space, CPU, memory, systemd services, and security updates — get checked.

```yaml theme={}
# config.yaml
scheduler:
  heartbeat:
    enabled: true
    intervalMs: 300000        # Check every 5 minutes
    alertThreshold: 2         # Failures before alerting
    alertCooldownMs: 300000   # Wait 5 min between alerts for same source
    staleMs: 120000           # Detect stuck checks after 2 min
    showOk: false             # Don't report when everything is fine
    showAlerts: true          # Do report problems
```

| Setting           | Default        | What It Does                                             |
| ----------------- | -------------- | -------------------------------------------------------- |
| `intervalMs`      | 300000 (5 min) | How often health checks run                              |
| `alertThreshold`  | 2              | Consecutive failures required before sending an alert    |
| `alertCooldownMs` | 300000 (5 min) | Minimum time between repeated alerts for the same issue  |
| `staleMs`         | 120000 (2 min) | If a check runs longer than this, it is considered stuck |
| `showOk`          | `false`        | Whether to include healthy sources in the report         |
| `showAlerts`      | `true`         | Whether to include alerts in the report                  |

The heartbeat uses **wake coalescing** — all monitoring checks are batched together into a single heartbeat cycle rather than each running independently. This prevents multiple checks from overwhelming the system simultaneously.

### Task Extraction

During a conversation, your agent might notice follow-up work — "I should check back on this tomorrow" or "remind the user about that deadline." Task extraction lets agents automatically schedule these follow-ups.

```yaml theme={}
# config.yaml
scheduler:
  tasks:
    enabled: true               # On by default — the agent proactively follows up
    confidenceThreshold: 0.8    # Minimum confidence (0-1) to schedule a task
```

<Info>Task extraction is **on by default** — the agent is fully capable out of the box and proactively schedules follow-ups it notices in conversation (only above `confidenceThreshold`). Set `enabled: false` to opt out if you prefer the agent never self-schedules.</Info>

## Quiet Hours

Quiet hours prevent non-critical scheduled tasks from running during specified times — for example, overnight when you do not want notifications.

```yaml theme={}
# config.yaml
scheduler:
  quietHours:
    enabled: false
    start: "22:00"
    end: "07:00"
    timezone: ""           # Empty = system local timezone
    criticalBypass: true   # Critical tasks ignore quiet hours
```

When quiet hours are active, cron jobs and heartbeat alerts are suppressed. They resume when the quiet window ends.

<Tip>Critical tasks (like security alerts) bypass quiet hours by default. Set `criticalBypass: false` only if you want complete silence during quiet hours.</Tip>

## Wake-Gate Efficiency

A polling cron job -- "check the build queue every five minutes," "scan the inbox every ten" -- usually finds nothing changed, and waking the model on every empty fire is pure waste. A [wake-gate](/agent-tools/scheduling#wake-gate-run-the-model-only-when-it-matters) lets a job answer "nothing to do" *before* the model runs: a small sandboxed script inspects the world and returns a verdict, and only a verdict that says "wake" spends a model turn. A gate can also deliver a routine ✓ status straight to the channel with no model turn at all -- and that routine status honors quiet hours, so an all-clear ping never lands overnight.

The gate is an **efficiency** layer, not a suppression control, and its posture is designed to stay legible to an operator:

* **Fail-open.** A gate that errors, times out, over-caps its output, or returns no verdict **wakes** the model. A broken gate can never silently drop a monitored job -- the worst a misbehaving gate does is wake the model too often (spending turns), never too rarely (missing an alert).
* **Skip-rate.** The share of a job's fires the gate skipped. A high skip-rate is usually the gate *working* -- most polls found nothing -- not a fault. A gate at a **100% skip-rate on a monitor you expect to fire** is the signal to inspect: it may be genuinely quiet, or its logic may have broken toward always-skip.
* **Net cost.** A gate's own tool calls (its `web_fetch`/`read`/`grep` cost) sit beside the model turns it saved. A gate whose tool-call cost exceeds the turns it avoids is not paying for itself -- compare the two numbers directly.

These signals roll up per agent in the [`comis fleet`](/reference/cli#comis-fleet) health report (skip-rate, turns-saved, tool-call cost -- counts only, never the gate's gathered findings), and each per-fire decision is visible in the job's run history via [`cron.runs`](/reference/json-rpc#cron-runs). Because the gate runs under the agent's own [autonomy capabilities](/agents/autonomy) in the standard jail, it grants no new reach -- see the [capability model](/security/capability-model#wake-gate-suppression-bound) for the suppression bound.

Scheduler-initiated gates are governed fleet-wide by the **`scheduler.cron.wakeGate`** toggle. It defaults to each agent's autonomy `script` surface -- where an agent may run `orchestrate` scripts, its cron jobs may run wake-gates. Set it to `false` to disable scheduler-initiated pre-run gates while still allowing model-initiated `orchestrate`: a scheduler gate is a distinct trust context (no human or model in the loop at fire time), and with the toggle off a gated job simply runs as it would with no gate. The toggle grants no capability -- it only decides whether the pre-run script runs.

## Safety

The scheduler includes several safety mechanisms to prevent runaway tasks:

* **Lock files** prevent the same cron job from running twice simultaneously
* **`maxConcurrentRuns`** limits how many jobs can execute at the same time (default: 3)
* **`staleMs`** detects stuck tasks that have been running too long (default: 2 minutes)
* **`maxJobs`** caps the total number of scheduled jobs (default: 100)

If a task exceeds the stale timeout, the scheduler marks it as stuck and releases the lock so it can run again on the next cycle.

## Full Configuration Reference

```yaml theme={}
# config.yaml
scheduler:
  cron:
    enabled: true
    storeDir: "./data/scheduler"
    maxConcurrentRuns: 3
    defaultTimezone: ""
    maxJobs: 100

  heartbeat:
    enabled: true
    intervalMs: 300000
    showOk: false
    showAlerts: true
    alertThreshold: 2
    alertCooldownMs: 300000
    staleMs: 120000

  quietHours:
    enabled: false
    start: "22:00"
    end: "07:00"
    timezone: ""
    criticalBypass: true

  tasks:
    enabled: false
    confidenceThreshold: 0.8
    storeDir: "./data/scheduler/tasks"
```

## Related Pages

<CardGroup cols={2}>
  <Card title="Monitoring" icon="heart-pulse" href="/operations/monitoring">
    The 5 health sources checked by heartbeat
  </Card>

  <Card title="Daemon" icon="server" href="/operations/daemon">
    The process that runs the scheduler
  </Card>

  <Card title="Agent Lifecycle" icon="arrows-spin" href="/agents/lifecycle">
    How agents process messages and use tools
  </Card>

  <Card title="Scheduler View" icon="calendar" href="/web-dashboard/scheduler-view">
    View and manage scheduled tasks in the dashboard
  </Card>
</CardGroup>
