Say you’ve written a Python script that does something useful, like downloading data or updating a spreadsheet, and you want it to run every evening without you touching it. Windows can do this for you. In this post I’ll explain every piece involved, assuming you’ve never done this before.
The big picture
Running a script on a schedule involves a short chain of handoffs. Each piece does one job and passes control to the next:
Task Scheduler → PowerShell → your .ps1 script → Python → your Python code
(the alarm clock) (the runner) (the instructions) (the engine) (the actual work)- Task Scheduler is a program built into Windows. Its job is to start other programs at set times. Think of it as an alarm clock that, instead of ringing, launches something.
- PowerShell is Windows’ command-line tool, the place where you type commands instead of clicking. Task Scheduler starts PowerShell.
- The .ps1 file is a list of PowerShell commands saved in a text file. PowerShell reads it and runs the commands one by one.
- Python is started by one of those commands.
- Your Python code finally runs and does the work.
One thing that often confuses people: Python doesn’t “pick up” the .ps1 file. It’s the other way around. The .ps1 file is in charge, and one of its lines tells Windows to start Python.
What is a .ps1 file?
A .ps1 file is a PowerShell script. It’s just a plain text file with commands in it, the same commands you could type into a PowerShell window yourself. Saving them in a file means you (or Task Scheduler) can run them all at once, the same way every time.
If you’ve used Linux or Mac, it’s the Windows equivalent of a .sh shell script.
Why not have Task Scheduler start Python directly? You can, but a small wrapper script makes life much easier. It lets you move into the right folder first, save the output to a log file, and report back whether the run worked. We’ll see each of those below.
The wrapper script, line by line
Here’s my run_daily_job.ps1. It lives in a scripts folder inside my project:
$RepoRoot = Split-Path -Parent $PSScriptRoot
$LogDir = Join-Path $RepoRoot "logs"
New-Item -ItemType Directory -Force -Path $LogDir | Out-Null
$LogFile = Join-Path $LogDir "daily_job_$(Get-Date -Format yyyy-MM-dd).log"
Set-Location $RepoRoot
python -m app.cli.daily_job *>&1 | Out-File -Append $LogFile
exit $LASTEXITCODEIn PowerShell, anything starting with $ is a variable, a named box that holds a value. Now line by line.
$RepoRoot = Split-Path -Parent $PSScriptRoot
$PSScriptRoot is a built-in variable that holds the folder the script lives in (here, scripts). Split-Path -Parent goes up one level, to the main project folder. Now we know where the project is, no matter where the script was started from.
$LogDir = Join-Path $RepoRoot "logs"
Builds the path to a logs folder inside the project. Join-Path glues folder names together with the right slashes.
New-Item -ItemType Directory -Force -Path $LogDir | Out-Null
Creates the logs folder if it doesn’t exist. -Force means “don’t complain if it’s already there,” and | Out-Null hides the message it would otherwise print.
$LogFile = Join-Path $LogDir "daily_job_$(Get-Date -Format yyyy-MM-dd).log"
Makes a log file name with today’s date in it, like daily_job_2026-10-16.log. One file per day makes it easy to find what happened on a specific day.
Set-Location $RepoRoot
Moves into the project folder, like cd in a terminal. This line matters more than it looks. When Task Scheduler starts a script, it starts in C:\Windows\System32, not in your project. If you skip this, Python won’t be able to find your code, and things like a .env file with your passwords or database settings won’t be found either, because libraries like python-dotenv look for .env in the current folder.
python -m app.cli.daily_job *>&1 | Out-File -Append $LogFile
This is the line that starts Python. Broken down:
pythonstarts the Python program.-m app.cli.daily_jobtells Python to run a module, meaning the fileapp/cli/daily_job.pyinside the project. Running it with-mfrom the project folder lets the file import the rest of your project normally.*>&1collects everything the script prints, including error messages.| Out-File -Append $LogFilesends all of that into the log file instead of the screen. This matters because a scheduled task runs with no visible window. The log is your only way to see what happened.
exit $LASTEXITCODE
When a program finishes, it reports a number called an exit code. 0 means success, anything else means something went wrong. $LASTEXITCODE holds Python’s exit code, and this line passes it on to Task Scheduler, so Task Scheduler can show you whether the run worked.
A note on “python”
When the script says python, Windows has to find the Python program. It searches a list of folders called the PATH. If typing python in a normal PowerShell window works, it will usually work here too.
If you use a virtual environment (a separate, project-specific Python with its own installed packages), point straight at that Python instead, so your scheduled job uses the same packages you tested with:
& "$RepoRoot\.venv\Scripts\python.exe" -m app.cli.daily_job *>&1 | Out-File -Append $LogFileThe & at the start tells PowerShell “run the program at this path.”
Telling Task Scheduler about it
Next, we register the task, telling Windows what to run, when, and how. You can do this by clicking through the Task Scheduler app, but a second script is quicker and repeatable. Save this as register_daily_task.ps1 and run it once:
$action = New-ScheduledTaskAction -Execute "powershell.exe" `
-Argument "-NoProfile -ExecutionPolicy Bypass -File C:\path\to\run_daily_job.ps1"
$trigger = New-ScheduledTaskTrigger -Daily -At "18:30"
$settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -RunOnlyIfNetworkAvailable `
-RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 10)
Register-ScheduledTask -TaskName "My Daily Job" -Action $action `
-Trigger $trigger -Settings $settings -Force(The backtick at the end of a line just means “this command continues on the next line.”)
A task has three parts.
Action: what to run. Start powershell.exe and give it our .ps1 file. The extra options:
-NoProfileskips your personal PowerShell settings, so the run is clean and predictable.-ExecutionPolicy Bypassis needed because Windows blocks running script files by default as a safety measure. This allows it for this one run only, not for your whole system.-Filesays which script to run. Use the full path here.
Trigger: when to run. Every day at 18:30. For weekdays only, use:
$trigger = New-ScheduledTaskTrigger -Weekly -DaysOfWeek Monday,Tuesday,Wednesday,Thursday,Friday -At "18:30"Settings: how to behave.
StartWhenAvailable: if the PC was off at 18:30, run as soon as it’s back on.RunOnlyIfNetworkAvailable: don’t try without internet.RestartCount 3with a 10-minute interval: if a run fails, retry up to 3 times.
Finally, Register-ScheduledTask saves it all under the name “My Daily Job.” -Force means that if you run this script again later, it updates the existing task instead of throwing an error.
What happens at 18:30
Putting it all together, here’s what happens each evening:
- Task Scheduler notices it’s 18:30 and starts
powershell.exe. - PowerShell opens
run_daily_job.ps1and runs it top to bottom. - The script moves into the project folder and prepares a log file.
- It starts Python, which runs
daily_job.py. Everything Python prints goes into the log. - Python finishes with an exit code, the script passes it back, and Task Scheduler records whether the run succeeded.
Does my PC need to be on?
Yes. By default, the task only runs while the computer is on and you’re logged in. To handle missed days, my script always fetches the last 10 days of data instead of just today, and the database skips rows it already has. If the PC is off for a few days, the next run fills in the gap. It’s a good habit for any scheduled job: make it safe to run more than once, and let it catch up.
Testing it
Test from the inside out, so you know which layer broke:
- Run the Python code directly:
python -m app.cli.daily_job. Does the code work? - Run the
.ps1file:.\scripts\run_daily_job.ps1. Do the paths and log file work? - Run the real scheduled task:
Start-ScheduledTask "My Daily Job", then check the result (below). A Last Run Result of0means success.
Handy commands
See when the task runs next and how the last run went:
Get-ScheduledTask "My Daily Job" | Get-ScheduledTaskInfoRun it now:
Start-ScheduledTask "My Daily Job"Remove it:
Unregister-ScheduledTask "My Daily Job" -Confirm:$falseYou can also open Task Scheduler from the Start menu and find your task under Task Scheduler Library.
Takeaway
Scheduling a Python job on Windows comes down to a chain: Task Scheduler starts PowerShell, PowerShell runs your .ps1 script, and the script starts Python. Keep the wrapper script small, move into your project folder first, log everything, and pass the exit code back. Then test each link in the chain on its own.