> ## Documentation Index
> Fetch the complete documentation index at: https://comis-fix-skill-import-vetting-gate.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 an agent create durable scheduled work, inspect its execution history, and manage the agent's periodic heartbeat.

**Who it's for:** Anyone who wants an agent to act at a fixed cadence, at an absolute time, or after a relative delay without another user prompt. The `cron` tool manages authored jobs; `heartbeat_manage` manages the agent's built-in periodic check-in.

## cron -- Scheduled Jobs

The `cron` tool has **8 actions** (`add`, `list`, `update`, `remove`, `status`, `runs`, `run`, `wake`) and **4 schedule types** (`cron`, `every`, `at`, `in`). Each agent owns a JSON job store at `<agentWorkspace>/.scheduler/cron-jobs.json` and an append-only execution ledger at `<agentWorkspace>/.scheduler/cron-executions.jsonl`. These files survive daemon restarts and are the scheduler's durable authorities.

The scheduler writes a durable claim for an occurrence and appends its started record before dispatch. Startup reconciliation can release a claim that provably stopped before start. Once a durable started record exists, or dispatch may have happened but its result is uncertain, the occurrence is terminalized as unknown and is **never automatically replayed**. New due occurrences and future schedules can still run normally.

`agent_turn` and `delivery` jobs created through the tool are bound to the **trusted originating conversation** as their exact delivery target. The tool does not expose target fields, and omitting them does not create an unbound job. Add and update responses do not echo that trusted route.

Tool profiles are explicit policy choices. [`cron-minimal`](/skills/tool-policy) is opt-in and is never silently applied to a job.

### Schedule Types

| Type    | Tool field            | Example               | Meaning                                                                          |
| ------- | --------------------- | --------------------- | -------------------------------------------------------------------------------- |
| `cron`  | `schedule_expr`       | `0 9 * * *`           | Recurring cron expression                                                        |
| `every` | `schedule_every_ms`   | `1800000`             | Recurring fixed interval in positive milliseconds                                |
| `at`    | `schedule_at`         | `2026-08-15T09:00:00` | One-shot absolute or wall-clock time                                             |
| `in`    | `schedule_in_seconds` | `120`                 | One-shot positive delay measured from now; resolved to a persisted `at` schedule |

<Tip>
  Use `in` for relative requests such as "in two minutes"; it needs no timezone or datetime calculation. Use `at` only for an absolute clock time. Pass an IANA `timezone` for user-facing wall-clock `cron` and `at` schedules so they resolve in the user's locale; an omitted timezone uses the agent's configured scheduler timezone.
</Tip>

### Payload and continuation behavior

| Payload           | Behavior                                                                                                                                                                         |
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `heartbeat_event` | Enqueues a typed event for the agent. `wake_mode: now` requests an immediate heartbeat admission; `next-heartbeat` holds it for the next periodic phase.                         |
| `delivery`        | Delivers `payload_text` exactly to the trusted originating conversation without a model turn.                                                                                    |
| `agent_turn`      | Runs the agent with `payload_text` as its message and delivers visible output through the trusted originating conversation route. An optional `model` overrides the job's model. |

Only `agent_turn` jobs have a session strategy or continuation mode:

* `fresh` starts a fresh scheduled session for every occurrence.
* `rolling` retains a bounded history; `max_history_turns` is required and may be 1--20.
* `none` performs no post-run continuation.
* `heartbeat_excerpt` admits up to 4 KiB of completed visible output as a typed event for the next periodic heartbeat.
* `origin_history` appends fully accepted delivered output idempotently to the originating conversation history. It requires the exact delivery target supplied by the trusted route.

### Actions

