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
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
- JSON-RPC
- REST
- Rust SDK
Trigger types
Once
Fire one time at an exact UTC timestamp.Interval
Fire repeatedly at a fixed cadence.end_at_utc to stop the schedule automatically:
Calendar
Fire at named wall-clock times in a timezone. Omitted fields default to{"kind":"any"}.
Target types
Session targets
target_kind must be "session" with one of these type variants:
exact_session: deliver to one specific existing sessionresumable_session: deliver to a specific session that may be idle/suspendedmaterialize_on_demand_session: create a session on first fire, then reuse it
meerkat_schedule_create and
meerkat_schedule_update also accept the host-only current_session
shortcut:
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.
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 memberflow: run a named mob flowspawn_helper: create a helper in a mob and wait for itfork_helper: fork from an existing member and wait for it
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
Recommended defaults
For most recurring jobs, start with:Schedule lifecycle
Schedule phase is intentionally small:activepauseddeleted
pendingclaimeddispatchingawaiting_completioncompletedskippedmisfiredsupersededdelivery_failed
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 endsdelivery_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 endsdelivery_failedwithlease_lost. A target that no longer knows the admitted input (a row that recovery discarded) ends itdelivery_failedwith failure classruntime_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 endsdelivery_failedwithinternal_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 itsdelivery_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_withinwith a window longer than the lease.
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_createmeerkat_schedule_getmeerkat_schedule_listmeerkat_schedule_updatemeerkat_schedule_pausemeerkat_schedule_resumemeerkat_schedule_deletemeerkat_schedule_occurrences
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’sdefault_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.
