Automation Guide
Scheduled Jobs That Actually Run When Nobody Is Watching
A scheduled job is only done when it runs on a night nobody checks. Put each job in the right tier, fix the defaults that stop it, and alarm on what it produced, not on whether it ran.
- Year 1Website
- Year 2Map Listings
- Year 3Booking Tool
- Year 4Front Desk
- Year 5Payments
- Year 6Reports
Same six pieces.
Years of lessons, one read.

Free to read. Most of this guide is open. The finishing pieces at the end come to you when you opt in.
The Short Version
- There are three tiers: a task inside an open app, Windows Task Scheduler, and a server cron.
- Task Scheduler's defaults are wrong for unattended work. Use S4U, wake to run, start when available and allow on batteries.
- A one time task that already fired shows Ready with an empty next run time. It is dead.
- Server cron can run late when busy, so anything a customer waits on belongs inside the request.
- Alarm on outcomes. Every routine prints its own line saying what it did.
Three Tiers, and Why the Tier Matters
Most small teams automate in three places, often without noticing they are different.
- Tier one: a task inside an app. Some desktop apps can run a prompt or a script on a schedule. It only runs while the app is open and the computer is awake. Close the lid and it waits.
- Tier two: Windows Task Scheduler. It runs while the computer is on, even with no app open. With the right settings it runs when nobody is signed in and can wake the machine.
- Tier three: a server cron. A scheduled function on your host. It runs whether or not any office computer is on.
Pick the tier by asking one question. What happens if this job does not run tonight? If the answer is nothing much, tier one is fine. If a report is late, use tier two. If a customer waits, or money moves, use tier three, or better, do the work inside the request that started it.
The common failure is a job that belongs in tier three living in tier one. It works for weeks, while someone happens to leave the app open. Then a laptop goes home for the weekend and nothing runs.

Task Scheduler Defaults Are Wrong for Unattended Work
Task Scheduler was built for tasks a signed in person starts. Its defaults show that. Out of the box, a new task:
- Runs only when the user is signed in.
- Does not start if the laptop is on battery, and stops if it goes on battery.
- Does not wake the computer.
- Skips a missed run for good if the computer was off at the time.
Any one of these can kill a nightly job. Together they mean the task works while you are at your desk and fails the rest of the time. You will not see an error. The task simply does not start.
Change four settings for any job that has to run on its own.
- Log on type S4U. This runs the task whether you are signed in or not, without storing your password. It cannot reach network shares that need your sign in, so keep its files on the local disk.
- Wake to run. The computer comes out of sleep to run it. Wake timers must also be on in the power plan.
- Start when available. If the computer was off at the set time, the task runs as soon as it can.
- Allow on batteries. And do not stop if the power cord comes out.
A Real Register-ScheduledTask Example
Here is a daily job registered with those settings. Run it in PowerShell opened as administrator. Change the program, the script path and the time to match your job.
$action = New-ScheduledTaskAction `
-Execute 'C:\Program Files\Python312\python.exe' `
-Argument 'C:\jobs\nightly_report.py' `
-WorkingDirectory 'C:\jobs'
$trigger = New-ScheduledTaskTrigger -Daily -At 6am
$principal = New-ScheduledTaskPrincipal `
-UserId "$env:USERDOMAIN\$env:USERNAME" `
-LogonType S4U `
-RunLevel Limited
$settings = New-ScheduledTaskSettingsSet `
-WakeToRun `
-StartWhenAvailable `
-AllowStartIfOnBatteries `
-DontStopIfGoingOnBatteries `
-ExecutionTimeLimit (New-TimeSpan -Hours 1) `
-MultipleInstances IgnoreNew
Register-ScheduledTask -TaskName 'Nightly Report' `
-Action $action -Trigger $trigger `
-Principal $principal -Settings $settingsTwo settings in there are worth a word. The time limit stops a hung job from running for days. IgnoreNew means that if last night's run is still going, tonight's does not start a second copy on top of it.
Always set the working directory. Without it, a script that opens a file by a short name looks in the system folder and fails.

