Why No Run - ptweezy/cronstable GitHub Wiki
Use GET /schedule/why or the cron_why_no_run MCP tool to check
why a job was not scheduled at a given time. Supply one job and one
timestamp to see the scheduler's match test field by field.
$ http get "http://127.0.0.1:8080/schedule/why?job=weekday-report&at=2026-07-14T09:00"
{
"job": "weekday-report",
"enabled": true,
"timezone": "UTC",
"at": "2026-07-14T09:00",
"at_in_zone": "2026-07-14T09:00:00+00:00",
"expression": "0 9 * * mon,fri",
"reboot": false,
"description": "At 09:00, on Monday and Friday",
"matches": false,
"checks": [
{"field": "second", "value": 0, "label": "0", "allowed": "0", "matched": true},
{"field": "minute", "value": 0, "label": "0", "allowed": "0", "matched": true},
{"field": "hour", "value": 9, "label": "9", "allowed": "9", "matched": true},
{"field": "day-of-month", "value": 14, "label": "14", "allowed": "any", "matched": true},
{"field": "month", "value": 7, "label": "July", "allowed": "any", "matched": true},
{"field": "day-of-week", "value": 2, "label": "Tuesday", "allowed": "Monday and Friday", "matched": false},
{"field": "year", "value": 2026, "label": "2026", "allowed": "any", "matched": true}
],
"failed": ["day-of-week"],
"notes": [],
"previous_fire": "2026-07-13T09:00:00+00:00",
"next_fire": "2026-07-17T09:00:00+00:00"
}Every other field matched; Tuesday is not in {Monday, Friday}.
previous_fire and next_fire give the nearest scheduled times before
and after the timestamp, computed in the job's time zone.
-
The probe runs in the job's own time zone. A timestamp with a UTC
offset (
2026-07-14T11:00:00+02:00, trailingZaccepted) is converted into the job's resolved zone first. A naive timestamp reads as wall time in that zone directly.at_in_zoneshows the instant the fields were checked against. -
One
checksrow per cron field, in field order (second, minute, hour, day-of-month, month, day-of-week, year).valueis the probe's value in cron terms (Sunday is0).labelis its human name.allowedrenders the field's accepted values as prose: an unrestricted field readsany, consecutive allowed values are shown as ranges (1-3 and 7,Monday-Friday), and theLforms are spelled out (the month's last day (L),the month's last Friday). -
matchesis exactly the engine's verdict. The explainer decomposesCronTab.testitself, one term per row, somatchesalways equals what the scheduler would compute for that civil instant. -
Hschedules check against their resolved slots. The payload carriesresolved, the expression with everyHreplaced by its hashed values.allowednames the concrete slot, so "minute 0 is not in 16" tells you where the hash landed.
Two scheduling semantics produce misses (or odd runs) that look like
bugs. The explainer flags both in notes:
-
day-fields-and-rule. In this dialect a day must satisfy both day fields when both are restricted (0 0 13 * 5is Friday the 13th). In that case classic Vixie cron fires when either matches. When exactly one of the two matched, the note says so and states plainly that Vixie cron would have run the schedule at that instant: the most common surprise when importing a system crontab. The schedule linter warns about the combination at config load, and this note pins it to the concrete instant you asked about. -
dst-skipped-time/dst-repeated-time. For a matching wall time that a DST transition in the job's zone skips, the note names the shifted wall time used by the schedule. For a repeated wall time, it says only the first occurrence is scheduled. See schedules and time zones for the underlying time model.
-
matches: true. The timetable selected the instant, so the schedule is not the cause. Check the job's run history (GET /jobs/<name>/runs, orcron_list_runsover MCP) for what execution did. Daemon downtime,concurrencyPolicy, or cluster leadership are the usual causes. -
Disabled jobs are still explained in full. The payload carries
enabled: false. A matching instant on a disabled job is its own answer. -
@rebootjobs returnreboot: truewith no checks: they run once at daemon startup and never fire on a timetable. -
DAG schedules resolve under their synthetic
dag:<name>job name, exactly as they appear in the schedule load fleet views.
cron_why_no_run serves the same payload over MCP, with a
one-line verdict an agent can act on ("NO: second, minute, hour,
day-of-month, month, year matched; day-of-week Tuesday is not in Monday
and Friday"). It pairs with cron_validate_schedule and
cron_explain_schedule. They run any prospective expression through the
daemon's engine (parse errors, description, upcoming fires,
lint findings) before it becomes a job, so the
agent's authoring loop and its debugging loop use the same source of
truth the scheduler does.
-
HTTP Control API: the
GET /schedule/whyreference, andGET /schedule/previewfor arbitrary expressions. - MCP Server: wiring an agent to the tools.
- Schedule Linting: whole-schedule advisory findings at config load.
- Schedules and Timezones: the dialect and the time model the explanations are grounded in.