Skip to main content

Scheduling Guide

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 first holds the store-issued executor lease, then claims due occurrences and delivers them to the target at runtime.

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

Trigger types

Once

Fire one time at an exact UTC timestamp.

Interval

Fire repeatedly at a fixed cadence.
Add end_at_utc to stop the schedule automatically:

Calendar

Fire at named wall-clock times in a timezone. Omitted fields default to {"kind":"any"}.
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:
Example event delivery:
Inside an agent, meerkat_schedule_create and meerkat_schedule_update also accept the host-only current_session shortcut:
The wrapper resolves this before persistence. A normal session becomes a resumable_session target. A mob member becomes a host-minted resumable_identity target, so replacing that member’s session does not strand future occurrences. current_session is not part of the REST or JSON-RPC target schema.

Identity targets

target_kind must be "identity"; the current variant is resumable_identity. The host resolves the stable identity to the current materialized session at delivery time.
Use host-minted identities only. Public creation refuses free-form identities that the active schedule host cannot resolve. Mob-member identities accept prompt actions only and reject session-only prompt overrides such as system_prompt, skill_refs, and additional_instructions.

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:

Host-runnable targets

target_kind: "host_runnable" fires a named callback the embedding host registered with the schedule surface at startup (library hosts only — the stock rkat binaries register none). Occurrences flow through the normal occurrence lifecycle, so listing, history, receipts, and failure surfaces work exactly as for session and mob targets. An unregistered runnable is a missing target (your missing_target_policy applies); a callback failure records a runtime_rejected delivery failure.
params is an optional host-owned JSON payload handed to the runnable verbatim; Meerkat never interprets it. The payload must be non-null valid JSON and is canonicalized before it participates in the durable target identity. For long-running work, register a runnable that submits or ensures a durable job and returns immediately after durable acceptance. Use the occurrence ID as the stable submission key. The built-in ScheduledDurableJobRunnable follows this pattern: Schedule owns when to submit, while the jobs subsystem owns the attempt, lease, progress, cancellation, and terminal result. A host runnable that calls a connector-bound MCP server (for example a collector that reads a provider on a schedule) needs governed work authorization for every call, and a fire carries none: the fire is a trigger, not an authority. On a governed runtime (Rust hosts only), the host enrolls a service original for the work at operator-reviewed setup, whose basis selects the operator’s grant for exactly that installation’s MCP server. Each occurrence then builds a ServiceCallRequest from that original and the occurrence’s identities, admits it with MeerkatMachine::prepare_service_call, and makes its calls through the returned ServiceCall. The operator’s grant, the connector approval it records and the host’s operation policy decide each call. Each call is audited in the runtime store’s service-call audit log: its decision is written before the call is sent, and a call whose record cannot be written is refused unsent; its outcome is written after it returns, and a failure there is reported with the call’s result. A call whose record ends at its entry, with no outcome after it, has an unknown outcome: it was cancelled or its record was lost after its entry was written, and it may have been sent and taken effect. The host reads an occurrence’s record back with RuntimeStore::read_service_call_audit under the request’s audit_key. The schedule and occurrence identities the request carries are audit data the host asserts, not authority. An ungoverned runtime, a runtime without a store, a store without the audit log, or a host whose composition does not enroll service originals, refuses.

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
For most recurring jobs, start with:

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
An update that changes the trigger, the target or a policy is a revise: the schedule’s revision goes up, and every outstanding occurrence of an older revision, claimed and in-flight ones included, becomes superseded. A delete does the same at its own revision. Each superseded occurrence is acknowledged back to the schedule, and the schedule’s superseded_ack_ids lists the occurrences the latest revise or delete superseded; the next revise or delete replaces the list. A delivery that finishes after its occurrence was superseded is recorded on that occurrence and leaves the schedule as it is. Use schedule/occurrences or GET /schedules/{id}/occurrences to inspect what actually fired and why.

Executor Lease And Crash Recovery

The store issues a fenced executor lease to one driver incarnation. Only that lease holder can plan or claim firing work. A standby driver reports a healthy non-authorized tick and can take over after release or expiry. The default occurrence claim lease is 60 seconds. A delivery renews it around the half-lease point while the target admits it and while it runs, so a slow admission keeps its claim too. Each dispatch also carries a stable occurrence-level delivery identity. If target admission succeeds but recording the receipt fails, a later reclaim retries the same identity. Target hosts must reconcile or deduplicate that identity; the scheduler does not claim exactly-once behavior for arbitrary external effects. A retry that the target deduplicates waits for the original input’s outcome and records it. While that outcome cannot be read yet, the occurrence keeps its lease and re-reads it for up to five lease periods; then it ends delivery_failed with failure class internal_error (runtime completion authority unavailable). A reclaim after a lapsed lease follows the misfire policy unless an earlier attempt recorded its dispatch. With skip, a lapse after the grace window ends misfired. When an earlier attempt did record its dispatch:
  • With a recorded admission (the target accepted an earlier attempt), the occurrence is reclaimed, also past its misfire deadline, for up to five attempts. The redelivery only finds that admission and never admits the delivery again, and the occurrence records the admitted input’s outcome. A target still busy with that input does not end the redelivery under skip_if_running: a redelivery starts no new run. After five lost executors it ends delivery_failed with lease_lost. A target that no longer knows the admitted input (a row that recovery discarded) ends it delivery_failed with failure class runtime_rejected, though nothing was rejected, and nothing is admitted. An identity target that no longer resolves to a session ends it the same way. A target that answers that it never admitted a delivery whose admission is recorded breaks the redelivery contract: the occurrence ends delivery_failed with internal_error. A host runnable keeps no admission record and runs again: for host runnables the guarantee is at-least-once, and the runnable must deduplicate on its delivery_idempotency_key. A custom target delivery that keeps the default redelivery relies on its own durable deduplication of the delivery identity.
  • With only a recorded dispatch start, a reclaim past the deadline asks the target, a busy one too, whether it admitted the delivery. If it did, the occurrence records that input’s outcome; if not, it ends misfired, and nothing is delivered late.
  • A crash before the dispatch start was recorded leaves no evidence, and the policy applies as before: keeping crash recovery then needs catch_up_within with a window longer than the lease.
