Cron & Task Scheduler: Schedule Scripts on Linux & Windows

You’ve written the script. It works. Now what?

Running scripts manually defeats the purpose of automation. The real power comes when your scripts run themselves – at 3 AM when you’re asleep, every hour for monitoring, or whenever a specific event occurs.

Linux uses cron. Windows uses Task Scheduler. Both accomplish the same goal: executing code on a schedule without human intervention.

Automation starts with scheduling. I use cron jobs across my entire infrastructure — backup scripts, certificate renewals, monitoring checks, DuckDNS updates. If you’re doing something manually on a regular basis, it should probably be a cron job.

Career Impact

Scheduled automation is what separates reactive admins from proactive engineers.

When you can set up reliable, self-running maintenance, monitoring, and reporting, you’re demonstrating the operational maturity that hiring managers look for in Senior Sysadmin, DevOps, and SRE roles paying £55k-80k+.

Split screen showing cron configuration in terminal and Windows Task Scheduler GUI

What You’ll Learn

  • Cron syntax and scheduling on Linux
  • Task Scheduler configuration on Windows
  • Common scheduling patterns
  • Error handling and notifications
  • Best practices for scheduled tasks

Quick Reference

Cron Time Fields

* * * * * command
| | | | |
| | | | +-- Day of week (0-7, Sun=0 or 7)
| | | +---- Month (1-12)
| | +------ Day of month (1-31)
| +-------- Hour (0-23)
+---------- Minute (0-59)

Common Cron Schedules

Schedule Cron Expression Description
Every minute * * * * * Testing only
Every hour 0 * * * * On the hour
Daily at midnight 0 0 * * * Backups, reports
Daily at 3 AM 0 3 * * * Maintenance
Every Monday 0 0 * * 1 Weekly tasks
First of month 0 0 1 * * Monthly tasks
Every 15 minutes */15 * * * * Frequent checks

Cron on Linux

Understanding Crontab

Cron reads schedules from crontab files. Each user can have their own crontab, plus system-wide crontabs.

User crontab commands:

# Edit your crontab
crontab -e

# List your crontab
crontab -l

# Remove your crontab (dangerous)
crontab -r

System crontabs:

  • /etc/crontab – System crontab (includes user field)
  • /etc/cron.d/ – Drop-in directory for packages
  • /etc/cron.daily/ – Scripts run daily
  • /etc/cron.hourly/ – Scripts run hourly
  • /etc/cron.weekly/ – Scripts run weekly
  • /etc/cron.monthly/ – Scripts run monthly

Your First Cron Job

crontab -e

Add this line:

# Run backup script every day at 2 AM
0 2 * * * /home/user/scripts/backup.sh >> /var/log/backup.log 2>&1

Breaking it down:

  • 0 2 * * * – At 2:00 AM every day
  • /home/user/scripts/backup.sh – Full path to script
  • >> /var/log/backup.log – Append stdout to log
  • 2>&1 – Also capture stderr

Cron Special Strings

Instead of five fields, use shortcuts:

@reboot     # Run once at startup
@yearly     # Same as 0 0 1 1 *
@monthly    # Same as 0 0 1 * *
@weekly     # Same as 0 0 * * 0
@daily      # Same as 0 0 * * *
@hourly     # Same as 0 * * * *

Example:

@daily /home/user/scripts/cleanup.sh
@reboot /home/user/scripts/startup-tasks.sh

Environment Considerations

Cron runs with a minimal environment. Common issues:

Problem: Script works manually but not in cron.

Solution: Use full paths everywhere:

Practitioner tip: The most common cron mistake: your script works perfectly when you run it manually but fails silently in cron. That’s usually because cron runs with a minimal PATH. Always use absolute paths to commands in your cron scripts, or set PATH explicitly at the top.

# Bad (might not find commands)
0 3 * * * mysqldump mydb > backup.sql

# Good (explicit paths)
0 3 * * * /usr/bin/mysqldump mydb > /home/user/backups/backup.sql

Or set PATH in crontab:

PATH=/usr/local/bin:/usr/bin:/bin
0 3 * * * backup-script.sh

Cron Logging

View cron execution logs:

# Debian/Ubuntu
grep CRON /var/log/syslog

# RHEL/CentOS
grep CRON /var/log/cron

# Recent entries
journalctl -u cron --since "1 hour ago"

Practical Cron Examples

# Log rotation - daily at 1 AM
0 1 * * * /usr/sbin/logrotate /etc/logrotate.conf

# Database backup - Full backup Sunday at 2 AM, incremental other days
0 2 * * 0 /opt/scripts/full-backup.sh
0 2 * * 1-6 /opt/scripts/incremental-backup.sh

# Disk space monitoring - Check every 30 minutes
*/30 * * * * /opt/scripts/check-disk.sh

