> ## 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.

# Scheduling

> Create scheduled jobs and manage agent heartbeat timing

**What it does:** Lets agents create automated tasks that run on a schedule -- daily briefings, periodic health checks, end-of-day summaries, weekly reports.

**Who it's for:** Anyone who wants their agent to wake up on a fixed cadence (or at a specific moment in time) without being prompted. The `cron` tool covers user-defined jobs; `heartbeat_manage` covers the agent's own internal periodic check-in.

## cron -- Scheduled Jobs

The `cron` tool manages scheduled jobs with **8 actions** (`add`, `list`, `update`, `remove`, `status`, `runs`, `run`, `wake`) and **3 schedule types** (`cron`, `every`, `at`). Jobs are persisted in `~/.comis/data.db` (SQLite) so they survive daemon restarts -- on startup the scheduler replays missed firings and resumes future schedules. Agents can create, update, monitor, and remove jobs from within a conversation.

Each cron job runs with its own [tool policy](/skills/tool-policy) -- the `cron-minimal` profile is the default, restricting the job to a safe subset (web\_search, message, read/write file, list\_dir, memory\_store/search, cron, discover). Override per job to give a scheduled agent more or fewer tools.

### Schedule Types

| Type    | Syntax                   | Example                | Meaning                     |
| ------- | ------------------------ | ---------------------- | --------------------------- |
| `cron`  | Standard cron expression | `0 9 * * *`            | Every day at 9:00 AM        |
| `every` | Interval in milliseconds | `1800000`              | Every 30 minutes            |
| `at`    | ISO 8601 timestamp       | `2026-03-15T14:00:00Z` | One-time at a specific time |

<Tip>
  Use `cron` for recurring jobs on a fixed schedule (like "every weekday at 9am"). Use `every` for repeating intervals specified in milliseconds. Use `at` for one-time tasks at a specific moment.
</Tip>

### Actions

