---
title: Scheduling
description: Automate durable session and mob work with once, interval, and calendar schedules.
icon: "clock"
---

# Scheduling

Meerkat's scheduler persists **schedules** and projects concrete **occurrences** from them.

## What this guide is for

Use this guide when you want to:

- create durable automated work
- understand trigger and target shapes
- choose overlap/misfire policies
- decide between host APIs and agent-side schedule tools

- A **schedule** is the durable rule: trigger + target + policies.
- An **occurrence** is one planned delivery produced from that rule.
- The **driver** claims due occurrences and delivers them to the target at runtime.

```text
Schedule (trigger + target + policies)
  -> projected occurrences
  -> driver claims due work
  -> runtime delivery
  -> completed / skipped / misfired / delivery_failed
```

## Host APIs vs agent tools

Use the typed host APIs when your application is managing schedules directly:

- JSON-RPC: `schedule/create`, `schedule/list`, `schedule/get`, `schedule/update`, `schedule/pause`, `schedule/resume`, `schedule/delete`, `schedule/occurrences`
- REST: `POST /schedules`, `GET /schedules`, `GET /schedules/{id}`, `PATCH /schedules/{id}`, `POST /schedules/{id}/pause`, `POST /schedules/{id}/resume`, `DELETE /schedules/{id}`, `GET /schedules/{id}/occurrences`

Inside an agent session, the scheduler is exposed through typed schedule tools such as `meerkat_schedule_create`, `meerkat_schedule_get`, and `meerkat_schedule_occurrences`.

When skills are enabled and the schedule capability is available, agents can
load the `schedule-workflow` companion skill for schedule authoring guidance.
Use WorkGraph for shared pending work and dependencies; use Schedule for time.

## Create a schedule

<Tabs>
  <Tab title="JSON-RPC">
    ```json
    {
      "jsonrpc": "2.0",
      "id": 1,
      "method": "schedule/create",
      "params": {
        "name": "hourly-health-check",
        "trigger": {
          "type": "interval",
          "start_at_utc": "2026-04-21T09:00:00Z",
          "every_seconds": 3600
        },
        "target": {
          "target_kind": "session",
          "type": "materialize_on_demand_session",
          "create": {
            "model": "claude-sonnet-4-6",
            "system_prompt": "You are a health-check assistant."
          },
          "action": {
            "type": "prompt",
            "prompt": "Check system health and report anomalies."
          }
        },
        "misfire_policy": { "type": "skip" },
        "overlap_policy": "skip_if_running",
        "missing_target_policy": "mark_misfired"
      }
    }
    ```
  </Tab>
  <Tab title="REST">
    ```bash
    curl -X POST http://localhost:8080/schedules \
      -H "Content-Type: application/json" \
      -d '{
        "name": "hourly-health-check",
        "trigger": {
          "type": "interval",
          "start_at_utc": "2026-04-21T09:00:00Z",
          "every_seconds": 3600
        },
        "target": {
          "target_kind": "session",
          "type": "materialize_on_demand_session",
          "create": {
            "model": "claude-sonnet-4-6",
            "system_prompt": "You are a health-check assistant."
          },
          "action": {
            "type": "prompt",
            "prompt": "Check system health and report anomalies."
          }
        },
        "misfire_policy": { "type": "skip" },
        "overlap_policy": "skip_if_running",
        "missing_target_policy": "mark_misfired"
      }'
    ```
  </Tab>
  <Tab title="Rust SDK">
    ```rust
    use chrono::Utc;
    use meerkat_schedule::{
        CreateScheduleRequest, IntervalTriggerSpec, MissingTargetPolicy,
        MisfirePolicy, OverlapPolicy, ScheduleService, SessionMaterializationSpec,
        SessionTargetBinding, TriggerSpec, TargetBinding, ScheduledSessionAction,
    };
    use std::collections::BTreeMap;

    let schedule = service.create(CreateScheduleRequest {
        name: Some("hourly-health-check".into()),
        description: None,
        trigger: TriggerSpec::Interval(IntervalTriggerSpec {
            start_at_utc: Utc::now(),
            every_seconds: 3600,
            end_at_utc: None,
        }),
        target: TargetBinding::session(SessionTargetBinding::materialize_on_demand(
            SessionMaterializationSpec {
                model: "claude-sonnet-4-6".into(),
                system_prompt: Some("You are a health-check assistant.".into()),
                ..Default::default()
            },
            ScheduledSessionAction::Prompt {
                prompt: "Check system health and report anomalies.".into(),
                system_prompt: None,
                render_metadata: None,
                skill_refs: Vec::new(),
                additional_instructions: Vec::new(),
            },
        )),
        misfire_policy: MisfirePolicy::Skip,
        overlap_policy: OverlapPolicy::SkipIfRunning,
        missing_target_policy: MissingTargetPolicy::MarkMisfired,
        labels: BTreeMap::new(),
        planning_horizon_days: None,
        planning_horizon_occurrences: None,
    }).await?;
    ```
  </Tab>