A One Time Task Dies Without a Sound
This one catches everyone. You make a task with a one time trigger to test it, or to run something on a date. It fires once. Then it sits in the list with a status of Ready, which looks healthy. It is not. It will never run again.
The tell is the next run time. A live task has one. A spent one time task has an empty next run time. Check it with one line.
Get-ScheduledTask -TaskName 'Nightly Report' |
Get-ScheduledTaskInfo |
Select-Object LastRunTime, LastTaskResult, NextRunTimeRead all three. NextRunTime should be a date in the future. LastTaskResult should be 0, which means the last run finished cleanly. Anything else is a code worth looking up. A task whose last run time is months ago, or blank, has not been running at all.
If you need a job to repeat every few minutes, do not stack one time tasks. Use a daily trigger with a repeat interval. The finishing pieces include the exact lines.
Server Cron Runs Late When It Is Busy
A server schedule of every five minutes is a wish, not a promise. When the host is busy, runs get delayed or skipped. A job that runs every minute and takes most of a minute can crowd out every other job on the same site. We have seen a five minute job wait several times its own interval before it ran.
Two things follow from that.
- Anything a customer is waiting on belongs inside the request. If someone sends a form, the reply goes out from the code that saved the form. Not from a sweep that looks for new forms later. A sweep can be a backup. It should not be the main path.
- A watchdog on a slow schedule is slow too. If the check that alerts you runs every five minutes in name and every thirty in practice, your alert is thirty minutes late.
Before you blame the schedule, count the runs. A job firing far more often than its cron says is its own bug, and it starves everything else.
Alarm on Outcomes, Not on Runs
A job that ran is not a job that worked. A task can run on time every night for a month and do nothing, because an input was empty, a key expired or an error was caught and ignored. Every dashboard that counts runs shows it as healthy.
So every routine prints its own line, every time, saying what it did. Not started. Not done. What it did.
nightly-report ok rows=214 sent=1 took=38s
nightly-report ok rows=0 sent=0 took=2s
nightly-report FAIL step=gsc error=401 took=4sLook at the middle line. It says ok. But zero rows on a weekday is not normal. A good alarm reads the numbers, not the word. Set a floor for each job. Say a report should never have fewer than ten rows on a weekday. Alarm when it does.
Then alarm on silence. If there is no line at all by an hour after the job should have run, that is a failure too. The job that cannot report is the one you most need to hear about.
Keep One List of Every Job
Jobs multiply. One report becomes three, a sweep gets a backup sweep, and a test task from last spring is still in the list. Nobody knows which ones matter until one stops.
Keep one plain list, in the repo or a shared doc, with a row for each job. Write down its name, its tier, when it runs, what it produces, who reads the result, and how to turn it off. Add the floor you expect, like at least one row on a weekday.
That list does three jobs. It tells a new person what runs where. It tells your watchdog what to check. And it makes a dead job easy to spot, because a job on the list with no line in the log stands out.
When you retire a job, remove it from the scheduler and from the list in the same change. A job that is gone from one and not the other is either a ghost or a false alarm.
What the Finishing Pieces Contain
You can make a job reliable with what is above. The finishing pieces give you a copy ready registration for a job that repeats every few minutes, a wrapper that makes any script print its own line and exit with the right code, and a watchdog that alarms on silence and on low numbers. They also list the edge cases that stop Task Scheduler in ways the status never shows, and the check we run on every job each week. Next, read the bugs that return success and do nothing.
The finishing pieces
Get the Rest of This Guide
You have the method. The finishing pieces are the parts you copy straight into your own work:
- A Repeating Task and a Wrapper That Reports
- A Watchdog That Alarms on Silence and Low Numbers
- Edge Cases and the Weekly Check
Questions People Ask
Why does my scheduled task only run when I am logged in?
That is the Task Scheduler default. Set the log on type to S4U so it runs whether you are signed in or not, without storing your password.
My task says Ready but never runs. What is wrong?
It is likely a one time task that already fired. Check its next run time. If that is empty, the task is spent and needs a daily or repeating trigger.
Should a customer text reply run on a cron schedule?
No. Server schedules can run late when busy. Send the reply inside the request that received the message, and keep any sweep only as a backup.
How do I know a nightly job actually did its work?
Have it print one line with real counts every run, then alarm when the line is missing or the counts are below a normal floor.
We sell time
Get Years of Building Switched On in Hours
A thirty minute call. A written price. Nothing built until you say yes.

