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:

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

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

See also