Schedule history
Auditing past scheduled workflow runs — CLI, REST, retention, and linking schedule fires to execution detail.
This is the page you reach when someone asks “did the 03:15 nightly actually run yesterday?” or “how often has this schedule fired in the last week?” Flux records every execution the scheduler creates, and the same record carries through to the workflow execution log — so an audit is always a two-step walk from schedule to execution_id to event stream.
What flux schedule history returns
flux schedule history <schedule-id-or-name>
flux schedule history nightly-rollup --limit 50
flux schedule history nightly-rollup --format json
The CLI (flux/cli.py:1446) accepts --limit (default 10) and --format (simple or json), resolves the schedule by ID or name, and prints one block per row. The simple formatter renders non-empty results without error, and --format json emits valid JSON even when the history is empty:
Execution history for schedule 'nightly-rollup':
✓ 2026-05-13T03:15:00+00:00 - COMPLETED
Execution ID: 8f1c3d2e9b7a4c61
...
✗ 2026-05-12T03:15:00+00:00 - FAILED
Execution ID: 1d4a7b2c5e8f9301
...
The status icon is ✓ for completed, ✗ for failed, ⏸ for anything else.
What the REST endpoint returns
GET /schedules/{schedule_id_or_name}/history?limit=50&offset=0
Permission: schedule:*:read. Response (ScheduleHistoryResponse):
{
"schedule_id": "9c2e1a3b4d5f6789",
"workflow_name": "rollup_daily",
"entries": [
{
"execution_id": "8f1c3d2e9b7a4c61",
"workflow_name": "rollup_daily",
"state": "COMPLETED",
"started_at": "2026-05-13T03:15:01.204+00:00",
"completed_at": "2026-05-13T03:15:48.911+00:00"
}
],
"total": 247,
"limit": 50,
"offset": 0
}
started_at and completed_at are populated from the execution’s event log — started_at from the WORKFLOW_STARTED event, completed_at from the terminal WORKFLOW_COMPLETED / WORKFLOW_FAILED / WORKFLOW_CANCELLED event. A row that has started but not finished carries a started_at and a null completed_at. Failed rows also carry an error string lifted from the WORKFLOW_FAILED event.
total is the unfiltered count, useful for paging. limit is capped only by the client. There is no since= or until= query parameter — if you want a date window, fetch JSON and filter on started_at.
What counts as a “fire”
History is scoped to the schedule that produced it. ScheduleManager.get_schedule_history (flux/schedule_manager.py) queries ExecutionContextModel filtered on executions.schedule_id — the column the scheduler stamps onto every execution it creates. Two practical consequences:
- Only scheduler-triggered executions appear. A manual
flux workflow run rollup_dailycarries noschedule_id, so it does not show up influx schedule history nightly-rollup. The history surface contains exactly the runs this schedule fired. - Two schedules for the same workflow have separate histories. If you have
nightly-rollupandnoon-rollupagainst the samerollup_daily, each schedule’s history endpoint returns only its own fires.
Treat the result as “the runs this schedule fired, recent first.” Corroborate with last_run_at / run_count on the schedule itself (flux schedule show) when you want the running totals.
Linking schedule fires to execution detail
Each row gives you an execution_id. From there:
flux execution show <execution_id> --detailed
--detailed reads the per-event log out of ExecutionEventModel, so you see the SCHEDULED → CLAIMED → RUNNING → COMPLETED transitions, every task’s start and finish, retries, and any error trace. See Events and audit logs for the full audit walkthrough.
Retention
Flux ships a retention job, but it is off by default: [flux.retention] enabled = false, so upgrades never silently remove history. Until you enable it, the executions and execution_events tables grow without bound — every task is a persisted event row — and so does the history endpoint’s result set. In production, turn it on and pick a window that matches your audit needs:
[flux.retention]
enabled = true
retention_days = 30 # delete terminal executions this long after their last event
The sweep runs inside the server (fleet-wide singleton per cycle when you run multiple replicas) and deletes terminal executions with their events, approvals, and sessions. Deleted executions disappear from flux schedule history output too — the schedule row’s run_count / failure_count totals persist, the per-run detail does not. See Retention for the sweep cadence and batch tuning, and Storage backends for the table layout.
Common audit questions
- How often has this schedule fired in the last week?
flux schedule history <name> --limit 200 --format json, filter entries client-side on execution timestamps. - Did the 03:15 run happen yesterday? Same — look for a row with the expected timestamp, and cross-check
last_run_atfromflux schedule show <name>. - Why was the 03:15 run missed? History only records fires, never gaps. Read the server log for the time window —
Found N due schedule(s)(flux/server.py) is what the scheduler logs each tick; at multiple replicas, only the replica holding the dispatch lock that cycle logs it. Absence at the expected minute across all replicas is the gap. - Did the scheduler trigger it or did someone run it manually? History only contains scheduler-triggered runs — every row is a scheduler fire. Manual
flux workflow runexecutions never appear, because they carry noschedule_id.
Missed runs and catchup
History only shows what fired. The scheduler polls every poll_interval seconds (default 30, flux/config.py:97) and a schedule is considered due when next_run_at <= now. If the server is down across a fire window, next_run_at stays where it is and the schedule fires once when the server comes back — there is no catchup of multiple missed intervals. See Cron and interval semantics for the policy detail.
What can go wrong
-
History is empty for a schedule that should have fired. The schedule is
PAUSED(flux schedule show <name>), the scheduler loop isn’t running (check server logs forError in scheduler cycle), or — with retention enabled — the rows aged out of the retention window. Multiple server replicas are not a cause: the dispatch cycle is advisory-lock-coordinated across replicas, so replica count changes neither the fire count nor the history. See Schedule management. -
Many
SCHEDULEDentries with no terminal state. The scheduler is creating executions but no worker is claiming them — almost always a labels or resources mismatch. Triage withflux worker listand the steps in Worker observability. -
History rows for a schedule you deleted. Schedule rows are deleted, but the executions they triggered persist in the
executionstable. The history endpoint returns 404 for the deleted schedule, but the underlying executions remain visible throughflux execution list --workflow <name>. This is intentional: deleting a schedule does not erase its operational record.
See also
- Schedule management — creating, pausing, resuming, deleting schedules.
- Cron and interval semantics — when “due” means “due now,” and what happens across server restarts.
- Events and audit logs — the underlying event stream every execution writes to.
- Events and audit logs —
flux execution show --detailed, the second half of every audit.