Schedules API
All schedule endpoints are project-scoped. The actions are enable / disable, pause / resume, and trigger.
disable and pause are different things and the difference matters. Disabling retires a schedule and is propagated to the owning adapter. Pausing holds it during an incident and records how long the hold has run, leaving the definition alone. Reach for pause when you mean "stop this for now", and disable when you mean "this should not be running any more".
List schedules
Section titled “List schedules”GET /api/v1/projects/{slug}/schedulesRole: viewer.
Query params: cursor and limit only. limit defaults to 50 and is capped at 500. The opaque cursor advances the keyset ordered by (name, id); this endpoint does not accept server-side scheduler or enabled filters.
{ "items": [ { "id": "...", "project_id": "...", "engine": "celery", "scheduler": "z4j-scheduler", "name": "email.daily-digest", "task_name": "email.send_daily_digest", "kind": "cron", "expression": "0 9 * * *", "timezone": "UTC", "queue": null, "priority": "normal", "args": [], "kwargs": {"locale": "en-US"}, "is_enabled": true, "paused_at": null, "overlap_policy": "allow", "last_run_at": "...", "next_run_at": "...", "total_runs": 42, "consecutive_failures": 0, "external_id": null, "catch_up": "skip", "source": "dashboard", "source_hash": null, "control_token": "...", "legacy_fire_control_token": null, "schedule_revision": 3, "quarantine_control_token": null, "quarantine_code": null, "quarantine_detail": null, "quarantined_at": null, "created_at": "...", "updated_at": "..." } ], "next_cursor": "...", "circuit_breaker_threshold": 5}consecutive_failures is the schedule's unbroken run of trailing failed /
acked_failed fires, newest first; 0 means the newest fire is not a failure.
circuit_breaker_threshold is the count at which the breaker auto-disables a
schedule, 0 when the breaker is switched off. See
Schedule fire history.
Get schedule
Section titled “Get schedule”GET /api/v1/projects/{slug}/schedules/{schedule_id}Role: viewer. Carries the same consecutive_failures as the list. Responses
to the mutating endpoints below return the schedule with
consecutive_failures: null, because they do not recount.
Recent runs, many schedules
Section titled “Recent runs, many schedules”GET /api/v1/projects/{slug}/schedules/runs?id={schedule_id}&id={schedule_id}&limit=20Role: viewer. id repeats, at most 500 per request (more is a 422); limit
is 1 to 50 and defaults to 20. One row per requested schedule with its newest
limit fires reduced to fire_id, status, scheduled_for, fired_at and
latency_ms, plus the project's circuit_breaker_threshold. Ids belonging to
another project are dropped from the response, not refused. This is the read
behind the dashboard's run strips; the full fire record stays on
Fire history.
{ "items": [ { "schedule_id": "...", "runs": [ { "fire_id": "...", "status": "acked_success", "scheduled_for": "...", "fired_at": "...", "latency_ms": 594 } ] } ], "limit": 20, "circuit_breaker_threshold": 5}Create schedule
Section titled “Create schedule”POST /api/v1/projects/{slug}/schedulesRole: admin. CSRF-protected.
{ "name": "email.daily-digest", "engine": "celery", "scheduler": "z4j-scheduler", "kind": "cron", "expression": "0 9 * * *", "task_name": "email.send_daily_digest", "timezone": "UTC", "queue": null, "args": [], "kwargs": {"locale": "en-US"}, "catch_up": "skip", "is_enabled": true}If scheduler is omitted, the project default owner is used. overlap_policy is not an implemented control: only "allow" is accepted, and any other value is refused rather than silently ignored.
priority is reported for every schedule and is always normal. It is not
settable on create or update and nothing in the dispatch path reads it; task
priority belongs to the engine, not to the schedule.
The field validators cap serialized args and kwargs at 64 KiB each. The
HTTP request-body limit is checked first, however, and defaults to 8192 bytes
for the whole request (Z4J_MAX_PAYLOAD_SIZE_BYTES), so the default deployment
cannot submit values near those field ceilings without raising that setting.
Update schedule
Section titled “Update schedule”PATCH /api/v1/projects/{slug}/schedules/{schedule_id}Role: admin. CSRF-protected. Mutable fields are engine, kind, expression, task_name, timezone, queue, args, kwargs, catch_up, is_enabled, and source_hash. name, scheduler, and source are not update fields; migrate ownership by deleting and recreating the schedule through the appropriate owner path.
Delete schedule
Section titled “Delete schedule”DELETE /api/v1/projects/{slug}/schedules/{schedule_id}Role: admin. CSRF-protected.
Enable / disable / trigger
Section titled “Enable / disable / trigger”POST /api/v1/projects/{slug}/schedules/{schedule_id}/enablePOST /api/v1/projects/{slug}/schedules/{schedule_id}/disablePOST /api/v1/projects/{slug}/schedules/{schedule_id}/triggerRole: operator. CSRF-protected. Enable and disable update the cadence.
trigger issues an immediate command without moving next_run_at and writes a
schedule.trigger_now audit row naming the caller. It does not create a
schedule_fires row, so REST-triggered runs do not appear in fire history and
do not populate triggered_by_user_id there.
Pause / resume
Section titled “Pause / resume”POST /api/v1/projects/{slug}/schedules/{schedule_id}/pausePOST /api/v1/projects/{slug}/schedules/{schedule_id}/resumeRole: operator. CSRF-protected. A hold, not a retirement: the definition is untouched and nothing is propagated to the owning adapter. The schedule stops firing until you resume it, and paused_at records when the hold began. Pausing an already-paused schedule does not reset that timestamp, because during an incident the useful question is how long this has been held.
Offered for schedules z4j fires. A schedule owned by celery-beat or another external scheduler is defined in that scheduler and projected here, so there is no channel to tell it to stop; those are refused with an explanation rather than given a hold that nothing would honour.
Rolling back to a release without paused_at is refused while any schedule is paused, because that column is the only record of the holds and dropping it would release every one of them silently.
Fire history
Section titled “Fire history”GET /api/v1/projects/{slug}/schedules/{schedule_id}/firesRole: viewer. Returns the schedule's recent fires, newest first. limit defaults to 100, capped at 1000. Recorded for schedules fired by z4j-scheduler.
[ { "id": "...", "fire_id": "...", "schedule_id": "...", "command_id": "...", "status": "acked_success", "scheduled_for": "...", "fired_at": "...", "acked_at": "...", "latency_ms": 594, "error_code": null, "error_message": null, "triggered_by_user_id": null }]status is stored as extensible text. Current paths use pending, accepted,
delivered, buffered, buffer_expired, buffer_stale, operator_skipped,
acked_success, acked_failed, failed, and the terminal_completed,
terminal_failed, terminal_cancelled, and terminal_timeout outcomes.
triggered_by_user_id may identify a manual scheduler-protocol fire, but null
is not proof of an automatic cadence fire: the REST trigger creates no fire
row, legacy rows omit it, and deleting the user sets the field to null. See
Schedule fire history for lifecycle and
retention.
Misfire history
Section titled “Misfire history”GET /api/v1/projects/{slug}/schedules/{schedule_id}/misfiresRole: viewer. Returns the schedule's detected misfires, newest first (limit defaults to 50, capped at 1000). A misfire is a system-detected "this enabled schedule missed its slot" event, written by the brain's misfire detector; each row carries detected_at, expected_fire_at, lateness_seconds, and the grace in force at detection time. See misfire detection.
Project-wide misfire history
Section titled “Project-wide misfire history”GET /api/v1/projects/{slug}/schedules/misfiresRole: viewer. Returns every detected misfire across ALL of the project's schedules, newest first (limit defaults to 50, capped at 1000). Unlike the per-schedule view above, each row carries its own schedule_id so an operator can triage missed slots project-wide in one call. The z4j misfires --project <slug> CLI command is the shell-side twin of this endpoint.
Import / diff / resync
Section titled “Import / diff / resync”When the scheduler is authoritative (Celery beat, APScheduler), three colon-suffixed endpoints drive the sync workflow:
POST /api/v1/projects/{slug}/schedules:importPOST /api/v1/projects/{slug}/schedules:diffPOST /api/v1/projects/{slug}/schedules:resyncAll three routes require project admin. :import accepts a body and writes
its supplied schedule snapshot into the brain. :diff accepts the same body
and previews the insert/update/delete set without writing. :resync takes no
body: it dispatches schedule.resync to online scheduler agents, then treats
their snapshots as authoritative and can delete brain rows missing from those
snapshots.
Fire-recovery routes
Section titled “Fire-recovery routes”Four further routes exist for unsticking a schedule whose fire evidence is ambiguous. They are recovery surfaces, not part of normal operation, and the incident response page is the place to start before using them.
POST /api/v1/projects/{slug}/schedules/{schedule_id}/legacy-fire-grantPOST /api/v1/projects/{slug}/schedules/{schedule_id}/resolve-terminal-firePOST /api/v1/projects/{slug}/schedules/{schedule_id}/resolve-legacy-evidenceGET /api/v1/projects/{slug}/schedules/external-control-operations/{operation_id}legacy-fire-grant requires project admin. resolve-terminal-fire and
resolve-legacy-evidence require operator. The external-control operation
lookup, which returns the durable status a Location header pointed at,
requires viewer.