Skip to content

Scheduling tasks

Practical 4.8 scheduling-tasks-annex.pdf · PDF

Your phone backs up your photos at night while it charges. It updates its apps at 3 a.m. Nobody presses a button. Who will remember to run backup.sh at 02:00, every night, for years?

Not you. In Shell scripts you wrote a backup that works perfectly, as long as somebody runs it. Today you hand it to an alarm clock that never sleeps: first on Linux with cron and systemd timers, then on Windows with Task Scheduler. And you learn the second half of the job: checking that it really ran.

What you will learn

  • Read and write a crontab line: the five time fields and the command
  • Schedule backup.sh with crontab -e, send its output to a log and prove that it ran
  • Name the three traps of cron: short PATH, no screen, machine switched off
  • Create a systemd timer with a .service and a .timer file and check it with list-timers
  • Create a scheduled task on Windows with Task Scheduler (trigger + action) and with schtasks
  • Why * 20 * * * runs sixty times, not once

Scheduled is not checked: GitLab, 2017

On 31 January 2017, at GitLab.com, an engineer deleted about 300 GB of the live database: he believed he was on the other server. The company went to its backups. The scheduled database dump had been failing every night for a long time, because of a version mismatch. It did send an email on each failure, but the mail system rejected those emails, so nobody ever saw one.

They recovered from a copy that was six hours old, and lost the work in between: about 5,000 projects, 5,000 comments and 700 new user accounts. GitLab published the whole story itself.

They had scheduled the backup. Nobody had looked at whether it worked. On this page you do both: schedule it, and look.

cron: the alarm clock of Unix

cron is a daemon that runs commands at the times you tell it.

A daemon is a program that runs in the background, without a window, waiting for something. This one waits for the clock. Its name comes from the Greek chronos, time. Unix has had cron since Version 7 (1979); the one in Debian descends from the cron that Paul Vixie wrote in 1987.

Each user has a table of alarms, the crontab. One line = one task. Each line has five time fields, then the command:

.---------------- minute        (0 - 59)
|  .------------- hour          (0 - 23)
|  |  .---------- day of month  (1 - 31)
|  |  |  .------- month         (1 - 12)
|  |  |  |  .---- day of week   (0 - 7; 0 and 7 are Sunday)
|  |  |  |  |
30 2  *  *  1-5   command to run

That example runs at 02:30, Monday to Friday. Four symbols cover almost everything:

Symbol Means Example Runs
* Every value 0 8 * * * Every day at 08:00
1-5 A range, from 1 to 5 30 2 * * 1-5 At 02:30, Monday to Friday
1,15 A list: 1 and 15 0 0 1,15 * * At 00:00 on days 1 and 15
*/10 Every 10 */15 * * * * Every 15 minutes

More practice: 0 0 1 * * runs at 00:00 on day 1 of every month; 30 3 * * 0 runs every Sunday at 03:30.

Careful: a star in the minute field

* 20 * * * does not run once at eight in the evening. It runs every minute from 20:00 to 20:59: sixty times. The minute must be a number: 0 20 * * *. And the fifth field is the day of the week, not the year: there is no year.

Three commands manage your table:

Command Does
crontab -e Edits your table (the first time it asks for an editor: choose nano)
crontab -l Lists your table
crontab -r Deletes all of it, without asking

The three traps of cron

  1. Short PATH. cron runs your line with /bin/sh and PATH=/usr/bin:/bin. It will not find your scripts by name. Write full paths.
  2. No screen. There is no terminal: whatever the script prints is lost (cron tries to email it). Send it to a file: >> $HOME/backup.log 2>&1. >> adds at the end of the file; 2>&1 means "the error messages too".
  3. Machine off. If the computer is off at that minute, cron does not run the task later.

backup.sh in cron

First check that the script works by hand (exit code 0):

~/scripts/backup.sh ~/tramuntana/dev ~/backups; echo $?

Then crontab -e and add this line at the end, all on one line. For the test it runs every two minutes:

*/2 * * * * $HOME/scripts/backup.sh $HOME/tramuntana/dev $HOME/backups >> $HOME/backup.log 2>&1

Wait about four minutes, then look:

cat ~/backup.log
ls ~/backups
2026-10-05 20:58:01 OK /home/student/backups/dev-20261005-205801.tar.gz
2026-10-05 21:00:01 OK /home/student/backups/dev-20261005-210001.tar.gz

One OK line per run, two minutes apart. Now crontab -e again and change the five fields to the real schedule, every day at 20:00 (0 20 * * *). Check with crontab -l.

