Chapter 15: Scripts and scheduling

systemd timers and one-off jobs

cron is not the only scheduler. On RHEL, systemd can start services on a schedule too, with some advantages: every run is an ordinary service run, logged in the journal, and a run missed while the machine was off can be made up. And for a job that should run once, later, there is at. This lesson shows both, and helps you choose.

Choosing a scheduler

Which scheduler?: Select a tool.

A timer is two units

A job is a service (what to run); the schedule is a timer (when). They share a name. Here is a small pair that writes a message to the journal:

[root@servera ~]# cat /etc/systemd/system/hello.service
[Unit]
Description=Write a hello line to the journal

[Service]
Type=oneshot
ExecStart=/usr/bin/logger -t hello-timer "hello from the timer"
[root@servera ~]# cat /etc/systemd/system/hello.timer
[Unit]
Description=Run hello every minute

[Timer]
OnCalendar=*-*-* *:*:00
Persistent=true

[Install]
WantedBy=timers.target

Type=oneshot says the service runs a command and exits; it has no [Install] section because it is started by the timer. The timer's OnCalendar= gives the schedule, and Persistent=true makes systemd run a job that was missed while the machine was off.

A timer and its service: Step through the pair of units that make a scheduled job.

[root@servera ~]# systemctl daemon-reload
[root@servera ~]# systemctl enable --now hello.timer
Created symlink /etc/systemd/system/timers.target.wants/hello.timer → /etc/systemd/system/hello.timer.
[root@servera ~]# systemctl list-timers hello.timer --no-pager
NEXT                        LEFT     LAST PASSED UNIT        ACTIVATES
Sat 2026-10-03 17:54:00 UTC 26s left -    -      hello.timer hello.service

1 timers listed.
[root@servera ~]# journalctl -t hello-timer --no-pager | tail -1 | cut -c1-110
Oct 03 17:54:03 servera.lab.example.com hello-timer[1327]: hello from the timer

You can test the job without waiting: systemctl start hello.service runs it right now.

Calendar expressions

OnCalendar= takes DayOfWeek Year-Month-Day Hour:Minute:Second, with * for "any" and , .. / for lists, ranges and steps. Shortcuts exist: hourly, daily, weekly, monthly. Always test an expression before you rely on it:

[root@servera ~]# systemd-analyze calendar 'Mon *-*-* 09:30'
  Original form: Mon *-*-* 09:30
Normalized form: Mon *-*-* 09:30:00
    Next elapse: Mon 2026-10-05 09:30:00 UTC
       From now: 1 day 15h left
[root@servera ~]# systemd-analyze calendar daily
  Original form: daily
Normalized form: *-*-* 00:00:00
    Next elapse: Sun 2026-10-04 00:00:00 UTC
       From now: 6h left

The system already uses timers: systemctl list-timers shows the tidying of temporary files, the log rotation (logrotate.timer, chapter 11), and the package cache refresh.

Other triggers exist besides calendars: OnBootSec=5min (5 minutes after boot), OnUnitActiveSec=1h (an hour after the service last ran). And systemd-run --on-active=30s COMMAND creates a throw-away timer for a single run.

at: run it once

at takes a time, then reads commands from standard input. It is in the at package, and needs atd running (dnf install at; systemctl enable --now atd):

[student@servera ~]$ echo "echo at job ran > /home/student/at.out" | at now + 1 minute
warning: commands will be executed using /bin/sh
job 1 at Sat Oct  3 17:53:00 2026
[student@servera ~]$ atq
1	Sat Oct  3 17:53:00 2026 a student
[student@servera ~]$ atrm 1

Times can be now + 5 minutes, 17:30, tomorrow, noon next friday. atq lists your pending jobs, at -c N shows one, and atrm N deletes it. The job runs with the environment captured at the moment you submitted it.

Try it

Check your understanding