Migration

Migrating Task Scheduler to cron.

Windows Task Scheduler and Linux cron solve the same problem with different design choices. Task Scheduler has richer triggers (on logon, on event, on idle) while cron is simpler and more transparent. This guide maps each piece of Task Scheduler functionality to its cron (or systemd-timer) equivalent.

How they differ at a glance

Task Scheduler (Windows)Cron (Linux/Unix)
ConfigurationXML files + GUIPlain text crontab files
TriggersSchedule, event, logon, idle, startup, connection…Only schedule (and @reboot)
ConditionsOn AC power, network available, idle time, …None built-in
Multiple actions per taskYesOne line = one action (chain in script)
Run as another userStored credentialsOwner of the crontab
Run with highest privilegesCheckboxRun as root
Missed runs"Run task as soon as possible" optionCron doesn't; anacron or systemd timers do
History/logsEvent Viewersyslog, journalctl, or your own log file

Trigger mapping

"On a schedule" → cron expression

Task SchedulerCron equivalent
Daily at 9:00 AM0 9 * * *
Weekly Mon at 9:00 AM0 9 * * 1
Weekly Mon, Wed, Fri0 9 * * 1,3,5
Monthly day 10 0 1 * *
Daily, repeat every 5 minutes for 1 hour*/5 9-9 * * * (runs minute 0,5,10…55 of hour 9)
Every 15 minutes*/15 * * * *

"At log on" → not cron's domain

Cron has no concept of user login. Use:

  • ~/.bashrc or ~/.profile for per-user-shell-start actions
  • systemd user services with --user and a custom target
  • /etc/profile.d/*.sh for system-wide login actions

"At startup" → @reboot

@reboot /usr/local/bin/my-startup-script.sh

Or for system-level startup, prefer a proper systemd unit file. @reboot works but doesn't give you start-order dependencies, retries, or restart-on-failure.

"On an event" → no direct mapping

Cron is time-based only. For event-based triggering, look at:

  • inotifywait for filesystem events
  • systemd path units (.path files) for file watches
  • systemd socket activation for network events
  • Message queues (RabbitMQ, Redis pub/sub) for application events

"On idle" → custom logic

Cron doesn't watch system load. If you need "run when idle," check load average inside your script:

0 2 * * * [ $(cat /proc/loadavg | cut -d. -f1) -lt 2 ] && /path/to/script.sh

Action mapping

Task Scheduler allows multiple actions per task. Cron is one line, one command. To chain:

0 2 * * * /path/to/step1.sh && /path/to/step2.sh && /path/to/step3.sh

Or — much better — wrap in a single script:

0 2 * * * /path/to/nightly-pipeline.sh

The script then handles step ordering, error checking, and logging in one place.

Send email action

Task Scheduler's deprecated "send email" maps to mail, sendmail, or msmtp from a script. For modern workflows, send via a webhook to Slack/Discord/email-service instead.

Conditions and settings

"Start only if computer is on AC power"

On laptops, check the battery state inside the script:

if grep -q 'Discharging' /sys/class/power_supply/BAT*/status 2>/dev/null; then
  echo "On battery — skipping" >&2
  exit 0
fi
# … real work …

"Start only if any network connection is available"

if ! ping -c 1 -W 5 8.8.8.8 > /dev/null 2>&1; then
  echo "No network — skipping" >&2
  exit 0
fi
# … real work …

"Stop the task if it runs longer than X"

Use timeout from coreutils:

0 2 * * * timeout 30m /path/to/script.sh

timeout 30m kills the job if it doesn't finish in 30 minutes.

"If the task fails, restart every N minutes up to M times"

Wrap with a retry loop. Or migrate to systemd, which has OnFailure= hooks built in.

Migration recipe

Take an existing scheduled task (e.g., "Run backup.bat daily at 2:00 AM, only on AC power, retry once if it fails"):

Step 1: Identify the trigger → daily at 2 AM → 0 2 * * *

Step 2: Rewrite the action — backup.bat becomes backup.sh (you'll need to port the logic). If it's a complex script, consider what shell features you need: variables, conditionals, control flow are all available in bash.

Step 3: Add the conditions inside the script (AC power, network, etc.) since cron has no built-in conditions.

Step 4: Add retry logic if needed:

#!/bin/bash
for attempt in 1 2; do
  if /usr/local/bin/run-backup; then
    exit 0
  fi
  echo "Attempt $attempt failed, retrying..." >&2
  sleep 60
done
exit 1

Step 5: Install the crontab entry:

crontab -e
# Add the line:
0 2 * * * /path/to/wrapped-backup.sh >> /var/log/backup.log 2>&1

Step 6: Add monitoring (see our monitoring guide) so you actually know whether it's running.

What cron can't do (and what to use instead)

Task Scheduler featureCron equivalent
Run on filesystem changeinotifywait in a wrapper, or systemd path units
Run on log onShell rc files or systemd user services
Wake the computer to runRTC wake (rtcwake) called from another job
Run as another user with stored passwordsudo -u other-user with a sudoers rule, or rewrite as a systemd service running as that user
Multiple triggers per taskMultiple crontab lines pointing to the same script
"Run task as soon as possible after a missed schedule"anacron or systemd timer with Persistent=true

For anything that needs the richer trigger model of Task Scheduler, the modern answer is systemd timers + units — they map almost 1:1 to Task Scheduler features and beat cron in every category except universal-availability. See our cron vs systemd guide.

Related

Continue reading.