<AccordionGroup>
  <Accordion title="add -- Create a scheduled job">
    Creates a new job. The tool resolves authoring schedules before storing them and returns strict `{ jobId, name, schedule }` data.

    **Parameters:**

    | Parameter                   | Type    | Required            | Description                                                 |
    | --------------------------- | ------- | ------------------- | ----------------------------------------------------------- |
    | `action`                    | string  | Yes                 | Must be `add`                                               |
    | `name`                      | string  | Yes                 | Human-readable job name; names must be unique for the agent |
    | `schedule_kind`             | string  | Yes                 | `cron`, `every`, `at`, or `in`                              |
    | `schedule_expr`             | string  | For cron            | Cron expression                                             |
    | `schedule_every_ms`         | integer | For every           | Positive interval in milliseconds                           |
    | `schedule_at`               | string  | For at              | ISO 8601 absolute time or wall-clock datetime               |
    | `schedule_in_seconds`       | integer | For in              | Positive number of seconds from now                         |
    | `timezone`                  | string  | For local time      | IANA timezone for `cron` or wall-clock `at` schedules       |
    | `payload_kind`              | string  | Yes                 | `heartbeat_event`, `delivery`, or `agent_turn`              |
    | `payload_text`              | string  | Yes                 | Event text, exact delivery text, or agent-turn message      |
    | `wake_mode`                 | string  | For heartbeat event | `now` (default) or `next-heartbeat`                         |
    | `session_strategy`          | string  | For agent turn      | `fresh` (default) or `rolling`                              |
    | `max_history_turns`         | integer | For rolling         | Bounded retained turns, 1--20                               |
    | `model`                     | string  | No                  | Model override; valid only for `agent_turn`                 |
    | `continuation_mode`         | string  | For agent turn      | `none` (default), `heartbeat_excerpt`, or `origin_history`  |
    | `wake_gate_script`          | string  | No                  | Pre-run gate script; valid only for `agent_turn`            |
    | `wake_gate_language`        | string  | No                  | `js` (default) or `ts`                                      |
    | `wake_gate_timeout_seconds` | integer | No                  | Gate timeout, 1--300 seconds (default: 30)                  |

    **Daily 9am briefing:**

    ```yaml theme={}
    action: add
    name: morning-briefing
    schedule_kind: cron
    schedule_expr: "0 9 * * *"
    timezone: America/New_York
    payload_kind: agent_turn
    payload_text: "Prepare a concise morning briefing for this conversation."
    session_strategy: rolling
    max_history_turns: 5
    continuation_mode: origin_history
    ```

    **Relative reminder:**

    ```yaml theme={}
    action: add
    name: stretch-reminder
    schedule_kind: in
    schedule_in_seconds: 120
    payload_kind: delivery
    payload_text: "Time to stretch."
    ```

    **Event for the next heartbeat:**

    ```yaml theme={}
    action: add
    name: next-check-in
    schedule_kind: every
    schedule_every_ms: 1800000
    payload_kind: heartbeat_event
    payload_text: "Review the current project risks."
    wake_mode: next-heartbeat
    ```
  </Accordion>

  <Accordion title="list -- List all scheduled jobs">
    Lists authored and config-owned jobs. Each projection includes `id`, `name`, `agentId`, `source`, resolved `schedule`, `lifecycle`, and `payload`, plus the session, continuation, target, gate, cache, or tool-policy fields that apply to that job kind.

    **Parameters:**

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

  <Accordion title="update -- Update an existing job">
    Changes an authored job. Config-owned jobs cannot be edited through this action. Supply a complete schedule or payload field group when replacing that part of the job. `session_strategy` and `continuation_mode` cannot be changed by the current tool; remove and recreate the job when those policies must change. A wake-gate can be set or replaced, but the tool does not expose a clear operation.

    **Parameters:**

    | Parameter                   | Type    | Required            | Description                                                           |
    | --------------------------- | ------- | ------------------- | --------------------------------------------------------------------- |
    | `action`                    | string  | Yes                 | Must be `update`                                                      |
    | `job_name`                  | string  | Yes                 | Current job name                                                      |
    | `name`                      | string  | No                  | Replacement job name                                                  |
    | `paused`                    | boolean | No                  | `true` pauses future occurrences; `false` resumes them                |
    | `schedule_kind`             | string  | No                  | Begin a complete schedule replacement: `cron`, `every`, `at`, or `in` |
    | `schedule_expr`             | string  | For cron            | Replacement cron expression                                           |
    | `schedule_every_ms`         | integer | For every           | Replacement positive interval in milliseconds                         |
    | `schedule_at`               | string  | For at              | Replacement ISO 8601 time                                             |
    | `schedule_in_seconds`       | integer | For in              | Replacement positive delay from now                                   |
    | `timezone`                  | string  | For local time      | IANA timezone for the replacement schedule                            |
    | `payload_kind`              | string  | No                  | Begin a complete payload replacement                                  |
    | `payload_text`              | string  | With payload kind   | Replacement event, delivery, or turn text                             |
    | `wake_mode`                 | string  | For heartbeat event | `now` (default) or `next-heartbeat`                                   |
    | `model`                     | string  | For agent turn      | Replacement model override                                            |
    | `wake_gate_script`          | string  | No                  | Set or replace the `agent_turn` pre-run gate                          |
    | `wake_gate_language`        | string  | With gate           | `js` (default) or `ts`                                                |
    | `wake_gate_timeout_seconds` | integer | With gate           | Gate timeout, 1--300 seconds (default: 30)                            |

    ```yaml theme={}
    action: update
    job_name: morning-briefing
    paused: true
    ```

    Returns `{ jobName, updated }`.
  </Accordion>

  <Accordion title="remove -- Delete a scheduled job">
    Permanently removes an authored job. Config-owned jobs cannot be removed. This destructive action uses the tool confirmation handshake.

    **Parameters:**

    | Parameter    | Type    | Required          | Description                                                                                  |
    | ------------ | ------- | ----------------- | -------------------------------------------------------------------------------------------- |
    | `action`     | string  | Yes               | Must be `remove`                                                                             |
    | `job_name`   | string  | Yes               | Job name to remove                                                                           |
    | `_confirmed` | boolean | On approved retry | The agent sets this to `true` only after the user approves the returned confirmation request |

    Returns `{ jobName, removed }`.
  </Accordion>

  <Accordion title="status -- Check scheduler authority">
    Returns maintenance and authority state, not a loose health summary. The strict result contains `state`, `configuredEnabled`, `running`, `strictAuthoritiesValid`, `ownershipReconciled`, `jobCount`, `activeClaimCount`, `resolvedAgentId`, raw `store` and `ledger` evidence, reset `intent`, and optional `lastError`.

    `state` is one of `initializing`, `disabled`, `ready`, `active`, `maintenance`, or `failed`. Each raw authority reports `exists`, `bytes`, and its SHA-256 `digest`; the intent reports no intent, a bounded pending phase, or an invalid digest.

    **Parameters:**

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

  <Accordion title="runs -- View immutable run history">
    Reads immutable start/terminal groups for one job from the execution ledger. Each run projects `executionId`, `jobId`, `agentId`, `scheduledForMs`, `trigger`, `workKind`, `rootRunId`, `startedAtMs`, optional `terminalAtMs` and `durationMs`, `status`, `deliveryStatus`, optional `errorKind`, and optional bounded content-free `counters` for terminal internal actions.

    Run `status` is `started`, `dispatched`, `completed`, `failed`, `aborted`, `skipped`, or `unknown`. Delivery `deliveryStatus` is `not_requested`, `suppressed`, `pre_send_failed`, `accepted`, `partial`, `rejected`, or `unknown`. These are separate: a completed model turn does not imply that platform delivery was accepted.

    **Parameters:**

    | Parameter  | Type    | Required | Description                                          |
    | ---------- | ------- | -------- | ---------------------------------------------------- |
    | `action`   | string  | Yes      | Must be `runs`                                       |
    | `job_name` | string  | Yes      | Job name                                             |
    | `limit`    | integer | No       | Positive history limit, at most 10,000 (default: 20) |
  </Accordion>

  <Accordion title="run -- Manually trigger cron execution">
    `force` runs the selected job without waiting for its schedule and returns `triggered`, `mode`, `jobName`, `resolvedAgentId`, and one `executionId`. `due` asks the scheduler to claim all currently overdue occurrences for the agent and returns `triggered`, `mode`, `resolvedAgentId`, and the `executionIds` array.

    The current tool requires `job_name` for both modes. In `due` mode the backend does not use that name as a filter; it evaluates the agent's complete due set.

    **Parameters:**

    | Parameter  | Type   | Required | Description                                                    |
    | ---------- | ------ | -------- | -------------------------------------------------------------- |
    | `action`   | string | Yes      | Must be `run`                                                  |
    | `job_name` | string | Yes      | Selected job for `force`; required but not filtering for `due` |
    | `mode`     | string | No       | `force` (default) or `due`                                     |
  </Accordion>

  <Accordion title="wake -- Submit a scheduler wake">
    Submits a routine wake to the heartbeat coordinator. `agent` targets the calling agent; `monitoring` targets the monitoring lane. This action does not inspect or execute cron jobs.

    The result is either `accepted` or `coalesced` and always carries `correlationId`, `lane`, and `retainedReason`. An `accepted` result also reports whether the wake created a new occurrence or upgraded one already admitted.

    **Parameters:**

    | Parameter     | Type   | Required | Description                       |
    | ------------- | ------ | -------- | --------------------------------- |
    | `action`      | string | Yes      | Must be `wake`                    |
    | `wake_target` | string | No       | `agent` (default) or `monitoring` |
  </Accordion>
