ToyTools Guide
Converting crontab Lines to systemd OnCalendar
How the OnCalendar grammar maps onto cron fields, and the one day rule that changes meaning in translation.
Quick Answer
A crontab line maps onto OnCalendar= field by field:
minute hour day-of-month month day-of-week becomes
DayOfWeek Year-Month-Day Hour:Minute:Second. For example,
0 9 * * 1-5 becomes OnCalendar=Mon..Fri *-*-* 09:00:00. The mapping is
mechanical except for one rule, covered below, which changes what the schedule means rather
than how it is written.
How The Fields Line Up
cron reads five fields in a fixed order. systemd reads a calendar expression with an optional
weekday prefix, a date part, and a time part. Both accept * for "any",
comma lists, and ranges, so most of a translation is rearrangement.
Going from crontab to OnCalendar is mostly a matter of learning where each field lands. The OnCalendar syntax is stricter than cron in one helpful way and looser in another: systemd timer units separate the schedule from the job, so the same systemd oncalendar syntax can drive several services, while the day-of-week and day-of-month rules differ in a way covered below. Everything else about crontab to systemd translation is mechanical.
- Minute and hour move into the time part, as
hour:minute:00. - Day of month and month move into the date part, as
*-month-day. - Day of week becomes the prefix, written
Mon,Mon..Fri, or a comma list. - Seconds have no cron field, so a translation always writes
:00.
To write an OnCalendar expression for every 15 minutes, keep the hour a wildcard and step the minute field:
OnCalendar=*-*-* *:00/15:00. Steps translate too, though they are written differently. Input:
0 star-slash-4 star star star in cron shorthand. Output:
OnCalendar=*-*-* 00/4:00:00, where 00/4 reads as "starting at 0, every
4". systemd also accepts a plain comma list, so a schedule that is not a clean arithmetic run
simply becomes *-*-* *:05,20,50:00.
The One Rule That Changes Meaning
This is the whole reason a converter is worth using rather than rearranging the fields by hand.
When a crontab line restricts both the day of the month and the day of the week, cron fires on a day where either matches. It is a documented quirk and it surprises people who have never needed it. systemd has no such rule: the date specification and the weekday prefix are independent constraints, so the timer fires only where both match.
For example, 0 0 13 * 5 under cron means midnight on the 13th of any month
or any Friday, which is roughly 60 runs a year. Translated literally to
Fri *-*-13 00:00:00, the timer fires only on a Friday that is also the 13th, which
is one or two runs a year. Both are valid. Both load without complaint. Nothing reports the
difference, and the job simply stops happening on most of the days it used to.
Therefore the tool evaluates both rules over the next year of dates and names the first day they disagree, or states plainly that they agree. When only one day field is restricted the rules coincide, so the check stays silent rather than warning about a schedule that translated exactly.
cron vs systemd timers
Beyond the schedule grammar the two systems differ in what they give you. A cron line is one row of text that
couples the schedule to the command. A systemd timer unit splits them: the schedule lives in a .timer
and the work in a matching .service, which means the job gets logging through the journal, resource
limits, dependency ordering, and a systemctl status that says whether the last run failed.
The trade is verbosity. Two files replace one line, and the cron vs systemd timer decision usually comes down to whether the job is worth that: a one-line cleanup probably is not, a nightly backup whose failures should be visible probably is.
Timezones And Missed Runs
A crontab can set TZ at the top of the file, and that setting does not survive the
move. OnCalendar is evaluated in the system timezone of the host, so a job written
to run at 02:00 in one zone can land hours away once it becomes a timer. Set
Timezone= in the [Timer] section when the schedule genuinely depends
on a zone rather than on the host.
The other behavioural difference is what happens when the machine was off. cron skips the run
entirely. A timer with Persistent=true runs as soon as it can afterwards, which is
usually what people expect from a nightly job on a laptop. The generated unit sets it; remove
the line to get cron's skipping behaviour back.
Shorthands And @reboot
@daily, @hourly, @weekly, @monthly and
@yearly expand to their five-field forms and then translate normally. Be careful
with @weekly: in cron it means Sunday at midnight, while systemd's own
weekly shorthand means Monday. That is why an explicit expression is emitted
rather than the shorthand passed through.
@reboot is different in kind. It is not a calendar event, so it has no
OnCalendar form at all. The replacement is OnBootSec=, which fires a
fixed interval after boot, or OnStartupSec= relative to systemd starting. The tool
says this instead of reporting a parse error, because a syntax error would send you looking for
a typo that does not exist.
Verifying On The Real Machine
Once the unit is in place, systemd-analyze calendar '<expression>' prints the
next elapse and confirms systemd parses the expression the way you meant. However, it is worth
being precise about what that proves: it validates the expression in isolation and never saw
the crontab line, so it cannot tell you the schedule still matches what you replaced. That
comparison is the one this page makes before you copy anything.
systemctl list-timers then shows the timer alongside its next and last run once it
is enabled.
Common Mistakes
- Treating a clean load as a correct schedule. Both grammars accept the translation; only the calendar tells you whether it matches.
- Carrying over TZ. The timer uses the host timezone unless told otherwise.
- Assuming weekly means the same day. cron day 0 is Sunday; systemd's week starts Monday.
- Looking for a syntax fix for @reboot. It needs a different directive, not a different expression.
Related Tools
You May Also Need
You may also need
- Cron Expression ParserRead the original crontab line in plain English first
- Timezone ConverterCheck what a run time becomes in the host timezone
Next steps
- Cron Expression ParserConfirm what the original line meant
- Timezone ConverterTranslate a run time into the server timezone
Alternatives
- Cron Expression ParserStay on cron and just understand the existing schedule