# Certificate renewal - Try renewal twice daily (certbot handles skip if not needed)
0 0,12 * * * /usr/bin/certbot renew --quiet

Task Scheduler on Windows

Accessing Task Scheduler

Server Manager > Tools > Task Scheduler

Or run: taskschd.msc

Creating a Basic Task (GUI Method)

  1. Right-click “Task Scheduler Library” > “Create Basic Task”
  2. Name and description
  3. Choose trigger (Daily, Weekly, etc.)
  4. Set time
  5. Choose action (Start a program)
  6. Browse to script/executable
  7. Finish

Creating an Advanced Task

For more control, use “Create Task” instead of “Create Basic Task”:

General Tab:

  • Name and description
  • Security options (run as different user)
  • “Run whether user is logged on or not”
  • “Run with highest privileges” (for admin tasks)

Triggers Tab:

  • Schedule (daily, weekly, etc.)
  • On startup, on logon, on event
  • Repeat task every X minutes/hours

Actions Tab:

  • Start a program
  • For PowerShell scripts:
    • Program: powershell.exe
    • Arguments: -ExecutionPolicy Bypass -File "C:\Scripts\script.ps1"

Conditions Tab:

  • Start only if on AC power
  • Wake computer to run task
  • Network conditions

Settings Tab:

  • Allow task to run on demand
  • Stop task if runs longer than X
  • If task fails, restart every X minutes

PowerShell Task Management

Create tasks programmatically:

# Create a scheduled task
$action = New-ScheduledTaskAction -Execute 'powershell.exe' `
    -Argument '-ExecutionPolicy Bypass -File "C:\Scripts\backup.ps1"'

$trigger = New-ScheduledTaskTrigger -Daily -At '3:00 AM'

$settings = New-ScheduledTaskSettingsSet -StartWhenAvailable

Register-ScheduledTask -TaskName "Daily Backup" `
    -Action $action `
    -Trigger $trigger `
    -Settings $settings `
    -User "SYSTEM" `
    -RunLevel Highest

Multiple triggers:

$trigger1 = New-ScheduledTaskTrigger -At '6:00 AM' -Daily
$trigger2 = New-ScheduledTaskTrigger -At '6:00 PM' -Daily

Register-ScheduledTask -TaskName "Twice Daily Check" `
    -Action $action `
    -Trigger $trigger1, $trigger2

View and manage tasks:

# List all tasks
Get-ScheduledTask

# Get specific task
Get-ScheduledTask -TaskName "Daily Backup"

# Run task now
Start-ScheduledTask -TaskName "Daily Backup"

# Disable task
Disable-ScheduledTask -TaskName "Daily Backup"

# Remove task
Unregister-ScheduledTask -TaskName "Daily Backup" -Confirm:$false

Task Scheduler Triggers

Trigger Type Use Case
On a schedule Regular maintenance
At startup Services, monitoring
At logon User-specific tasks
On idle Low-priority background tasks
On an event React to log entries
On workstation lock/unlock Security auditing
On connect/disconnect Session management

Side-by-Side Comparison

Daily Script at 3 AM

Cron

0 3 * * * /opt/scripts/daily-task.sh >> /var/log/daily-task.log 2>&1

Task Scheduler (PowerShell)

