Make your scripts run themselves: nightly backups and free site-down alerts
A script you have to remember to run is a script you'll forget. By the end, your files back themselves up every night, GitHub checks your website every morning while your PC is off, and your phone buzzes the moment either one fails. Task Scheduler, cron, launchd and GitHub Actions, each with a clean off switch.
- A daily check that runs on GitHub's servers while your PC is off, and emails you when something is down
- Phone alerts through ntfy when anything fails, and the exact commands to pause or remove every schedule
- Done first: the Terminal survival kit. You'll copy commands and move between folders
- uv, the Python tool used across this site. It brings its own Python, so there's nothing else to install. Check with
uv --version; step 1 shows how to get it if that fails - Windows 10 or 11, macOS, or Linux. Steps 2 and 3 are Windows, step 4 is Mac and Linux, step 5 is any of them
- For step 5: a free GitHub ↗ account. Scheduled runs are free in public repositories, and a free account includes 2,000 minutes a month for private ones. If you never add a payment method, GitHub blocks further runs when the minutes run out instead of charging you (GitHub Actions billing ↗)
- Optional: the free ntfy app on your phone for failure alerts. No account needed. The paid ntfy plans (from $5 a month) are not needed here
- Optional but sensible: a USB drive or second disk for the backups. A backup on the same disk as the original doesn't survive that disk dying
Get the kit and run the backup by hand
A scheduler only presses "run" for you. If the command doesn't work when you type it, it won't work at 8pm either, and you won't be there to see the error.
First check uv is there:
Terminaluv --versionIf you get "not recognised" or "command not found", install it with the official one-liner from the uv docs ↗, then close the terminal and open a new one:
Windowspowershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"Mac / Linuxcurl -LsSf https://astral.sh/uv/install.sh | shDownload the starter kit (11KB) and unzip it into your home folder. You get a
schedule-anythingfolder with the backup script, the Windows, cron and launchd set-ups, and agithub-actionsfolder for step 5. You can also read each file on its own first: daily_backup.py, register-windows-task.ps1, crontab-example.txt, the launchd file, site_check.py and daily-check.yml.Windowscd $HOME\Downloads Expand-Archive schedule-anything-kit.zip -DestinationPath $HOME cd $HOME\schedule-anythingMac / Linuxcd ~/Downloads unzip schedule-anything-kit.zip -d ~ cd ~/schedule-anythingMake a practice folder to back up, with one file in it. Use a practice folder until the whole lesson works, then point the schedule at your real one.
Windowsmkdir $HOME\Important "Shopping list" | Out-File $HOME\Important\list.txtMac / Linuxmkdir -p ~/Important echo "Shopping list" > ~/Important/list.txtNow a dry run.
--dry-runshows what the script would do and changes nothing. The first time, uv may take a few seconds to find or download a Python.Windowsuv run daily_backup.py "$HOME\Important" "$HOME\Backups\Important" --dry-run uv run daily_backup.py "$HOME\Important" "$HOME\Backups\Important" $LASTEXITCODEMac / Linuxuv run daily_backup.py ~/Important ~/Backups/Important --dry-run uv run daily_backup.py ~/Important ~/Backups/Important echo $?On Windows it looks like this:
Output2026-10-12 01:33:59 INFO Backing up C:\Users\user\Important: 1 files, 0.0 MB 2026-10-12 01:33:59 INFO Dry run: would write a new zip to C:\Users\user\Backups\Important 2026-10-12 01:34:00 INFO Backing up C:\Users\user\Important: 1 files, 0.0 MB 2026-10-12 01:34:00 INFO Saved backup-2026-10-12_013400.zip (1 files, 0.0 MB) 0That last
0is the exit code, and it's the most important number in this mission. Every program hands one back when it finishes: 0 means it worked, anything else means it didn't. Task Scheduler, cron, launchd and GitHub Actions all read it. The kit's script returns 1 if the backup fails and 2 if the source folder is missing (a USB drive that isn't plugged in, say).
Exit code 0 when the backup works, 2 when the folder isn't there. That one number is what every scheduler checks. What the script does, and why each part matters when nobody is watching:
- It writes a log. Every run adds lines to
backup-log.txtin the backup folder. A scheduled job has no window, so the log is the only place an error can go. - It uses full paths. A scheduler doesn't start in your folder, so
Importanton its own would mean nothing. Give it the whole path every time. - It writes the zip under a temporary name, then renames it. If the PC dies halfway, you're left with an obvious
.partialfile, never a broken zip that looks finished. - It keeps the newest 7 zips (change it with
--keep) and only deletes files named exactly like its own,backup-YYYY-MM-DD_HHMMSS.zip. Anything else in that folder is never touched. - It only uses Python's standard library, so there's nothing to install and nothing to break when a package updates.
TipThis is a simple zip copy, good for a folder of documents. For whole drives, versions going back months, encryption and protection from ransomware, use restic from Backups that survive ransomware. The scheduling in this mission works exactly the same for it.CarefulIf the folder holds other people's personal data (clients' details, invoices with names on), the backup is personal data too under UK GDPR. Keep it as safe as the original: an encrypted drive, not a spare USB stick in a drawer.- It writes a log. Every run adds lines to
backup-...zip file is in the Backups\Important folder, backup-log.txt ends with a Saved line, and the exit code is 0.If it breaks"uv" is not recognised, or "command not found: uv" + 3 more
"uv" is not recognised, or "command not found: uv"
"No such file or directory: daily_backup.py"
schedule-anything-kit\schedule-anything. Run ls and cd into the folder that contains daily_backup.py.