Scheduler reference¶
Baseline: integrated runtime boundary. The experimental runner is a separate Cargo workspace. It uses Apalis pulses, the shared durable policy core, fixed-voter Raft, and a dedicated actual-server worker. Operator commands are available through stdin and an opt-in, separately authenticated HTTP/MCP listener. These interfaces share one command dispatcher.
Configuration¶
Run mcp-js-scheduler-runner CONFIG.json. Unknown runner fields are rejected.
| Runner field | Meaning |
|---|---|
node_id |
Stable unique voter ID |
advertise_addr, cluster_port |
Raft address and listening port |
peers |
Other fixed voters as id@host:port |
data_dir |
Dedicated durable runner store |
admission_addr |
Callback HTTP listener address |
admission_bearer_env |
Environment reference for shared callback credential |
cluster_bearer_env |
Required environment reference for the same existing Raft bearer on every voter |
limits |
Optional core limits below |
schedules |
Initial definitions; default empty |
remote |
Required worker/authority configuration below |
management |
Optional operator listener configuration below; omitted means no listener |
All replicas must share the same limits, initial definitions, and remote config. The fixed voter mapping and contract are sealed into storage. Existing incompatible stores fail closed; there is no automatic migration. The runner checkpoint schema is version 2 and the worker wire protocol version 1.
remote fields: url (worker origin), bearer_env (worker credential reference),
expires_at (RFC3339 grant expiry), session (logging tag), timeout_seconds,
heap_mb, namespace, and worker_id. The session is not a principal identity.
The worker reads JSON from MCP_SCHEDULED_WORKER_CONFIG:
{
"namespace": "operator-scheduler-1",
"worker_id": "worker-1",
"data_dir": "/var/lib/scheduled-worker/admissions",
"bearer_env": "MCP_SCHEDULER_WORKER_TOKEN",
"admission_bearer_env": "MCP_SCHEDULER_ADMISSION_TOKEN",
"admission_urls": ["http://127.0.0.1:4501"]
}
Namespace and worker ID must match the runner's remote configuration. Origins require HTTPS except loopback HTTP; embedded credentials, paths, queries, and fragments are rejected. Redirects and automatic retries are disabled. Callback fallback occurs on HTTP 409 or a definite connection-establishment failure; ambiguous sends and other errors stop the attempt. Tokens are resolved from the named environment variables, not stored as config values.
The actual server must use HTTP mode and --heap-store none (the default).
SnapshotCreator cancellation can abort with pending async references, so heap
snapshot mode is rejected. Filesystem persistence is independent of heap storage.
Worker and admission HTTP routes¶
| Route | Purpose |
|---|---|
POST /api/scheduled/v1/exec |
Bound dispatch; reserve, obtain quorum permit, consume, submit; 202 means accepted |
GET /api/scheduled/v1/executions/{id} |
Observe execution with worker credential |
POST /api/scheduled/v1/cancel |
Original bound dispatch; fence launch and reconcile stop |
POST /scheduler/v1/admit |
Runner callback; atomic checkpoint admission |
The dedicated worker mounts only the first three routes, with its configured
bearer credential and any existing JWKS enforcement. There is no fallback to
/api/exec, MCP, SSE, or HTTP functions. The runner callback uses the admission
credential. Dispatch binds version, namespace, schedule/occurrence/attempt,
owner/epoch, worker, payload digest and execution authority. It is an internal
runner protocol, not a replacement for operator management. Scheduled/admission
routers cap bodies at 512 KiB, including JSON escaping.
The management request limit applies to serialized JSON (72 KiB), so heavily escaped source can exceed it even when the decoded source is below 64 KiB.
Definition and calendar policy¶
{"name":"example","code":"console.log('scheduled')","cron":"* * * * *","timezone":"UTC","policy":{"missed":"Skip","overlap":"Skip","retry":{"max_retries":0,"delay_seconds":0}}}
| Field | Contract |
|---|---|
name |
Optional, at most 128 bytes |
code |
At most 64 KiB |
cron |
Numeric five-field expression, at most 256 bytes |
timezone |
Explicit IANA timezone, at most 128 bytes |
policy.missed |
"Skip", "RunOnce", or {"CatchUp":{"cap":N}} |
policy.overlap |
"Skip", "Queue", "Concurrent" |
policy.retry |
Additional attempts: max_retries 0–100, fixed delay_seconds 0–86400 |
Cron accepts stars, lists, ranges, and steps. Sunday is 0 or 7; restricted day-of-
month and day-of-week use OR. Named fields, aliases, seconds, years, and L/W/#/?
extensions are rejected. Croner's fixed-time jobs move spring-gap times to the first valid instant after
the gap and run only the first fall-fold instance. Wildcard/interval jobs skip
the gap and can run in both fold instances as distinct UTC due times. Creation
schedules strictly after creation time.
Skip selects the latest due slot only within the grace window. RunOnce
coalesces unobserved slots into the newest. CatchUp selects at most the newest
cap slots in chronological order and discards older slots for that observation.
The cadence stays on original calendar slots. Duplicate/backward observations do
not rearm consumed slots.
Overlap follows missed-run selection. Skip drops a slot when the schedule has
active, unknown, queued, or retry-waiting work. Queue uses FIFO with one active
occurrence per schedule. Concurrent permits different occurrences subject to
the global running limit. Retries retain occurrence identity and increment attempt;
only confirmed Failed outcomes retry. Unknown never retries automatically.
Limits¶
| Core field | Default | Validation |
|---|---|---|
schedules |
64 | 1–1024 |
records |
4096 | 1–65536 |
outstanding |
128 | Positive, no greater than records |
running |
16 | Positive, no greater than outstanding |
grace_seconds |
5 | 0–59, inclusive grace window |
Checkpoint JSON is capped at 4 MiB; oversized transitions fail closed. Claims and admission metadata retire when their occurrence/attempt leaves core retention. Cluster bodies are capped at 16 MiB; outgoing replication batches at 32 entries / 12 MiB. The Raft log and worker receipts still grow: there is no general compaction or history erasure, and these budgets are not throughput guarantees.
Operator commands and states¶
One JSON command per stdin line, sent to the acquired leader:
{"action":"status"}
{"action":"list"}
{"action":"create","definition":{"name":"example","code":"console.log('scheduled')","cron":"* * * * *","timezone":"UTC","policy":{"missed":"Skip","overlap":"Skip","retry":{"max_retries":0,"delay_seconds":0}}}}
{"action":"disable","id":1}
{"action":"cancel","occurrence_id":2}
{"action":"delete","id":1}
{"action":"acknowledge_stopped","occurrence_id":2,"dispatcher_and_work_quiesced":true}
status identifies acquired ownership; list is explicitly a stale local view.
Mutations require quorum. Creation lacks request deduplication: inspect state
after an uncertain result. EOF leaves scheduling active. Ctrl-C drains bounded
observation tasks; it does not cancel accepted external effects.
States are Pending, Running, Cancelling, Cancelled, Succeeded, Failed,
Unknown, Abandoned, each carrying attempt information. Unknown and
Cancelling retain running/overlap reservations. acknowledge_stopped accepts
Unknown or unresolved Cancelling, with explicit evidence that both the old
dispatcher and accepted work are quiescent. It marks the occurrence Abandoned
without replay; it does not cancel a previously admitted permit.
Older binaries cannot read cancellation enum variants. Do not roll back a live
store with unresolved cancellation to such a binary.
See operational recovery and admission safety.
Operator HTTP and MCP transport¶
The runner accepts this optional top-level configuration object:
{
"management": {
"addr": "127.0.0.1:4601",
"bearer_env": "MCP_SCHEDULER_OPERATOR_TOKEN",
"recovery_bearer_env": "MCP_SCHEDULER_RECOVERY_TOKEN"
}
}
addr must be a loopback socket address. bearer_env names an existing environment
variable. recovery_bearer_env is optional; omitting it disables remote recovery.
Resolved operator and recovery credentials must differ from each other and from
the worker, admission, and cluster credentials. Startup rejects reused credentials or a
non-loopback management address. Credential values are never stored in the
configuration or checkpoint. Management listener settings are per-node transport
settings; they do not change the sealed scheduling contract.
| HTTP method and path | Operation |
|---|---|
GET /scheduler/v1/status |
status command |
GET /scheduler/v1/schedules |
list command |
POST /scheduler/v1/commands |
One tagged JSON command from the operator command list above |
POST /mcp |
Sessionless MCP tools using protocol 2026-07-28 |
MCP tool names are scheduler_status, scheduler_list, scheduler_create,
scheduler_disable, scheduler_cancel, scheduler_delete, and
scheduler_acknowledge_stopped. Arguments match each stdin command with its
action field removed. status and list take an empty object. Unknown fields
and unknown tools are rejected. No update, enable, retry, or new recovery
operation is introduced.
The MCP endpoint uses per-request negotiation and authorization; it does not
create legacy MCP sessions. Every request needs a bearer header. Management
requests are limited to 72 KiB and enter a bounded queue of 64 commands. HTTP
and MCP requests with an Origin header are rejected; this listener has no
browser flow. Fixed-voter Raft separately requires its cluster bearer and private,
encrypted remote network boundary as described in feature status.
Management authentication neither encrypts Raft traffic nor grants cluster authority.
Authorization matrix¶
| Caller credential | Status, list, create, disable, cancel, delete | Acknowledge stopped |
|---|---|---|
| Missing, invalid, worker, admission, or cluster credential | Denied before body parsing | Denied before body parsing |
| Management operator credential | Allowed | Denied; recovery tool omitted from discovery |
| Separate management recovery credential | Allowed | Allowed only with explicit dispatcher_and_work_quiesced: true |
| Local operator stdin | Existing process authority | Existing explicit attestation requirement |
The recovery credential proves authorization, not quiescence. A human operator
must first establish that the old dispatcher cannot submit and accepted work has
stopped. An agent must not infer or automatically approve that attestation.
Acknowledgement accepts Unknown or unresolved Cancelling; without explicit
attestation and a committed transition, their capacity remains reserved.
Error behavior¶
| HTTP status | Management meaning |
|---|---|
| 400 | Malformed command or missing/false explicit quiescence attestation |
| 401 | Missing or invalid management bearer |
| 403 | Insufficient recovery authority or an Origin header |
| 409 | No acquired owner; command not accepted |
| 413 | Request body exceeds transport limit |
| 503 | Queue/runner unavailable, or core operation failed with potentially uncertain outcome |
Remote responses redact source, storage paths, and internal errors. Errors after
queue admission conservatively report outcome: "uncertain" and
retry_safe: false; the core does not classify validation and storage errors
separately. A queue rejection reports accepted: false. Inspect state and
operator diagnostics after an uncertain mutation; never retry creation blindly.
Disconnecting does not withdraw a queued command.
MCP operation failures use isError: true with the same safe JSON error and a
numeric status in text content. Invalid MCP tool names or argument shapes use
JSON-RPC invalid-parameters errors. HTTP authentication failures precede MCP
handling. List remains a stale local view, and successful cancellation still
means only that cancellation intent was recorded.