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

In the timer

Mon..Fri 09:00:00

Parser

Fires at the same instants

Field by field

Field As written In the timer Note
Minute 0 00 Carried over unchanged inside the time field.
Hour 9 09 Carried over unchanged; systemd writes it before the colon, the crontab after.
Day of month * — Any day: the timer omits the date part entirely.
Month * — Any month.
Day of week 1-5 Mon..Fri Written as names, before the date; a Sunday-first cron range becomes a list.
  • The line is carried over field by field — nothing was approximated.
  • Both schedulers read the machine’s local time, so the instants do not move. systemd 257 also accepts a zone inline (OnCalendar=… Europe/Kyiv).
  • A timer may fire up to a minute late by design: AccuracySec defaults to 1min so wake-ups can be batched. Set AccuracySec=1us in the [Timer] section if the exact second matters.
  • A crontab line missed while the machine was off is skipped. Persistent=true in [Timer] is the systemd answer: the run is caught up at the next boot.
  • Two files, one basename: job.timer schedules, job.service runs. Enable with systemctl enable --now job.timer — enabling the .service does nothing.

The unit pair

job.timer

[Unit]
Description=job

[Timer]
OnCalendar=Mon..Fri 09:00:00
Persistent=true

[Install]
WantedBy=timers.target

job.service

[Unit]
Description=job

[Service]
Type=oneshot
ExecStart=/usr/local/bin/job

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-timers then 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 an OnCalendar value, 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.