One Python Daemon to Schedule All Your Scripts
The problem with one Task Scheduler entry per script
If you've automated more than three or four things on a Windows box — a daily report, a weekly backup, an hourly sync — you've probably felt Task Scheduler's ceiling. Each script needs its own task, its own trigger, its own "run whether user is logged in or not" checkbox to remember, and its own place to check when something silently stopped firing. There's no shared retry logic, no shared logging, and no single place to see "what's supposed to run today."
python-scripts-scheduler
is a small alternative: one background daemon, MasterRunner.py, that reads
a JSON config and runs everything else on its own schedule. You create
exactly one Task Scheduler entry — for the daemon itself — and manage every
other script's timing in a config file instead of the Windows GUI.
How it works
The daemon polls a config file every two minutes. For each script it checks
whether it's "due," and if so, runs it as a subprocess and records the
result. That's the whole design — no OS-level cron, no Windows service, just
a while True loop with a sleep at the bottom.
pip install -r requirements.txt
copy scripts_config.example.json scripts_config.json
python MasterRunner.py
A config entry looks like this:
{
"scripts": [
{
"path": "path/to/backup.py",
"description": "Weekly Backup",
"schedule": {
"type": "weekly",
"days": ["Monday", "Friday"],
"time": "22:00"
}
}
]
}
Four schedule types are supported: daily, weekly (with a days list),
date (one-off), and cron (via croniter, for anything the other three
can't express — every 15 minutes, business hours on weekdays, and so on).
Catch-up runs are the actual selling point
The detail that makes this more than a toy wrapper around subprocess calls: because the daemon checks "is it past the scheduled time and has this not run today" rather than "is it exactly this time," a script scheduled for 11:00 that your PC missed — because it was asleep, or you logged in late — still runs the next time the daemon wakes up. Plain Task Scheduler misses these unless you separately enable "run task as soon as possible after a scheduled start is missed," and even then it doesn't track per-script retry state.
The state that makes this possible lives in runner_state.json, one entry
per script path:
{
"path/to/backup.py": {
"last_run": "2026-09-19T22:00:04",
"status": "success",
"retry_count": 0
}
}
is_script_due() in MasterRunner.py reads this before deciding whether to
fire, so a restart of the daemon doesn't cause a script to double-run or
forget it already ran today.
Retries, timeouts, and knowing when something broke
Each script gets configurable max_retries, retry_delay_minutes, and
timeout_minutes — either per-script or as global defaults. If a script
fails, the daemon flips its state to failed and retries it after the delay
on a later poll, up to the retry limit. If a script hangs, run_script()
kills it once timeout_minutes elapses rather than blocking the daemon's
loop forever — the read-stdout thread is joined with a timeout, and the
process is killed if that thread is still alive when the timeout hits.
reader_thread = threading.Thread(target=read_stdout, daemon=True)
reader_thread.start()
reader_thread.join(timeout=timeout_seconds)
if reader_thread.is_alive():
timed_out = True
logging.error(f"Timeout: {description} exceeded {timeout_minutes} minutes. Killing process.")
process.kill()
On failure, it also calls winsound.Beep() — a genuinely useful touch if
the daemon runs on a machine you're sitting at: you hear a failure instead
of discovering it three days later in a log file.
Setting it up
Setup is one Task Scheduler entry, pointed at pythonw.exe (so no console
window pops up) with MasterRunner.py as the argument, triggered "at log
on." Everything else — what runs, when, how many retries — lives in
scripts_config.json, which is gitignored so your personal schedule doesn't
leak into version control if you fork the repo. Logs rotate automatically
via RotatingFileHandler, capped at 2MB with three backups, so
master_runner.log doesn't grow unbounded on a machine that's always on.
When this isn't the right tool
This is built for a single Windows machine you control, running as the logged-in user — it's not a distributed job scheduler, there's no web UI, and a two-minute polling interval means it's not meant for sub-minute precision. If you need any of that, you want something like Airflow or a proper cron daemon on Linux. For the common case — a handful of personal automation scripts on a Windows PC that need to "just run" without babysitting — a 300-line daemon and a JSON file cover it.