$action = New-ScheduledTaskAction -Execute 'powershell.exe' `
    -Argument '-File "C:\Scripts\daily-task.ps1"'
$trigger = New-ScheduledTaskTrigger -Daily -At '3:00 AM'
Register-ScheduledTask -TaskName "Daily Task" -Action $action -Trigger $trigger

Every 15 Minutes

Cron

*/15 * * * * /opt/scripts/check-status.sh

Task Scheduler (PowerShell)

$trigger = New-ScheduledTaskTrigger -Once -At (Get-Date) `
    -RepetitionInterval (New-TimeSpan -Minutes 15) `
    -RepetitionDuration (New-TimeSpan -Days 9999)

On System Startup

Cron

@reboot /opt/scripts/startup.sh

Task Scheduler (PowerShell)

$trigger = New-ScheduledTaskTrigger -AtStartup

Error Handling and Notifications

Cron Error Notifications

Email on output: By default, cron emails any output to the user. Ensure mail is configured:

# Set email recipient
[email protected]

# Suppress normal output, only email errors
0 3 * * * /opt/scripts/backup.sh > /dev/null

Slack/webhook notification:

#!/bin/bash
# backup.sh with notification

if /opt/scripts/do-backup.sh; then
    curl -X POST -H 'Content-type: application/json' \
        --data '{"text":"Backup completed successfully"}' \
        https://hooks.slack.com/services/XXX/YYY/ZZZ
else
    curl -X POST -H 'Content-type: application/json' \
        --data '{"text":"BACKUP FAILED - check logs"}' \
        https://hooks.slack.com/services/XXX/YYY/ZZZ
    exit 1
fi

Task Scheduler Error Notifications

Email on completion/failure:

# In your script, send email at the end
$result = & C:\Scripts\actual-task.ps1

if ($LASTEXITCODE -eq 0) {
    Send-MailMessage -To "[email protected]" -Subject "Task completed" `
        -Body "Success" -SmtpServer "smtp.company.com"
} else {
    Send-MailMessage -To "[email protected]" -Subject "TASK FAILED" `
        -Body "Check logs" -SmtpServer "smtp.company.com"
}

Task history: View in Task Scheduler GUI or:

Get-WinEvent -LogName "Microsoft-Windows-TaskScheduler/Operational" |
    Where-Object { $_.Message -like "*Daily Backup*" } |
    Select-Object TimeCreated, Message -First 10

Best Practices

General

  1. Use full paths – Never rely on PATH in scheduled tasks
  2. Log everything – You’ll need to troubleshoot eventually
  3. Handle errors – Exit codes, notifications
  4. Don’t overlap – Use locks if tasks might run long
  5. Document – Comments explaining why, not what
  6. Test manually first – Ensure script works before scheduling

Cron Specific

  1. Redirect output>> log 2>&1 to capture both stdout and stderr
  2. Set MAILTO – Get notified of issues
  3. Use absolute times – Avoid @reboot for production services (use systemd)
  4. Consider timezone – Cron uses system timezone
  5. Avoid minute 0 – Spread tasks to avoid load spikes

Task Scheduler Specific

  1. Run as SYSTEM – For tasks not requiring user profile
  2. “Run whether logged on or not” – For server tasks
  3. Store credentials – Be aware of password expiration
  4. Use highest privileges – Only when necessary
  5. Set task timeouts – Prevent hung tasks from blocking

Avoiding Common Pitfalls

Cron Pitfalls

Task doesn’t run:

Task runs but fails:

Overlapping tasks:

# Use flock to prevent overlap
* * * * * /usr/bin/flock -n /tmp/script.lock /opt/scripts/script.sh

Task Scheduler Pitfalls

Task doesn’t run:

“Operation could not be completed”:

Task runs but script fails:

Monitoring Scheduled Tasks

Linux: Monitor Cron

#!/bin/bash
# Alert if cron job hasn't run in expected time

expected_file="/var/backup/last-backup-timestamp"
max_age_hours=26

if [ -f "$expected_file" ]; then
    file_age=$(( ($(date +%s) - $(stat -c %Y "$expected_file")) / 3600 ))
    if [ $file_age -gt $max_age_hours ]; then
        echo "WARNING: Backup hasn't run in ${file_age} hours"
    fi
else
    echo "ERROR: Backup timestamp file not found"
fi

Windows: Monitor Tasks

# Get tasks that failed recently
Get-ScheduledTask | ForEach-Object {
    $info = Get-ScheduledTaskInfo -TaskName $_.TaskName
    if ($info.LastTaskResult -ne 0) {
        [PSCustomObject]@{
            Task = $_.TaskName
            LastRun = $info.LastRunTime
            Result = $info.LastTaskResult
        }
    }
}

Interview Questions

Q1: “Explain cron syntax. When would ‘0 */4 * * *’ run?”

Good Answer: “Cron syntax is five fields: minute, hour, day of month, month, day of week. 0 */4 * * * means at minute 0, every 4 hours, every day. So that’s midnight, 4 AM, 8 AM, noon, 4 PM, and 8 PM – six times per day.”

Q2: “A scheduled task works when you run it manually but fails when scheduled. What do you check?”

Good Answer: “First, I’d check if it’s an environment issue – scheduled tasks run with minimal environments. I’d verify all paths are absolute, not relative. Then I’d check the user context – is the task running as the right user with appropriate permissions? For Windows, I’d check ‘Run whether user is logged on or not’ and execution policy. For cron, I’d check if the script assumes a login shell with certain variables set.”

Q3: “How do you prevent a scheduled task from overlapping if the previous run is still going?”

Good Answer: “On Linux, I’d use flock to obtain a lock file before running – if the lock is held, the new instance exits. For Windows, you can check ‘Do not start a new instance’ in settings, or implement locking in the script itself using a flag file or mutex. Either way, you want a mechanism that prevents the second instance from starting if the first is still running.”

Career Application

On Your Resume

Demonstrate

Next Steps

The best automation runs without you. Set it, monitor it, forget about it – until the alert tells you something needs attention.

Automation for Sysadmins
Part 2 of 4
The RTM Essential Stack - Gear I Actually Use

Enjoyed this guide?

New articles on Linux, homelab, cloud, and automation every 2 days. No spam, unsubscribe anytime.

Scroll to Top