</AccordionGroup>

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

An `agent_turn` job may carry a wake-gate: a pre-run script that inspects a condition and decides whether the model should run for that occurrence. A quiet monitoring job can therefore poll cheaply and invoke the model only when the gate finds something actionable.

The gate runs in the same jailed sandbox as an autonomous [`orchestrate`](/agent-tools/orchestrate) script, under the agent's resolved capabilities. It grants no additional capability. The cron tool exposes all three gate fields:

| Field                       | Type         | Default | Description                                                          |
| --------------------------- | ------------ | ------- | -------------------------------------------------------------------- |
| `wake_gate_script`          | string       | --      | Gate source code; valid only for `agent_turn` jobs                   |
| `wake_gate_language`        | `js` \| `ts` | `js`    | Gate language                                                        |
| `wake_gate_timeout_seconds` | integer      | `30`    | Positive timeout bounded to 1--300 seconds by the scheduler contract |

The same fields can set or replace a gate through `add` and `update`. The timeout defaults to 30 seconds. A timed-out or failed gate is fail-open: the model wakes instead of silently dropping the occurrence.

### The verdict protocol

The last non-empty stdout line is the verdict:

| Last line                           | Meaning                                                            |
| ----------------------------------- | ------------------------------------------------------------------ |
| `{"wake": false}`                   | Skip the model turn                                                |
| `{"wake": true, "context": "..."}`  | Run the model and inject the bounded context                       |
| `{"wake": false, "deliver": "..."}` | Deliver the bounded status directly without a model turn           |
| `HEARTBEAT_OK` or `NO_REPLY`        | Skip the model turn                                                |
| `[SILENT]` or `[SILENT] <status>`   | Skip; a trailing status can be delivered directly                  |
| anything else                       | Fail open and wake the model with a bounded stdout tail as context |

