The scheduled job that silently skips when the clocks change
A cron translator with timezone-aware next runs and DST warnings.
Converts cron expressions to plain language and to timezone-aware next-run times, and warns when a schedule crosses a daylight saving transition where a run will be skipped or duplicated. Small, pipeable, with an exit code for an invalid expression so CI can validate crontabs. The plain-language half is what web tools already do; the DST half is the one that produces the outage nobody attributes to a schedule.
The problem
Two failures, separated by an order of magnitude in how often they are noticed.
The one everyone knows. */15 9-17 * * 1-5 is not hard, and 0 0 1 * 1 is: it does not mean "the first Monday of the month". Cron's day-of-month and day-of-week fields are OR'd when both are restricted, so that expression fires on the first of every month and on every Monday. This is documented, surprising, and the reason a certain class of monthly report arrives weekly. The general form of the problem is that a cron expression is read at review time by someone who is not going to look up the semantics.
The one almost nobody catches. A job is scheduled at 02:30 local time. In America/New_York, on the second Sunday in March, local time jumps from 01:59 to 03:00. 02:30 does not occur. On many schedulers the job does not run, and nothing records that: from cron's perspective nothing was scheduled and nothing failed. In November the reverse happens: 01:00–01:59 occurs twice, and a job scheduled in that window may run twice.
The consequences are rarely attributed to the schedule. A nightly reconciliation that skips one night leaves a day of unreconciled records that surface as data inconsistencies a week later. A billing job that runs twice charges twice. The investigation starts from the data problem, and the schedule is the last place anyone looks, because it did not change and it has been correct for the previous 180 days.
The window between 01:00 and 03:00 local is where a great deal of batch work gets scheduled, because it is quiet. It is also the window DST disturbs.
The method
Translate, project, and flag, in that order. The output leads with the plain-language reading, then the next runs in the requested timezone, with any DST problem marked on the run it affects. The description is cron-descriptor's and the run times are croniter's; the tool does not carry a sentence builder or a scheduler of its own. The repository's README shows the common case:
$ cron-translate '*/15 9-17 * * 1-5'
*/15 9-17 * * 1-5
→ Every 15 minutes, between 09:00 and 17:59, Monday through Friday
Next 3 runs (UTC):
2026-07-10 14:15 UTC (in 0.2h)
...
Timezone is a first-class input, because the bug is a timezone bug. --tz America/New_York computes next-run times in that IANA zone, and each run a transition touches is flagged where it is printed. --between START END lists every run in a window, so a reviewer can look at the transition night itself (again quoted from the README):
$ cron-translate '30 2 * * *' --tz America/New_York \
--between 2027-03-13T00:00 2027-03-15T00:00
...
2027-03-13 02:30 EST
2027-03-14 03:00 EDT ⚠ DST: this wall-clock time does not exist (spring
forward) — the run is skipped or shifted
A run in the repeated November hour is flagged on both copies with "happens twice (fall back) — the run may fire twice". The two notes are different failures with different fixes: a skipped run needs a catch-up mechanism, a duplicated one needs idempotency.
English in, with refusals. A phrase is accepted where an expression would be, and translated only when cron can say it. "every 2 weeks" has no cron form, because the day-of-week field repeats every week, so the tool says so, offers the closest workaround and exits 64 rather than print something that looks right. Phrases cron can express with a catch, such as "every 7 minutes" (*/7 restarts at the top of each hour), are translated and flagged.
Convert, and check the conversion. cron-translate convert --from quartz --to eventbridge '0 0 12 ? * MON-FRI *' prints both expressions, one description, and the next runs of each on the same clock. The converted expression is parsed back with the target dialect's own rules and scheduled independently, so the two run columns are a check rather than a restatement. Where the target cannot express the schedule (seconds into vixie, a year limit into Kubernetes, the day-of-month/day-of-week OR reading into EventBridge or Quartz), the tool exits 65 and names the closest expression as not equivalent.
Exit codes make it a gate. 0 for a valid expression, 64 for one that cannot be read, 65 for one that was read and cannot be converted. Using the sysexits values rather than a generic 1 lets a caller distinguish "this expression is wrong" from "the tool failed".
The trade-off is that the tool's wording and parsing are only as good as the two libraries underneath, and it accepts what croniter accepts: @daily, a six-field expression with seconds, L and # all describe without complaint on the default path. What it owns, and what its tests cover, is the DST check, the phrase refusals and the converter.
The headline results
- DST flags.
- The day-field rule.
- Phrases.
- Conversion.
- What the tool does not claim
The full results, with the tables and the reasoning behind each figure, are in the PDF.
What is in the PDF
- The problem — what goes unanswered without it, and who notices first.
- Why the obvious alternative falls short — stated plainly, including where it is the better choice.
- How it works — the method, not a feature list.
- Concrete use cases — with console output quoted from the repository, never reconstructed.
- The methodology behind any number it emits — every term shown, so the figure survives a question.
- What it deliberately does not do — the section most papers leave out.
Part of the Platform & SRE theme.
Get the PDF
This paper already selected; the download starts when you send the form. No follow-up sequence.