</Tabs>

## Trigger types

### Once

Fire one time at an exact UTC timestamp.

```json
{
  "type": "once",
  "due_at_utc": "2026-04-21T15:00:00Z"
}
```

### Interval

Fire repeatedly at a fixed cadence.

```json
{
  "type": "interval",
  "start_at_utc": "2026-04-21T09:00:00Z",
  "every_seconds": 3600
}
```

Add `end_at_utc` to stop the schedule automatically:

```json
{
  "type": "interval",
  "start_at_utc": "2026-04-21T09:00:00Z",
  "every_seconds": 3600,
  "end_at_utc": "2026-04-22T09:00:00Z"
}
```

### Calendar

Fire at named wall-clock times in a timezone. Omitted fields default to `{"kind":"any"}`.

```json
{
  "type": "calendar",
  "timezone": "Europe/Stockholm",
  "minute": { "kind": "values", "values": [0] },
  "hour": { "kind": "values", "values": [9] },
  "day_of_week": { "kind": "values", "values": [1, 2, 3, 4, 5] }
}
```

That means “09:00 every weekday in Stockholm”.

## Target types

### Session targets

`target_kind` must be `"session"` with one of these `type` variants:

- `exact_session`: deliver to one specific existing session
- `resumable_session`: deliver to a specific session that may be idle/suspended
- `materialize_on_demand_session`: create a session on first fire, then reuse it

Example exact-session prompt delivery:

```json
{
  "target_kind": "session",
  "type": "exact_session",
  "session_id": "01936f8a-7b2c-7000-8000-000000000001",
  "action": {
    "type": "prompt",
    "prompt": "Run the daily follow-up."
  }
}
```

Example event delivery:

```json
{
  "target_kind": "session",
  "type": "resumable_session",
  "session_id": "01936f8a-7b2c-7000-8000-000000000001",
  "action": {
    "type": "event",
    "event_type": "deploy_complete",
    "payload": { "environment": "prod" }
  }
}
```

### Mob targets

`target_kind` must be `"mob"` with one of these `type` variants:

- `member`: deliver content to a specific mob member
- `flow`: run a named mob flow
- `spawn_helper`: create a helper in a mob and wait for it
- `fork_helper`: fork from an existing member and wait for it

Example flow target:

```json
{
  "target_kind": "mob",
  "type": "flow",
  "mob_id": "release-triage",
  "flow_id": "triage",
  "params": { "severity": "high" }
}
```

## Policies

### Misfire policy

What happens when an occurrence is materially late:

- `{"type":"skip"}`: skip overdue work after the grace window
- `{"type":"catch_up_within","window_seconds":300}`: catch up if still within the lateness window

### Overlap policy

What happens when the next occurrence fires while previous work is still active:

- `skip_if_running` (recommended)
- `allow_concurrent`

### Missing target policy

What happens when the target is missing at fire time:

- `mark_misfired` (recommended)
- `skip`

## Recommended defaults

For most recurring jobs, start with:

```json
{
  "misfire_policy": { "type": "skip" },
  "overlap_policy": "skip_if_running",
  "missing_target_policy": "mark_misfired"
}
```

## Schedule lifecycle

Schedule phase is intentionally small:

- `active`
- `paused`
- `deleted`

Occurrence phase is more detailed:

- `pending`
- `claimed`
- `dispatching`
- `awaiting_completion`
- `completed`
- `skipped`
- `misfired`
- `superseded`
- `delivery_failed`

Use `schedule/occurrences` or `GET /schedules/{id}/occurrences` to inspect what actually fired and why.

## Agent-side schedule tools

When scheduling is exposed inside a running agent, the canonical tool names are:

- `meerkat_schedule_create`
- `meerkat_schedule_get`
- `meerkat_schedule_list`
- `meerkat_schedule_update`
- `meerkat_schedule_pause`
- `meerkat_schedule_resume`
- `meerkat_schedule_delete`
- `meerkat_schedule_occurrences`

These are distinct from the host APIs (`schedule/*` over RPC and `/schedules/*` over REST).

## See also

- [JSON-RPC API](/api/rpc)
- [REST API](/api/rest)
- [Builtin tools reference](/reference/builtin-tools)
