Why most converters silently change your schedule
In cron, if you restrict both the day of the month and the day of the week, the two conditions are joined with OR. Every other pair of fields is joined with AND. So this line:
30 2 15 * 1
does not mean "the 15th, but only if it is a Monday". It means 2:30am on the 15th of every month and 2:30am every Monday. Roughly 60 runs a year, not one.
systemd has no such exception. OnCalendar specifiers are always ANDed. Write the obvious single line and you get the opposite of what cron did:
| Translation | Result | Runs per year |
|---|---|---|
Mon *-*-15 02:30:00 |
Monday that is also the 15th | about 1–2 |
*-*-15 02:30:00Mon *-*-* 02:30:00 |
15th, plus every Monday | about 60 |
Two OnCalendar= lines in one timer unit are ORed by systemd, which is exactly the semantics cron had. That is the whole fix, and it is why the output above has two lines instead of one.
Three more places the two schedulers disagree
- Missed runs. If the machine is off at 3am, cron does not run the job later. A systemd timer does not either, unless you set
Persistent=true— which then does catch up, and that is a behaviour change worth deciding on rather than inheriting. - Who it runs as.
crontab -einstalls into your user's crontab, so the job runs as you. A unit file in/etc/systemd/system/runs as root. Migrating withoutUser=quietly escalates every file your script touches. @reboot. There is no timer equivalent. The correct mapping is aType=oneshotservice withWantedBy=multi-user.targetand no.timerat all.
Frequently asked
Does this handle @reboot?
Yes, and it deliberately produces a service with no timer unit. A timer cannot express "run once at boot", and stacking OnBootSec=0 on a timer gives you retriggering behaviour you probably did not ask for.
What timezone does the timer use?
Local time of the machine, same as cron — so a migration does not change when things fire. If you need a fixed zone regardless of host configuration, append it directly in the calendar spec, e.g. *-*-* 03:00:00 UTC.
Why is Persistent off by default?
Because cron does not catch up missed runs, and a converter that silently adds the behaviour is not a converter. Turn it on if you want the improvement, but know you asked for it.
My cron job runs as my user, not root.
Fill in the "Run as user" field. Alternatively install the units under ~/.config/systemd/user/ and use systemctl --user, which also lets the job run without root and without a login session if you enable lingering with loginctl enable-linger $USER.
Can I keep seconds precision from a six-field cron?
Not reliably. OnCalendar accepts a seconds field, but timers are not a real-time scheduler and drift is normal. If a job genuinely needs sub-minute accuracy, it belongs in a loop or a queue worker, not in either cron or a timer.
Does OnCalendar support step values?
Yes. *:0/5 means every five minutes starting at zero, and 0/2 in the hour position means every two hours from midnight. Ranges use two dots, as in 09..17, and that is why 9-17/2 is expanded into an explicit list instead of being passed through.
How do I verify the schedule before installing?
Run systemd-analyze calendar 'Mon..Fri *-*-* 09:00:00'. It prints the next elapse times, which is the fastest way to catch a translation that looks plausible but fires on the wrong days.