With a runtime that keeps no store, an input that completed and was archived looks the same as one that was never admitted: a late redelivery with only a recorded dispatch start then ends misfired, and one with a recorded admission ends delivery_failed with runtime_rejected. A redelivery follows missing_target_policy like any claim. When the target probe finds the target missing, the occurrence ends skipped (skip) or misfired (mark_misfired) with failure class target_missing, before the target is asked about the earlier admission. For a redelivery with a recorded admission, neither ending means that the delivery never ran: its attempt_count shows the reclaim, and the occurrence’s receipt history (ScheduleStore::list_receipts) keeps the earlier attempt’s dispatch_accepted receipt. Poisoned schedule and occurrence rows are isolated as typed per-row faults so healthy rows can continue. Hosts should surface the driver fault report rather than treating a successful partial tick as a clean scheduler.

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). Every schedule tool that returns one schedule (meerkat_schedule_get, meerkat_schedule_create, meerkat_schedule_update, meerkat_schedule_pause, meerkat_schedule_resume and meerkat_schedule_delete) returns it in the public shape of schedule/get: the full trigger and target, policies, planning state and configuration, without the persistence machine state. meerkat_schedule_list returns one page of summaries, in creation order: schedule_id, name, phase, revision, trigger (only its type and timezone, with trigger_chars, when its serialized form is over 1,024 bytes), the target’s kind and ids (each cut to its first 200 characters), the first 160 characters of the action text (action_head, with its full length in action_chars), and the created and updated times. Deleted schedules are not listed. The page holds 50 schedules unless limit asks for another size, at most 200 (a larger limit returns 200, and page.has_more says more follow); pass the reply’s opaque page.next_cursor as cursor to continue. A limit of 0, a negative or non-integer limit, or a cursor this listing did not write is refused. Read a schedule’s whole action, policies and options with meerkat_schedule_get. The host listings (schedule/list over RPC and GET /schedules over REST) are unchanged: every schedule, whole. Bulk readers use them and the typed occurrence listing (schedule/occurrences, GET /schedules/{id}/occurrences) rather than the agent tools. A schedule with many occurrences has a long history, and meerkat_schedule_occurrences returns it one page of summaries at a time, in due order: 50 occurrences unless limit asks for another size, at most 200 (a larger limit returns 200), with the opaque cursor from page.next_cursor to continue. A summary (view: "summary", the default for a page) carries the occurrence’s identity, timing and delivery state, the target’s kind and ids, the last receipt summarized, and the first 160 characters of a runtime outcome or failure detail (runtime_outcome.detail_head, failure_detail_head): no trigger, target action or prompt. view: "full" returns each occurrence whole, with its trigger and target once. To read less, pass occurrence_id (that one occurrence whole, with all of its delivery receipts; add view: "summary" to shorten it), due_after_utc / due_before_utc (inclusive, RFC 3339 with any offset) or phase. To cover one local day, pass its first instant and its last instant, for example 2026-03-29T00:00:00+01:00 and 2026-03-29T23:59:59.999+02:00. A receipt with stage completed means delivery finished, not that the target’s work succeeded. Agent-authored spawn_helper and fork_helper schedules capture the creator’s effective tool-access policy at creation time. This prevents a scheduled helper from gaining tools the creator could not use. Public host APIs reject helper-tooling modes that depend on an active parent context.

Tearing down an embedded schedule host

Rust hosts that compose scheduling themselves (agent-side schedule tools in the factory builder’s default_schedule_tools slot, and a schedule host from spawn_runtime_backed_schedule_host, its _with_mobs variants or spawn_schedule_host) need no extra teardown step for them. The schedule host attaches its session and mob hosts to the ScheduleService as the create-time target probe for as long as the host runs, and the service and every clone of it, including the one inside the schedule tools in the builder’s slot, hold that probe only weakly. Once the schedule host has shut down, nothing reached through the service keeps the session service (which the runtime-backed session host holds) or the mob state (which a mob host holds) alive, so a mob store’s observer drain (SqliteMobStores::drain_observers) does not need the slot cleared. Anything the host holds itself, such as a registry that resolves schedule targets to mob state, stays its own to release. After the schedule host stops, and until another one starts for the same service, no surface judges identity targets: creating a schedule with an identity target, or updating a schedule to one, fails with ScheduleDomainError::DriverStopped, and the agent-facing schedule tools report the tool as temporarily unavailable. Session targets and every other schedule operation (get, list, pause, resume, delete, occurrences) keep working against the store.

See also