<AccordionGroup>
  <Accordion title="add -- Create a scheduled job">
    Creates a new cron job with a specified schedule and payload.

    **Parameters:**

    | Parameter           | Type    | Required  | Description                                                                                                                                             |
    | ------------------- | ------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
    | `action`            | string  | Yes       | Must be `add`                                                                                                                                           |
    | `name`              | string  | Yes       | A human-readable name for the job                                                                                                                       |
    | `schedule_kind`     | string  | Yes       | Schedule type: `cron`, `every`, or `at`                                                                                                                 |
    | `schedule_expr`     | string  | For cron  | Standard cron expression (for `schedule_kind: cron`)                                                                                                    |
    | `schedule_every_ms` | integer | For every | Interval in milliseconds (for `schedule_kind: every`)                                                                                                   |
    | `schedule_at`       | string  | For at    | ISO 8601 datetime string (for `schedule_kind: at`)                                                                                                      |
    | `timezone`          | string  | No        | IANA timezone (e.g., `America/New_York`). Defaults to UTC.                                                                                              |
    | `payload_kind`      | string  | Yes       | What happens when the job fires: `system_event` or `agent_turn`                                                                                         |
    | `payload_text`      | string  | Yes       | The message or payload content sent when the job triggers                                                                                               |
    | `session_strategy`  | string  | No        | Session history strategy for recurring jobs: `fresh` (new session each run, default), `rolling` (keep last N turns), or `accumulate` (keep all history) |
    | `max_history_turns` | integer | No        | Number of recent turns to keep when using `rolling` strategy (default: 3)                                                                               |
    | `model`             | string  | No        | Model override for cron-triggered agent turns (e.g., `gemini-2.5-flash`). Only applies to `agent_turn` payload kind.                                    |

    **Example -- Daily 9am briefing:**

    ```yaml theme={null}
    action: add
    name: morning-briefing
    schedule_kind: cron
    schedule_expr: "0 9 * * *"
    timezone: America/New_York
    payload_kind: agent_turn
    payload_text: "Good morning! Run the daily briefing."
    session_strategy: rolling
    max_history_turns: 5
    ```

    **Example -- One-time reminder:**

    ```yaml theme={null}
    action: add
    name: meeting-reminder
    schedule_kind: at
    schedule_at: "2026-03-15T14:00:00Z"
    payload_kind: agent_turn
    payload_text: "Reminder: Team meeting starts now."
    ```

    **Example -- Periodic system event every 30 minutes:**

    ```yaml theme={null}
    action: add
    name: health-check
    schedule_kind: every
    schedule_every_ms: 1800000
    payload_kind: system_event
    payload_text: "Run health check"
    ```
  </Accordion>

  <Accordion title="list -- List all scheduled jobs">
    Lists all scheduled jobs with their names, schedules, and current status.

    **Parameters:**

    | Parameter | Type   | Required | Description    |
    | --------- | ------ | -------- | -------------- |
    | `action`  | string | Yes      | Must be `list` |

    Returns a list of all jobs including their name, schedule type, expression, and whether they are active or paused.
  </Accordion>

  <Accordion title="update -- Update an existing job">
    Modifies an existing scheduled job. You can change the name or enable/disable the job.

    **Parameters:**

    | Parameter  | Type    | Required | Description                   |
    | ---------- | ------- | -------- | ----------------------------- |
    | `action`   | string  | Yes      | Must be `update`              |
    | `job_name` | string  | Yes      | The name of the job to update |
    | `name`     | string  | No       | New name for the job          |
    | `enabled`  | boolean | No       | Enable or disable the job     |

    **Example -- Disable a job:**

    ```yaml theme={null}
    action: update
    job_name: morning-briefing
    enabled: false
    ```
  </Accordion>

  <Accordion title="remove -- Delete a scheduled job">
    Permanently deletes a scheduled job. This is a destructive action requiring user confirmation.

    **Parameters:**

    | Parameter  | Type   | Required | Description                   |
    | ---------- | ------ | -------- | ----------------------------- |
    | `action`   | string | Yes      | Must be `remove`              |
    | `job_name` | string | Yes      | The name of the job to delete |
  </Accordion>

  <Accordion title="status -- Check scheduler status">
    Shows the current status of the scheduler, including overall health information.

    **Parameters:**

    | Parameter | Type   | Required | Description      |
    | --------- | ------ | -------- | ---------------- |
    | `action`  | string | Yes      | Must be `status` |

    Returns the scheduler's health status, active job count, and overall state.
  </Accordion>

  <Accordion title="runs -- View recent run history">
    Shows recent execution history for a job, including success/failure status and timestamps.

    **Parameters:**

    | Parameter  | Type    | Required | Description                                                   |
    | ---------- | ------- | -------- | ------------------------------------------------------------- |
    | `action`   | string  | Yes      | Must be `runs`                                                |
    | `job_name` | string  | Yes      | The name of the job                                           |
    | `limit`    | integer | No       | Maximum number of run history entries to return (default: 20) |
  </Accordion>

  <Accordion title="run -- Manually trigger a job">
    Immediately triggers a scheduled job, regardless of its schedule. Useful for testing or one-off execution.

    **Parameters:**

    | Parameter  | Type   | Required | Description                                                                            |
    | ---------- | ------ | -------- | -------------------------------------------------------------------------------------- |
    | `action`   | string | Yes      | Must be `run`                                                                          |
    | `job_name` | string | Yes      | The name of the job to trigger                                                         |
    | `mode`     | string | No       | Run mode: `force` (ignore schedule, run now -- default) or `due` (run only if overdue) |
  </Accordion>

  <Accordion title="wake -- Wake the scheduler">
    Wakes the scheduler after a daemon restart. This is primarily used internally to ensure the scheduler picks up any jobs that were due while the daemon was stopped.

    **Parameters:**

    | Parameter | Type   | Required | Description                                      |
    | --------- | ------ | -------- | ------------------------------------------------ |
    | `action`  | string | Yes      | Must be `wake`                                   |
    | `source`  | string | No       | Audit trail source identifier (default: `agent`) |
  </Accordion>
