Cron, turned into a systemd timer.
A crontab line and an OnCalendar= value say the same kind of thing in two languages, and the
two disagree where it hurts: systemd writes the date before the time, reads a stepped range from
01 rather than 00, and calls the week from Monday. Paste a line here and you get
the calendar value, the two unit files, and every difference written out.
Crontab to systemd timer converter
Try one
Where the two languages, part ways.
A five-field crontab and a systemd calendar describe the same idea, a set of instants. The differences are all in the spelling. These are the ones that change when a job runs, not just how the line looks.
| Subject | A crontab | A systemd calendar |
|---|---|---|
| Field order | minute hour day-of-month month day-of-week | day-of-week, then date, then time: Mon..Fri 09:00:00 |
| Day names | 0 to 7, where Sunday is 0 and 7 | Mon … Sun, and a range only runs forwards; Sun..Sat is refused |
| Both day fields restricted | fires when either field matches | one expression, so both would have to match at once |
| weekOfDay steps | */2 counts from Sunday | ranges cannot wrap, so a stepped cron list is written out: Sun,Tue,Thu,Sat |
| Steps | */15 counts from zero | padded to two digits: *:00/15:00, and *-01/3-01 in the date |
| @weekly | Sunday midnight | weekly means Monday midnight |
| Seconds | not expressible: the line fires at second zero | a calendar has seconds: *:*:* fires every second |
| @reboot | the boot event | not a calendar: OnBootSec=, or a oneshot service at boot |
| Time zone | the server’s own zone | the calendar can carry one: Mon..Fri 09:00:00 Europe/Kyiv |
The two files, and the three commands.
A schedule in systemd is never one file. The .timer unit holds the calendar, the
.service unit holds what to run, and the timer starts the service. Both files go in
/etc/systemd/system/, and the names have to match: job.timer starts
job.service.
job.timer
[Unit] Description=Run job.timer [Timer] OnCalendar=Mon..Fri 09:00:00 Persistent=true [Install] WantedBy=timers.target
job.service
[Unit] Description=What this job does [Service] Type=oneshot ExecStart=/usr/local/bin/job.sh
-
systemd-analyze calendar "Mon..Fri 09:00:00": systemd's own reading of the line, and the next time it would fire. Run it before enabling anything: it is the same check this page's tests run against every conversion. -
sudo systemctl enable --now job.timer: writes the symlink and starts the timer.systemctl list-timersthen shows the next elapse. -
Persistent=true: a missed run (the machine was off) is started as soon as the timer comes up. Without it systemd waits for the next matching instant, however far away it is. -
RandomizedDelaySec=5m: spreads many timers that share anOnCalendarvalue, which is the systemd answer to a thundering herd across machines. -
Monotonic timers (
OnBootSec=,OnUnitActiveSec=) count from an event rather than a clock time, and are the right tool for "ten minutes after boot" or "an hour after the last run".OnCalendar=is the one that stands in for a crontab.
Lines, and what they become.
Every row below is rendered from the converter itself, at build time: the table is the tool, not a copy of it.
| A crontab line | OnCalendar | Why |
|---|---|---|
| 0 9 * * 1-5 | Mon..Fri 09:00:00 | a weekday range becomes names, and the date part disappears |
| */15 9-17 * * 1-5 | Mon..Fri 09..17:00/15:00 | the hour range carries the minute step inside the time field |
| 0 0 1 * * | monthly | the first of the month has a systemd shortcut: monthly |
| @weekly | Sun 00:00:00 | the crontab nickname is Sunday, so it cannot become systemd’s weekly |
| 30 3 * * 0 | Sun 03:30:00 | cron numbers Sunday from zero, systemd names it |
| 0 0 29 2 * | *-02-29 00:00:00 | the timer is asked for a date only leap years reach |
| 0 0 1 */3 * | *-01/3-01 00:00:00 | a month step starts at 01 in a calendar, not at 00 |
| 0 0 1 * 1 | This line restricts both the day of the month and the day of the week. A crontab fires when either one matches; a systemd calendar would need both at once, which is a different (and much rarer) schedule. Split it into two timers, one per field. | both day fields restricted: refused, with the reason why |
Two of these come back as a refusal rather than a value, on purpose. A line that restricts both the day of the month and the day of the week has no single calendar equivalent, and a timer that silently ran on the wrong days would be worse than a message that says why.
systemd timer questions, answered.
How do I convert a crontab line to a systemd timer?
Take the three schedule fields and reorder them. A crontab writes minute, hour, day of month, month and day of week; a systemd calendar writes the day of week, then the date, then the time: 0 9 * * 1-5 becomes Mon..Fri 09:00:00. Put that value in the OnCalendar= line of a .timer unit and run the job from the matching .service unit. The converter above writes both files, and refuses a line that has no calendar equivalent instead of guessing.
Why does my OnCalendar timer not fire on the day I expected?
The usual cause is a line that restricts both the day of the month and the day of the week, such as 0 0 1 * 1. A crontab fires when either field matches, so that line runs on the first of the month and on every Monday. A systemd calendar is a single expression, so it would need both at once, a much rarer schedule. The converter refuses these lines and tells you to split them into two timers.
Does systemd weekly mean Monday or Sunday?
Monday. systemd's own weekly shortcut is equivalent to Mon 00:00:00. The crontab nickname @weekly is Sunday midnight, because cron counts its week from Sunday. That one line is where the two languages disagree most quietly: @weekly converts to Sun 00:00:00, never to weekly.
Can a systemd timer run every second?
Yes. A calendar carries seconds, so OnCalendar=*:*:* fires every second. A five-field crontab cannot say that at all: it fires once per matching minute, at second zero. This is the one direction where the timer is more precise than the line it came from, and the converter says so rather than rounding your schedule to the minute.
How do I run a job at boot with a systemd timer?
@reboot is an event, not a time of day, so it has no OnCalendar value. Ask systemd for the event instead: a oneshot service with WantedBy=multi-user.target starts it at boot, or a timer with OnBootSec=1min does the same after a short delay. If the job also has a schedule and must catch up a run missed while the machine was off, add Persistent=true to the timer.
How do I check an OnCalendar expression before I enable it?
systemd-analyze calendar "Mon..Fri 09:00:00" prints the normalised form and the next elapse time, and it is the fastest way to see whether systemd accepts what you wrote. This site uses that very command as the oracle for the converter: every line in the table below is checked against systemd itself, not only against our own reading of it.
The line itself is worth reading before you schedule it: the parser breaks any five-field expression into its fields and shows what each one means. Coming from Quartz, Spring or a Node scheduler instead? The converter drops the seconds field and maps the rest. And if the line came from someone else's notes, the mistakes page collects the traps these expressions are usually carrying.