Cron & Scheduling
by @gitgoodordietrying
Schedule and manage recurring tasks with cron and systemd timers. Use when setting up cron jobs, writing systemd timer units, handling timezone-aware scheduling, monitoring failed jobs, implementing retry patterns, or debugging why a scheduled task didn't run.
clawhub install cron-schedulingπ About This Skill
name: cron-scheduling description: Schedule and manage recurring tasks with cron and systemd timers. Use when setting up cron jobs, writing systemd timer units, handling timezone-aware scheduling, monitoring failed jobs, implementing retry patterns, or debugging why a scheduled task didn't run. metadata: {"clawdbot":{"emoji":"β°","requires":{"anyBins":["crontab","systemctl","at"]},"os":["linux","darwin"]}}
Cron & Scheduling
Schedule and manage recurring tasks. Covers cron syntax, crontab management, systemd timers, one-off scheduling, timezone handling, monitoring, and common failure patterns.
When to Use
Cron Syntax
The five fields
ββββββββββ minute (0-59)
β ββββββββ hour (0-23)
β β ββββββ day of month (1-31)
β β β ββββ month (1-12 or JAN-DEC)
β β β β ββ day of week (0-7, 0 and 7 = Sunday, or SUN-SAT)
β β β β β
* * * * * command
Common schedules
# Every minute
* * * * * /path/to/script.shEvery 5 minutes
*/5 * * * * /path/to/script.shEvery hour at :00
0 * * * * /path/to/script.shEvery day at 2:30 AM
30 2 * * * /path/to/script.shEvery Monday at 9:00 AM
0 9 * * 1 /path/to/script.shEvery weekday at 8:00 AM
0 8 * * 1-5 /path/to/script.shFirst day of every month at midnight
0 0 1 * * /path/to/script.shEvery 15 minutes during business hours (Mon-Fri 9-17)
*/15 9-17 * * 1-5 /path/to/script.shTwice a day (9 AM and 5 PM)
0 9,17 * * * /path/to/script.shEvery quarter (Jan, Apr, Jul, Oct) on the 1st at midnight
0 0 1 1,4,7,10 * /path/to/script.shEvery Sunday at 3 AM
0 3 * * 0 /path/to/script.sh
Special strings (shorthand)
@reboot /path/to/script.sh # Run once at startup
@yearly /path/to/script.sh # 0 0 1 1 *
@monthly /path/to/script.sh # 0 0 1 * *
@weekly /path/to/script.sh # 0 0 * * 0
@daily /path/to/script.sh # 0 0 * * *
@hourly /path/to/script.sh # 0 * * * *
Crontab Management
# Edit current user's crontab
crontab -eList current crontab
crontab -lEdit another user's crontab (root)
sudo crontab -u www-data -eRemove all cron jobs (be careful!)
crontab -rInstall crontab from file
crontab mycrontab.txtBackup crontab
crontab -l > crontab-backup-$(date +%Y%m%d).txt
Crontab best practices
# Set PATH explicitly (cron has minimal PATH)
PATH=/usr/local/bin:/usr/bin:/binSet MAILTO for error notifications
MAILTO=admin@example.comSet shell explicitly
SHELL=/bin/bashFull crontab example
PATH=/usr/local/bin:/usr/bin:/bin
MAILTO=admin@example.com
SHELL=/bin/bashBackups
0 2 * * * /opt/scripts/backup.sh >> /var/log/backup.log 2>&1Cleanup old logs
0 3 * * 0 find /var/log/myapp -name "*.log" -mtime +30 -deleteHealth check
*/5 * * * * /opt/scripts/healthcheck.sh || /opt/scripts/alert.sh "Health check failed"
Systemd Timers
Create a timer (modern cron replacement)
# /etc/systemd/system/backup.service
[Unit]
Description=Daily backup[Service]
Type=oneshot
ExecStart=/opt/scripts/backup.sh
User=backup
StandardOutput=journal
StandardError=journal
# /etc/systemd/system/backup.timer
[Unit]
Description=Run backup daily at 2 AM[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
RandomizedDelaySec=300
[Install]
WantedBy=timers.target
# Enable and start the timer
sudo systemctl daemon-reload
sudo systemctl enable --now backup.timerCheck timer status
systemctl list-timers
systemctl list-timers --allCheck last run
systemctl status backup.service
journalctl -u backup.service --since todayRun manually (for testing)
sudo systemctl start backup.serviceDisable timer
sudo systemctl disable --now backup.timer
OnCalendar syntax
# Systemd calendar expressionsDaily at midnight
OnCalendar=daily
or: OnCalendar=*-*-* 00:00:00
Every Monday at 9 AM
OnCalendar=Mon *-*-* 09:00:00Every 15 minutes
OnCalendar=*:0/15Weekdays at 8 AM
OnCalendar=Mon..Fri *-*-* 08:00:00First of every month
OnCalendar=*-*-01 00:00:00Every 6 hours
OnCalendar=0/6:00:00Specific dates
OnCalendar=2026-02-03 12:00:00Test calendar expressions
systemd-analyze calendar "Mon *-*-* 09:00:00"
systemd-analyze calendar "*:0/15"
systemd-analyze calendar --iterations=5 "Mon..Fri *-*-* 08:00:00"
Advantages over cron
Systemd timers vs cron:
+ Logs in journald (journalctl -u service-name)
+ Persistent: catches up on missed runs after reboot
+ RandomizedDelaySec: prevents thundering herd
+ Dependencies: can depend on network, mounts, etc.
+ Resource limits: CPUQuota, MemoryMax, etc.
+ No lost-email problem (MAILTO often misconfigured)
More files to create (service + timer)
More verbose configuration
One-Off Scheduling
at (run once at a specific time)
# Schedule a command
echo "/opt/scripts/deploy.sh" | at 2:00 AM tomorrow
echo "reboot" | at now + 30 minutes
echo "/opt/scripts/report.sh" | at 5:00 PM FridayInteractive (type commands, Ctrl+D to finish)
at 10:00 AM
> /opt/scripts/task.sh
> echo "Done" | mail -s "Task complete" admin@example.com
> List pending jobs
atqView job details
at -c Remove a job
atrm
sleep-based (simplest)
# Run something after a delay
(sleep 3600 && /opt/scripts/task.sh) &With nohup (survives logout)
nohup bash -c "sleep 7200 && /opt/scripts/task.sh" &
Timezone Handling
# Cron runs in the system timezone by default
Check system timezone
timedatectl
date +%ZSet timezone for a specific cron job
Method 1: TZ variable in crontab
TZ=America/New_York
0 9 * * * /opt/scripts/report.shMethod 2: In the script itself
#!/bin/bash
export TZ=UTC
All date operations now use UTC
Method 3: Wrapper
TZ=Europe/London date '+%Y-%m-%d %H:%M:%S'List available timezones
timedatectl list-timezones
timedatectl list-timezones | grep America
DST pitfalls
Problem: A job scheduled for 2:30 AM may run twice or not at all
during DST transitions."Spring forward": 2:30 AM doesn't exist (clock jumps 2:00 β 3:00)
"Fall back": 2:30 AM happens twice
Mitigation:
1. Schedule critical jobs outside 1:00-3:00 AM
2. Use UTC for the schedule: TZ=UTC in crontab
3. Make jobs idempotent (safe to run twice)
4. Systemd timers handle DST correctly
Monitoring and Debugging
Why didn't my cron job run?
# 1. Check cron daemon is running
systemctl status cron # Debian/Ubuntu
systemctl status crond # CentOS/RHEL2. Check cron logs
grep CRON /var/log/syslog # Debian/Ubuntu
grep CRON /var/log/cron # CentOS/RHEL
journalctl -u cron --since today # systemd3. Check crontab actually exists
crontab -l4. Test the command manually (with cron's environment)
env -i HOME=$HOME SHELL=/bin/sh PATH=/usr/bin:/bin /opt/scripts/backup.sh
If it fails here but works normally β PATH or env issue
5. Check permissions
ls -la /opt/scripts/backup.sh # Must be executable
ls -la /var/spool/cron/ # Crontab file permissions6. Check for syntax errors in crontab
cron silently ignores lines with errors
7. Check if output is being discarded
By default, cron emails output. If no MTA, output is lost.
Always redirect: >> /var/log/myjob.log 2>&1
Job wrapper with logging and alerting
#!/bin/bash
cron-wrapper.sh β Run a command with logging, timing, and error alerting
Usage: cron-wrapper.sh [args...]
set -euo pipefail
JOB_NAME="${1:?Usage: cron-wrapper.sh [args...]}"
shift
COMMAND=("$@")
LOG_DIR="/var/log/cron-jobs"
mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/$JOB_NAME.log"
log() { echo "[$(date -u '+%Y-%m-%dT%H:%M:%SZ')] $*" >> "$LOG_FILE"; }
log "START: ${COMMAND[*]}"
START_TIME=$(date +%s)
if "${COMMAND[@]}" >> "$LOG_FILE" 2>&1; then
ELAPSED=$(( $(date +%s) - START_TIME ))
log "SUCCESS (${ELAPSED}s)"
else
EXIT_CODE=$?
ELAPSED=$(( $(date +%s) - START_TIME ))
log "FAILED with exit code $EXIT_CODE (${ELAPSED}s)"
# Alert (customize as needed)
echo "Cron job '$JOB_NAME' failed with exit $EXIT_CODE" | \
mail -s "CRON FAIL: $JOB_NAME" admin@example.com 2>/dev/null || true
exit $EXIT_CODE
fi
# Use in crontab:
0 2 * * * /opt/scripts/cron-wrapper.sh daily-backup /opt/scripts/backup.sh
*/5 * * * * /opt/scripts/cron-wrapper.sh health-check /opt/scripts/healthcheck.sh
Lock to prevent overlap
# Prevent concurrent runs (job takes longer than interval)
Method 1: flock
* * * * * flock -n /tmp/myjob.lock /opt/scripts/slow-job.shMethod 2: In the script
LOCKFILE="/tmp/myjob.lock"
exec 200>"$LOCKFILE"
flock -n 200 || { echo "Already running"; exit 0; }
... do work ...
Idempotent Job Patterns
# Idempotent backup (only creates if newer than last backup)
#!/bin/bash
BACKUP_DIR="/backups/$(date +%Y%m%d)"
[[ -d "$BACKUP_DIR" ]] && { echo "Backup already exists"; exit 0; }
mkdir -p "$BACKUP_DIR"
pg_dump mydb > "$BACKUP_DIR/mydb.sql"Idempotent cleanup (safe to run multiple times)
find /tmp/uploads -mtime +7 -type f -delete 2>/dev/null || trueIdempotent sync (rsync only transfers changes)
rsync -az /data/ backup-server:/backups/data/
Tips
>> /var/log/job.log 2>&1. Without this, output goes to mail (if configured) or is silently lost.env -i to simulate cron's minimal environment. Most failures are caused by missing PATH or environment variables.flock to prevent overlapping runs when a job might take longer than its schedule interval.systemd-analyze calendar is invaluable for verifying timer schedules before deploying.Persistent=true), and resource limits for free.β‘ When to Use
π Tips & Best Practices
>> /var/log/job.log 2>&1. Without this, output goes to mail (if configured) or is silently lost.env -i to simulate cron's minimal environment. Most failures are caused by missing PATH or environment variables.flock to prevent overlapping runs when a job might take longer than its schedule interval.systemd-analyze calendar is invaluable for verifying timer schedules before deploying.Persistent=true), and resource limits for free.