</AccordionGroup>

## Wake-Gate -- Run the Model Only When It Matters

A scheduled job may carry an optional **wake-gate**: a small pre-run script that runs *before* the job's payload and decides whether to invoke the model at all. It is the efficiency layer for a polling job -- a monitor that fires every few minutes usually finds nothing changed, and a wake-gate lets it answer "nothing to do" *without* spending a model turn.

The gate runs in the same jailed sandbox an autonomous [`orchestrate`](/agent-tools/orchestrate) script uses, under the agent's own resolved capabilities -- it can `web_fetch`, `read`, `grep`, and the other tools the agent already holds, but grants no new reach. It carries three fields:

| Field            | Type         | Default | Description                                                                   |
| ---------------- | ------------ | ------- | ----------------------------------------------------------------------------- |
| `script`         | string       | --      | The gate source -- the code that inspects the world and prints a verdict      |
| `language`       | `js` \| `ts` | `js`    | The gate language                                                             |
| `timeoutSeconds` | integer      | `30`    | Wall-clock budget for the gate (1--300); a gate that overruns wakes the model |

An agent attaches a gate when it creates or edits a job -- pass the gate source as **`wake_gate_script`** (and optionally **`wake_gate_language`**, `js` or `ts`) to the `cron` tool's `add` or `update` action. Only the script and language are authorable this way: a gate created through the `cron` tool or the web editor always uses the 30-second default `timeoutSeconds` -- the field is not exposed as a tool or editor parameter.

### The verdict protocol

The gate decides by what it prints. Its **last non-empty line of stdout** is read as the verdict:

| Last line                           | Meaning                                                                                                     |
| ----------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `{"wake": false}`                   | **Skip** -- the payload never runs and the model is never invoked                                           |
| `{"wake": true, "context": "..."}`  | **Wake** -- run the model, injecting the pre-gathered `context` finding so the turn starts already informed |
| `{"wake": false, "deliver": "..."}` | **Routine status** -- deliver the `deliver` text straight to the job's channel with **no** model turn       |
| `HEARTBEAT_OK` or `NO_REPLY`        | **Skip** -- a bare sentinel line, same as `{"wake": false}`                                                 |
| `[SILENT]` or `[SILENT] <status>`   | **Skip** -- a trailing `<status>` after `[SILENT]` is delivered like `deliver`                              |
| anything else                       | **Wake** -- a bounded tail of the gate's stdout rides along as `context`                                    |

The sentinel match is case-insensitive. Both `context` and `deliver` are bounded (roughly the last 4,000 characters) so a gate injects a content-light finding rather than an unbounded script dump, and a delivered status is scrubbed for secrets before it reaches the channel.

<Warning>
  **The gate is fail-open.** A gate that crashes, exits non-zero, times out, over-caps its output, or prints no parseable verdict **wakes** the model -- a broken monitor is never silently dropped. Only an explicit `{"wake": false}` (or a silent sentinel) on the last line skips a fire. If the host cannot build the sandbox, or the agent's autonomy is disabled, the job simply runs as it would without a gate.
</Warning>

### Example -- skip the model when the queue is empty

A gate that checks a build queue and only wakes the agent when something is waiting:

```javascript theme={null}
// wake-gate script (language: js). The jailed SDK is imported, not global —
// the same typed `comis_tools` an orchestrate script uses.
import { comis_tools } from "./comis_tools.js";

// web_fetch returns a ResultRef; inspect it with .jq()/.grep() (or .preview).
const queue = await comis_tools.web_fetch({ url: "https://ci.example.com/api/queue" });
const pending = await queue.jq(".pending | length");

if (pending === 0) {
  // Nothing to triage -- deliver a one-line status, no model turn.
  console.log('{"wake": false, "deliver": "Build queue clear ✓"}');
} else {
  // Wake the agent with the pending count pre-gathered as context.
  console.log(JSON.stringify({ wake: true, context: `Pending builds: ${pending}` }));
}
```