When nothing appears in the log

  • The script is not executable, or the path is wrong. Run the exact command from crontab -l by hand.
  • "-":0: bad minute and errors in crontab file, can't install: a field has an impossible value (61 minutes) or there are fewer than five fields. Nothing was saved.
  • A % in the line: in a crontab, % means "new line", so a date +%F written directly in the crontab breaks it. Inside the script it is fine.
  • sudo journalctl -u cron -n 5 shows lines like CRON[386]: (student) CMD (...): proof that cron started it.

systemd timers

When Linux boots, the kernel starts one first process. On Debian that is systemd, released in 2010 by Lennart Poettering and Kay Sievers, and the default in Debian since Debian 8 (2015). It starts and watches everything else, and it has its own alarm clock: timers.

A timer needs two small text files, each one called a unit, in ~/.config/systemd/user/:

File Says Key lines
backup.service What to do Type=oneshot (run once and finish); ExecStart= the command, with full paths (%h = your home folder)
backup.timer When to do it OnCalendar= the schedule; Persistent=true; WantedBy=timers.target
flowchart LR
    T["<b>backup.timer</b><br>the alarm clock:<br>OnCalendar=..."] -->|"rings and starts"| S["<b>backup.service</b><br>the task:<br>ExecStart=backup.sh"]
    S -->|"everything it prints"| J["<b>journal</b><br>journalctl --user<br>-u backup.service"]

Create the folder and the two files:

mkdir -p ~/.config/systemd/user
nano ~/.config/systemd/user/backup.service
[Unit]
Description=Back up the dev folder

[Service]
Type=oneshot
ExecStart=%h/scripts/backup.sh %h/tramuntana/dev %h/backups
nano ~/.config/systemd/user/backup.timer
[Unit]
Description=Run backup.service on a schedule

[Timer]
OnCalendar=*:0/2
Persistent=true

[Install]
WantedBy=timers.target

OnCalendar=*:0/2 means every two minutes, for the test. Now tell systemd to read the files, test the what without waiting, and switch on the when:

systemctl --user daemon-reload
systemctl --user start backup.service
journalctl --user -u backup.service -n 3
systemctl --user enable --now backup.timer
systemctl --user list-timers
Oct 05 20:52:11 deb-lab systemd[310]: Starting backup.service - Back up the dev folder...
Oct 05 20:52:11 deb-lab backup.sh[361]: 2026-10-05 20:52:11 OK /home/student/backups/dev-20261005-205211.tar.gz
Oct 05 20:52:11 deb-lab systemd[310]: Finished backup.service - Back up the dev folder.

NEXT                            LEFT LAST PASSED UNIT         ACTIVATES
Mon 2026-10-05 20:54:00 UTC 1min 48s -         - backup.timer backup.service

The OK line of backup.sh is in the journal: no log file needed. Two minutes later, the LAST column of list-timers shows a time instead of -.

For the real schedule, change OnCalendar to *-*-* 20:00:00 ("any year, any month, any day, at 20:00:00"), then systemctl --user daemon-reload and systemctl --user restart backup.timer. Mon..Fri 14:30 also works, and systemd can check a schedule for you before you trust it:

$ systemd-analyze calendar "Mon..Fri 14:30"
Normalized form: Mon..Fri *-*-* 14:30:00

Timers answer two of cron's traps. Persistent=true: if the machine was off at the time, the task runs when the timer is next started. And everything the script prints goes to the journal.

Common mistakes in unit files

Section names need their capital letter ([Timer], [Service], [Install]). After editing a file, run daemon-reload. And ExecStart=~/scripts/... fails: systemd does not understand ~. Use %h or the full path.

A user timer needs a logged-in user

Timers in your home folder live while you have a session. Servers use system timers in /etc/systemd/system (with sudo), or loginctl enable-linger. Services come back later in the course.

Now cron and the timer both run at 20:00. Keep one and delete the other (crontab -e, or systemctl --user disable --now backup.timer). On a laptop, the timer is the better choice, because of Persistent=true.

Windows Task Scheduler

Same idea, other words. On Windows a task has a trigger (when) and an action (what).

  • Graphical: Win+R, type taskschd.msc. Action → Create Basic Task... opens a wizard: a name, the trigger (Daily, Weekly, When I log on...), the action (Start a program), Finish. The task appears in Task Scheduler Library.
  • Command: schtasks /create with /tn (task name), /tr (task to run), /sc (schedule: minute, hourly, daily, weekly...) and /st (start time, 24-hour HH:mm).

What to run? robocopy ("robust copy") comes with Windows: robocopy SOURCE DEST /E copies a folder with all its subfolders. You put that line in a batch file (backup.bat), the Windows cousin of a shell script. In a Command Prompt of win11:

