Run Claude Code Automatically Every Day with Windows Task Scheduler
"Organize my Notion tasks every morning." "Summarize today's notes every evening." Once you have recurring tasks for Claude Code, launching it manually each time becomes tedious. On Windows, you can run it on a set schedule using the built-in Task Scheduler.
However, it often happens that while it works when run manually, it fails the moment you automate it. In this article, I'll share how I set up my daily automated runs and the pitfalls I encountered.
How the Automation Works
- Task SchedulerLaunches PowerShell at the scheduled time
- PowerShell ScriptChanges to the working folder and calls claude -p
- Claude CodePerforms the requested task and returns the result
- Logs & Exit CodesSaves results to a file; marks task as failed on error
Run Once Non-interactively with claude -p
By adding -p (--print), Claude Code runs a single task without opening the interactive interface and then exits. The official documentation introduces this as a way to use it from scripts or CI.
claude -p "Organize today's tasks" --output-format json
Here are three key points to keep in mind:
- You can invoke skills. Predefined skills or custom commands can be called in the form of
-p "/skill-name". This saves you from writing long instructions every time and follows the exact same workflow as running it manually. - You can receive results as JSON. With
--output-format json, the output text is placed inresult, and an estimated cost is included intotal_cost_usd(the official documentation notes that this is a local estimate and may differ from the actual bill). - It exits with a non-zero exit code on failure. Your script can determine whether the execution succeeded or failed.
Note that while adding --bare makes startup faster, it does not load CLAUDE.md or MCP configurations, and it requires an API key instead of your regular login session. If you want to use your everyday settings and the Notion MCP as-is, run it without this flag.
Limit Permissions to "Only What Is Allowed"
During automated runs, no one is there to respond when a prompt asks for confirmation. Therefore, specify the tools allowed to run with --allowedTools, and set the mode to reject anything else without prompting via --permission-mode dontAsk. According to the official documentation, in dontAsk mode "Claude Code denies every call that would otherwise prompt," while actions covered by your --allowedTools entries still run.
claude -p "/daily-tasks" `
--allowedTools "Read,Write,mcp__notion" `
--permission-mode dontAsk `
--output-format json
mcp__notion is the syntax to allow all tools from an MCP server registered under the name notion. You can verify the server name in Claude Code's /mcp list. Rather than allowing everything, listing only what is required for that specific task prevents unintended operations.
Prepare a PowerShell Script
Putting a script in between makes it much easier to manage than calling claude directly from the task. Below is a simplified version of what I use.
run_daily.ps1 (Save as UTF-8 with BOM)
# Called every morning by Task Scheduler
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
$ProjectDir = "C:\path\to\working_folder"
$LogDir = Join-Path $ProjectDir "logs"
New-Item -ItemType Directory -Force -Path $LogDir | Out-Null
Set-Location $ProjectDir
# Specify the full path found via where.exe claude
$claude = "C:\path\to\claude.cmd"
$log = Join-Path $LogDir ("run_" + (Get-Date -Format "yyyy-MM-dd_HHmmss") + ".log")
$output = & $claude -p "/daily-tasks" `
--allowedTools "Read,Write,mcp__notion" `
--permission-mode dontAsk `
--output-format json 2>&1
$code = $LASTEXITCODE
$output | Out-File -FilePath $log -Encoding utf8
Add-Content -Path $log -Value "exit code: $code"
# Without this, the task will appear successful even if claude fails
exit $code
Here are the key points to keep in mind for this script:
- Call
claudeusing its full path. The environment when running from Task Scheduler is not necessarily the same as your usual terminal. Runningwhere.exe claudein PowerShell will show its location, so specifying that full path ensures reliability. - Add
exit $codeat the end. Without this, the script's exit code defaults to the result of the last command (writing to the log), treating the task as successful even if Claude Code failed. In my experience, this once caused a delay in noticing a failure. - Capture output as UTF-8. Without the
[Console]::OutputEncodingline at the beginning, non-ASCII characters in the logs were corrupted in my environment.
Save Scripts Containing Non-ASCII Characters as UTF-8 with BOM
Windows PowerShell 5.1, which comes pre-installed with Windows, reads UTF-8 scripts without a BOM using the legacy "ANSI" encoding. Microsoft's documentation says: "If you need to use non-Ascii characters in your scripts, save them as UTF-8 with BOM." That includes accented letters, symbols, emoji, and non-Latin scripts. If you save a script containing non-ASCII characters in comments or strings without a BOM, it can cause errors during parsing. When saving in Notepad or VS Code, be sure to select "UTF-8 with BOM".
Registering the Task
While you can register the task through the GUI, writing it in PowerShell makes it easier to review and adjust your settings later.
$action = New-ScheduledTaskAction -Execute "powershell.exe" `
-Argument '-NoProfile -ExecutionPolicy Bypass -File "C:\path\to\run_daily.ps1"'
$trigger = New-ScheduledTaskTrigger -Daily -At 07:00
$settings = New-ScheduledTaskSettingsSet `
-StartWhenAvailable `
-AllowStartIfOnBatteries `
-DontStopIfGoingOnBatteries `
-MultipleInstances IgnoreNew `
-ExecutionTimeLimit (New-TimeSpan -Hours 1)
$principal = New-ScheduledTaskPrincipal -UserId $env:USERNAME -LogonType Interactive
Register-ScheduledTask -TaskName "DailyTasks" -Action $action -Trigger $trigger `
-Settings $settings -Principal $principal
| Setting | Meaning | Reason |
|---|---|---|
| -ExecutionPolicy Bypass | Bypasses execution policy restrictions specifically for running this script | Because the default Windows execution policy (Restricted) prevents running scripts |
| -StartWhenAvailable | Runs the task later even if the scheduled time was missed | To run it upon waking up on days when the PC was asleep at the scheduled time |
| -AllowStartIfOnBatteries | Starts the task even when running on battery power | Because by default, tasks are configured not to start while on battery power |
| -DontStopIfGoingOnBatteries | Does not stop if disconnected from AC power mid-run | To prevent leaving tasks in a half-finished state if stopped midway |
| -MultipleInstances IgnoreNew | Does not start a new instance if the previous one is still running | To prevent overlapping runs from writing duplicate data |
| -ExecutionTimeLimit | Upper limit on execution time | Defaults to 3 days if unspecified; keeping it short prevents tasks from hanging indefinitely |
Laptops Often Fail to Run Due to Battery Settings
According to Microsoft's documentation, the setting that keeps a task from starting on battery power (DisallowStartIfOnBatteries) is on by default. Since I carry and use my laptop on the go, leaving this setting enabled caused the task to remain queued and never run on days when it wasn't plugged in. If a task runs manually but fails when automated, check this setting first. In the GUI, it is the "Start the task only if the computer is on AC power" option on the Conditions tab of the task's properties.
Managing Costs and Skipping Unneeded Days
Claude Code consumes usage allowance (or incurs costs) with every run. In my environment, for tasks like reading and writing to Notion, total_cost_usd in the JSON was generally around $0.50 to $1.00 per run. Running it every day, even on days with nothing to do, adds up to a noticeable amount.
Therefore, before invoking Claude Code, I use a small PowerShell or Python check to verify if there is any work to do today, exiting early if there isn't. For example, exiting if there are no new requests in Notion, or exiting if there are no files to import. Handling things that can be decided without AI beforehand is the key to saving costs.
Summary
- Run non-interactively once with
claude -p. Invoking a skill replicates the exact manual workflow. - Restrict permissions to only what is needed using
--allowedToolsand--permission-mode dontAsk. - Place a PowerShell script in between, call
claudeby its full path, and end withexit $codeto pass along failures. - Save scripts containing non-ASCII characters as UTF-8 with BOM.
- On laptops, include
-AllowStartIfOnBatteries, or the task won't run when unplugged.
If you also automate Python scripts alongside this, you need to pay attention to where you place the virtual environment (venv). In A Python venv Inside OneDrive Can Break Your Scheduled Scripts, I share an issue I ran into firsthand. To notice when a scheduled task has quietly stopped, see Catch Task Scheduler Failures: Check Your Automated Runs Every Morning.