On an empty queue the fire costs one sandboxed HTTP call and zero model turns; only a non-empty queue pays for a model turn, and that turn starts with the pending list already in hand.

<Note>
  **A wake-gate is not the `wake` action.** The `cron` tool's `wake` action (and the `scheduler.wake` RPC) is a **restart-replay** -- it nudges the scheduler to pick up firings missed while the daemon was down. A **wake-gate** is a per-job pre-run decision about a *single* fire. They share a word and nothing else.
</Note>

Every gated fire is observable without exposing what the gate gathered: a skipped fire is recorded in the job's run history (`runs` shows it with a `skipped` status and the avoided-turn count), and the daemon-wide skip-rate, turns-saved, and gate tool-call cost roll up into the [fleet health report](/reference/cli#comis-fleet). See the [operations scheduler guide](/operations/scheduler#wake-gate-efficiency) for the operator view.

## heartbeat\_manage -- Agent Heartbeat

The `heartbeat_manage` tool controls the agent's periodic heartbeat -- a built-in scheduled event that fires on its own clock (separate from `cron` jobs). The heartbeat runs with the `heartbeat-minimal` tool policy by default (just `message`, `memory_store`, `memory_search`, `discover`) and is intended for lightweight check-ins.

**Heartbeat semantics:**

* The agent runs its heartbeat prompt every `interval_ms` (millisecond resolution).
* Output is filtered before delivery: a special `HEARTBEAT_OK` token signals "all good" and is silently dropped, so users only see something when there is actually something to report.
* `light_context: true` boots the heartbeat with just `HEARTBEAT.md` in scope, keeping context tiny.
* Alert thresholds (`alert_threshold`, `alert_cooldown_ms`) let the agent escalate after N consecutive failures while avoiding alert spam.
* Stale detection (`stale_ms`) flags an agent whose heartbeats stop landing.

### Actions

<AccordionGroup>
  <Accordion title="get -- View heartbeat configuration">
    Returns the current heartbeat settings (per-agent and effective config), including the interval and the prompt that runs on each beat.

    **Parameters:**

    | Parameter  | Type   | Required | Description                                         |
    | ---------- | ------ | -------- | --------------------------------------------------- |
    | `action`   | string | Yes      | Must be `get`                                       |
    | `agent_id` | string | No       | Agent ID to inspect (defaults to the calling agent) |
  </Accordion>

  <Accordion title="update -- Change heartbeat settings">
    Updates the heartbeat configuration. Pass only the fields you want to change.

    **Parameters:**

    | Parameter             | Type    | Required | Description                                                   |
    | --------------------- | ------- | -------- | ------------------------------------------------------------- |
    | `action`              | string  | Yes      | Must be `update`                                              |
    | `agent_id`            | string  | No       | Agent ID to update (defaults to the calling agent)            |
    | `enabled`             | boolean | No       | Enable or disable the heartbeat                               |
    | `interval_ms`         | integer | No       | Heartbeat interval in milliseconds (e.g., `300000` for 5 min) |
    | `prompt`              | string  | No       | Custom heartbeat prompt text                                  |
    | `model`               | string  | No       | Model override for heartbeat LLM calls                        |
    | `target_channel_type` | string  | No       | Delivery target channel type (e.g., `telegram`, `discord`)    |
    | `target_channel_id`   | string  | No       | Delivery target channel identifier                            |
    | `target_chat_id`      | string  | No       | Delivery target chat/conversation ID                          |
    | `target_is_dm`        | boolean | No       | Whether the delivery target is a DM conversation              |
    | `light_context`       | boolean | No       | Use lightweight bootstrap context (HEARTBEAT.md only)         |
    | `show_ok`             | boolean | No       | Show OK status notifications                                  |
    | `show_alerts`         | boolean | No       | Show alert notifications                                      |
    | `allow_dm`            | boolean | No       | Allow DM delivery of heartbeat alerts                         |
    | `alert_threshold`     | integer | No       | Consecutive failures before alerting                          |
    | `alert_cooldown_ms`   | integer | No       | Minimum ms between alerts                                     |
    | `stale_ms`            | integer | No       | Max ms before stuck detection triggers                        |

    **Example -- Check for new emails every 5 minutes:**

    ```yaml theme={null}
    action: update
    interval_ms: 300000
    prompt: "Check for new emails and summarize any important ones."
    ```
  </Accordion>

  <Accordion title="status -- Check heartbeat status">
    Shows runtime heartbeat state across all agents, including last fire time and next scheduled fire.

    **Parameters:**

    | Parameter | Type   | Required | Description      |
    | --------- | ------ | -------- | ---------------- |
    | `action`  | string | Yes      | Must be `status` |
  </Accordion>

  <Accordion title="trigger -- Manually trigger a heartbeat">
    Immediately fires the heartbeat for an agent, running the configured prompt right now.

    **Parameters:**

    | Parameter  | Type   | Required | Description                                      |
    | ---------- | ------ | -------- | ------------------------------------------------ |
    | `action`   | string | Yes      | Must be `trigger`                                |
    | `agent_id` | string | No       | Agent ID to fire (defaults to the calling agent) |
  </Accordion>