<Warning>
  A crash, non-zero exit, timeout, output-cap failure, or unparseable verdict wakes the model. If the sandbox cannot be created or scheduler-initiated gates are disabled, the occurrence runs as if it had no gate.
</Warning>

```javascript theme={}
import { comis_tools } from "./comis_tools.js";

const queue = await comis_tools.web_fetch({ url: "https://ci.example.com/api/queue" });
const pending = await queue.jq(".pending | length");

if (pending === 0) {
  console.log('{"wake": false, "deliver": "Build queue clear"}');
} else {
  console.log(JSON.stringify({ wake: true, context: `Pending builds: ${pending}` }));
}
```

<Note>
  A wake-gate is a per-job pre-run decision. The `wake` action submits a typed heartbeat-coordinator wake. Neither mechanism authorizes another occurrence of uncertain cron work.
</Note>

Gated occurrences remain visible without exposing gathered content: a skipped occurrence appears as `skipped` in `runs`, while aggregate gate efficiency appears in the [system health report](/reference/cli#comis-system-health). 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, which uses a separate clock from authored cron jobs. All four actions require admin trust. The [`heartbeat-minimal`](/skills/tool-policy) tool profile is opt-in and is never silently applied.

Heartbeat response processing recognizes `HEARTBEAT_OK` as a soft acknowledgement and suppresses empty replies plus the shared `NO_REPLY` and `[SILENT]` markers. Plain text without the heartbeat token is an alert regardless of length; `ack_max_chars` applies only to residual text accompanying `HEARTBEAT_OK`. `show_ok` and `show_alerts` control visibility, `light_context` limits bootstrap context to `HEARTBEAT.md`, and `response_prefix` removes a configured prefix before classification and delivery. `alert_threshold` and `alert_cooldown_ms` govern heartbeat-source alerts; `stale_ms` bounds stuck-tick detection.

### Actions

<AccordionGroup>
  <Accordion title="get -- View heartbeat configuration">
    Calls `heartbeat.get` and returns `{ agentId, perAgent, effective }`: the stored per-agent patch and, when resolvable, the effective merged configuration.

    **Parameters:**

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

  <Accordion title="update -- Change heartbeat settings">
    Calls `heartbeat.update`, deep-merges the supplied fields into the per-agent config, validates the result, reconfigures periodic admission, and persists the config when persistence is available. It returns `{ agentId, config, updated, nextDueAtMs }`.

    **Parameters:**

    | Parameter           | Type    | Required | Description                                                          |
    | ------------------- | ------- | -------- | -------------------------------------------------------------------- |
    | `action`            | string  | Yes      | Must be `update`                                                     |
    | `agent_id`          | string  | No       | Agent to update (defaults to the calling agent)                      |
    | `enabled`           | boolean | No       | Enable or disable periodic heartbeat admission                       |
    | `interval_ms`       | integer | No       | Positive heartbeat interval in milliseconds                          |
    | `prompt`            | string  | No       | Heartbeat prompt text                                                |
    | `target`            | object  | No       | Complete exact delivery endpoint described below                     |
    | `light_context`     | boolean | No       | Use `HEARTBEAT.md`-only bootstrap context                            |
    | `show_ok`           | boolean | No       | Surface soft acknowledgements                                        |
    | `show_alerts`       | boolean | No       | Surface alert-level results                                          |
    | `allow_dm`          | boolean | No       | Allow heartbeat alert delivery to direct conversations               |
    | `ack_max_chars`     | integer | No       | Maximum response length treated as a soft acknowledgement            |
    | `response_prefix`   | string  | No       | Prefix removed before heartbeat response classification and delivery |
    | `alert_threshold`   | integer | No       | Consecutive source failures before heartbeat alerting                |
    | `alert_cooldown_ms` | integer | No       | Minimum time between alerts for the same heartbeat source            |
    | `stale_ms`          | integer | No       | Maximum tick duration before stuck detection                         |

    **Target object:**

    | Field                 | Type   | Required | Description                               |
    | --------------------- | ------ | -------- | ----------------------------------------- |
    | `channel_type`        | string | Yes      | Channel adapter type                      |
    | `channel_instance_id` | string | Yes      | Registered channel adapter instance       |
    | `conversation_id`     | string | Yes      | Exact platform conversation identifier    |
    | `thread_id`           | string | No       | Exact platform thread or topic identifier |
    | `conversation_kind`   | string | Yes      | `direct` or `shared`                      |

    The `target` object is atomic: flattened or partial endpoint fields are rejected.
  </Accordion>

  <Accordion title="status -- Check heartbeat status">
    Calls `heartbeat.states` and returns runtime state for every agent. Each entry contains exactly `agentId`, `enabled`, `intervalMs`, and nullable `nextDueAtMs`.

    **Parameters:**

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

  <Accordion title="trigger -- Submit a manual heartbeat">
    Calls `heartbeat.trigger` with a manual spacing bypass. The coordinator may return `accepted` or `coalesced`; both results include `correlationId`, `lane`, and `retainedReason`. Accepted admissions also include their disposition. A successful tool call therefore proves admission, not necessarily a new independently running occurrence.

    **Parameters:**

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

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

In a conversation, ask the agent to send a weekday summary back to that same conversation:

```
You: Every weekday at 9am Eastern, research the latest infrastructure
     news from the last 24 hours and send me a three-bullet summary here.
```

The agent creates a job without caller-supplied routing fields:

```yaml theme={}
action: add
name: daily-research-summary
schedule_kind: cron
schedule_expr: "0 9 * * 1-5"
timezone: America/New_York
payload_kind: agent_turn
payload_text: >
  Research the latest infrastructure news from the past 24 hours.
  Return a concise three-bullet summary with source links.
session_strategy: rolling
max_history_turns: 3
continuation_mode: origin_history
```

What happens next:

1. The resolved job is written to `<agentWorkspace>/.scheduler/cron-jobs.json`; execution starts and terminals append to `<agentWorkspace>/.scheduler/cron-executions.jsonl`.
2. The daemon binds the exact delivery target from the trusted originating conversation. Missing target parameters do not weaken that authority.
3. At 9:00 Eastern on weekdays, the scheduler durably claims the occurrence, records its start, and dispatches the agent turn.
4. The visible result is delivered back to the originating conversation. Because `origin_history` is selected, fully accepted text is also appended idempotently to that conversation's history.

Use `cron action: runs job_name: daily-research-summary limit: 5` to inspect execution and delivery outcomes, or `cron action: run job_name: daily-research-summary mode: force` to submit a manual occurrence.

## Failure modes

* **Invalid schedule or field group** -- strict authoring validation rejects malformed expressions, non-positive intervals, missing schedule-specific fields, unsupported values, and incomplete payload replacements before persistence.
* **Uncertain execution** -- a durable started occurrence whose owner or terminal result is lost becomes an unknown terminal. The scheduler never guesses that it is safe to replay.
* **Cron dependency suspension** -- only consecutive provider/dependency failures count toward `maxConsecutiveDependencyErrors`; reaching the configured limit pauses that cron job. Ordinary validation or agent-output failures do not consume this dependency-only budget.
* **Heartbeat alerting** -- `alert_threshold` and `alert_cooldown_ms` are separate heartbeat-source alert controls; they do not suspend cron jobs.
* **Delivery ambiguity** -- `runs` keeps computation `status` separate from `deliveryStatus`, including `partial` and `unknown`, so accepted platform delivery is never inferred from a completed turn.
* **Heartbeat suppression** -- The [heartbeat response rules](#heartbeat-manage-agent-heartbeat), visibility settings, or a missing target can legitimately produce no delivered heartbeat message while the execution remains observable.

## 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="Tool Policy" icon="shield" href="/skills/tool-policy">
    Explicit tool-profile selection for scheduled work
  </Card>

  <Card title="Sessions" icon="users" href="/agent-tools/sessions">
    Session and history behavior
  </Card>
</CardGroup>