mkdir C:\tramuntana\dev C:\backup
echo print(42) > C:\tramuntana\dev\app.py
echo robocopy C:\tramuntana C:\backup\tramuntana /E /LOG+:C:\backup\backup.log > C:\backup\backup.bat
schtasks /create /tn "Tramuntana backup" /tr C:\backup\backup.bat /sc daily /st 20:00
schtasks /run /tn "Tramuntana backup"
dir C:\backup\tramuntana\dev
SUCCESS: The scheduled task "Tramuntana backup" has successfully been created.
SUCCESS: Attempted to run the scheduled task "Tramuntana backup".

dir shows app.py in the copy. A task name with spaces needs quotes, and you can check the batch file with type C:\backup\backup.bat.

Careful: 0x1 is not a failure here

In Task Scheduler, the task shows Last Run Result 0x1 after the first copy. Did it fail, because it is not 0? Not here. robocopy has its own exit codes: 1 means "all files were copied", 0 means "nothing needed copying", and 8 or more means a failure. Bash's rule "0 or failure" is a convention, and this program does not follow it. Always read the manual of the tool.

Three tools compared

cron systemd timer Task Scheduler
Where the schedule is One line in the crontab .timer + .service files A task: trigger + action
The machine was off The run is lost Caught up with Persistent=true Caught up if "run as soon as possible after a missed start" is on
Where the output goes Lost, unless you redirect it The journal The program's own log; Last Run Result

Every step, with space for your evidence and this table to fill, is on the Practical 4.8 sheet.

Key takeaways

  • A crontab line has five time fields (minute, hour, day of month, month, day of week) and a command; * means every value.
  • cron's three traps: a short PATH (use full paths), no screen (redirect to a log with >> file 2>&1), and a machine that was off (the run is lost).
  • A systemd timer is two units: a .service (what) and a .timer (when). Persistent=true catches up missed runs; the output goes to the journal.
  • On Windows, Task Scheduler tasks have a trigger and an action; schtasks /create makes them from the command line.
  • Exit codes are a convention: robocopy returns 1 for success.
  • Scheduled is not checked: read the log. GitLab's backups failed for months because nobody did.

Quick check

1. When does the crontab line 0 7 * * 1 run?
  • Every Monday at 07:00
  • Every day at 07:01
  • On day 1 of every month at 07:00
  • Every 7 minutes on Mondays

The fields are minute (0), hour (7), day of month (any), month (any) and day of week (1 = Monday). Day 1 of the month would be the third field.

2. Which crontab line runs every day at 20:00?
  • 0 20 * * *
  • * 20 * * *
  • 20 0 * * *
  • 0 0 20 * *

Minute first, then hour: 0 20. A * in the minute field runs every minute of that hour, sixty times. 20 0 is 00:20, and a 20 in the third field means day 20 of the month.

3. A script works when you run it by hand but does nothing from cron. What is the most likely cause?
  • It calls files or commands without the full path
  • cron only runs scripts that are owned by root
  • The script has too many lines for cron
  • cron needs the script name to end in .cron

cron runs with a very short PATH and no terminal. Full paths, and a log file to read the output, solve most of these mysteries.

4. The laptop was off at 20:00. Which tool can run the missed backup later?
  • A systemd timer with Persistent=true
  • cron, which always catches up at boot
  • The command crontab -l run at login
  • None of them can run a missed task

Persistent=true makes the timer remember a missed run and do it when the timer next starts. Plain cron simply skips it. Task Scheduler can also catch up, if that option is switched on.

5. What are the two files of a systemd timer for?
  • The .service says what to run and the .timer says when
  • The .timer says what to run and the .service says when
  • The .service stores the log and the .timer stores the script
  • The .service is for root and the .timer is for normal users

Both are units. backup.service holds ExecStart= (the command); backup.timer holds OnCalendar= (the schedule) and starts the service when it rings.

6. In Windows Task Scheduler, what is the trigger of a task?
  • The condition that starts the task, such as a time
  • The program that the task starts when it runs
  • The user account that owns the task
  • The log file where the task writes its result

A task is a trigger (when: daily at 20:00, at log on...) plus an action (what: start backup.bat). In schtasks, /sc and /st describe the trigger and /tr the action.

7. A scheduled robocopy task shows Last Run Result 0x1. Select the two that are true.
  • For robocopy, 1 means the files were copied
  • Each tool can define its own exit codes
  • Any result that is not 0 means the task failed
  • The task did not start because of the trigger

By convention 0 is success, but robocopy uses 1 for "all files were copied" and 8 or more for failure. Always read the manual of the tool before deciding a task failed.

Disks, file systems, volumes, backups, and the scripts and schedules that keep them safe without you: that is the whole of Unit 4. Before the exam, one last look at why a mirror never replaces a backup, with real stories: RAID is not a backup.