</AccordionGroup>

## End-to-end example: daily research summary

A common workflow is to have an agent run an autonomous research pass every weekday morning and post the findings to a Discord channel. You can set this up entirely in chat -- the agent calls `cron` once and the daemon handles the rest:

```
You: Every weekday at 9am Eastern, research the latest AI infrastructure
     news from the last 24 hours and post a 3-bullet summary to my
     #ai-news Discord channel.
```

The agent translates that into a single `cron` add call:

```yaml theme={null}
action: add
name: ai-news-daily-summary
schedule_kind: cron
schedule_expr: "0 9 * * 1-5"            # 9:00 AM, Monday through Friday
timezone: America/New_York
payload_kind: agent_turn
payload_text: >
  Research the latest AI infrastructure news from the past 24 hours
  using web_search. Post a 3-bullet summary (sources linked) to the
  #ai-news Discord channel via the message tool.
session_strategy: rolling                # Keep last 3 turns for continuity
max_history_turns: 3
```

What happens next:

1. The cron entry persists in `~/.comis/data.db` immediately.
2. At 9:00 ET each weekday the scheduler enqueues an agent turn with the configured payload.
3. The agent runs against the `cron-minimal` tool policy by default -- enough to call `web_search` and `message`.
4. Output lands in `#ai-news` as a normal Discord message; the `runs` action shows the per-execution log.

Use `cron action: runs name: ai-news-daily-summary limit: 5` to inspect recent executions, or `cron action: run name: ai-news-daily-summary` to fire it immediately for a sanity check.

## Failure modes

* **Invalid cron expression** -- the `add` action validates the expression up front and returns a structured error before persisting anything.
* **Missed firings during downtime** -- on daemon restart, the `wake` action replays any jobs whose `schedule_at` or cron tick was missed while the daemon was offline.
* **Agent turn errors** -- failures are logged with `errorKind` in the run history; alerts fire when `alert_threshold` consecutive failures occur within `alert_cooldown_ms`.
* **Heartbeat suppression** -- a heartbeat that returns `HEARTBEAT_OK` is silenced before delivery, so users only see meaningful pings.

## Related

<CardGroup cols={2}>
  <Card title="Operations Scheduler" icon="gears" href="/operations/scheduler">
    Server-side scheduler configuration and monitoring
  </Card>

  <Card title="Agent Tools Overview" icon="toolbox" href="/agent-tools/index">
    Master reference table of all tools
  </Card>

  <Card title="Messaging" icon="paper-plane" href="/agent-tools/messaging">
    Send, reply, react, edit, and delete messages
  </Card>

  <Card title="Sessions" icon="users" href="/agent-tools/sessions">
    Sub-agents, pipelines, and session management
  </Card>
</CardGroup>
