How to Schedule a PowerShell Script with Task Scheduler (the Right Way)

How to Schedule a PowerShell Script with Task Scheduler (the Right Way)

A PowerShell script that runs fine in your console often does nothing when Task Scheduler runs it: the task shows “Last Run Result 0x1”, or it says it succeeded but no output appears. Almost every time, the cause is one of five things: the program path, the arguments, the “Start in” folder, the account, or the execution policy.

Quick answer

In the task’s action, set Program/script to powershell.exe (or the full path to pwsh.exe for PowerShell 7), put the script in Add arguments with -File, and fill in Start in:

Program/script:  powershell.exe
Add arguments:   -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "C:\Scripts\Cleanup-Logs.ps1"
Start in:        C:\Scripts

Then run it as an account that has the rights the script needs, select “Run whether user is logged on or not”, and make the script write its own log file. The rest of this guide explains each setting and shows how to create the same task with PowerShell.

Step 1: Make the script safe to run unattended

Before you schedule anything, check that the script can run without a person in front of it:

  • No Read-Host, Get-Credential, Pause or confirmation prompts. A prompt in a scheduled task waits forever (or until the task time limit kills it).
  • Use full paths for every file and folder. Do not rely on the current directory.
  • Add -Confirm:$false to cmdlets that ask for confirmation, for example Remove-Item on a folder.
  • Return a useful exit code, so Task Scheduler can show success or failure.

A simple pattern that does all of this, including logging:

$LogDir = "C:\Scripts\Logs"
New-Item -Path $LogDir -ItemType Directory -Force | Out-Null
$Log = Join-Path $LogDir ("Cleanup-Logs_{0:yyyyMMdd_HHmmss}.log" -f (Get-Date))
Start-Transcript -Path $Log

try {
    $ErrorActionPreference = "Stop"
    Get-ChildItem "D:\AppLogs" -Filter *.log |
        Where-Object LastWriteTime -lt (Get-Date).AddDays(-30) |
        Remove-Item -Confirm:$false
    Write-Output "Cleanup finished."
    $code = 0
}
catch {
    Write-Output "ERROR: $($_.Exception.Message)"
    $code = 1
}
finally {
    Stop-Transcript
}
exit $code

Start-Transcript writes everything the script outputs (and the errors) to a file, which is the fastest way to see what happened during a scheduled run. $ErrorActionPreference = "Stop" turns normal errors into terminating errors so the catch block sees them. exit $code passes the result back to Task Scheduler.

Warning: this example deletes files. Test any script that removes or changes data by hand first, ideally with -WhatIf, before you schedule it.

Step 2: Set the action correctly (program, arguments, Start in)

This is where most scheduled scripts break. Do not put the .ps1 file in “Program/script”. Windows does not run .ps1 files directly; by default they open in Notepad or do nothing. The program is always the PowerShell executable, and the script goes in the arguments.

Which PowerShell to use

  • Windows PowerShell 5.1 (built into Windows Server 2016, 2019, 2022 and 2025): powershell.exe, which is in C:\Windows\System32\WindowsPowerShell\v1.0\ and on the PATH.
  • PowerShell 7 (installed separately): pwsh.exe. Use the full path, for example "C:\Program Files\PowerShell\7\pwsh.exe", so the task does not depend on the PATH of the run-as account.

Use the version you tested the script with. Some modules and cmdlets behave differently between 5.1 and 7.

The arguments

-NoProfile -NonInteractive -ExecutionPolicy Bypass -File "C:\Scripts\Cleanup-Logs.ps1" -Days 30

What each part does:

  • -NoProfile: does not load the PowerShell profile of the run-as account. This makes runs faster and predictable.
  • -NonInteractive: tells PowerShell no user is present, so commands that would prompt fail instead of hanging.
  • -ExecutionPolicy Bypass: sets the execution policy for this one process only (see Step 4).
  • -File "path": runs the script. Put the path in double quotes if it has spaces. -File must be the last PowerShell parameter, because everything after it is passed to the script as script parameters (here -Days 30).

Use -File, not -Command, for scripts. With -File, the script’s exit code becomes the process exit code, which Task Scheduler shows as “Last Run Result”.

The “Start in” field

Task Scheduler starts the program in C:\Windows\System32 if “Start in” is empty. Any relative path in the script (for example .\config.json or Out-File report.csv) then points to System32, and the script fails or writes files in the wrong place. Set “Start in” to the script folder, without quotes:

C:\Scripts

Even better, use $PSScriptRoot inside the script to build paths, for example Join-Path $PSScriptRoot "config.json". That works no matter what the working directory is.

Step 3: Choose the right account

The account decides what the script can access. Pick the option that matches what the script does:

  • SYSTEM: full rights on the local machine, no password to manage. On a domain-joined server it reaches the network as the computer account (DOMAIN\SERVER01$). Good for local maintenance. Avoid it for scripts that do not need full local rights.
  • A dedicated service account: a domain or local user created only for the task, with only the rights it needs. Select “Run whether user is logged on or not” and enter the password. The account needs the “Log on as a batch job” user right; local Administrators have it by default, but if you use a non-admin account, check this in Group Policy or secpol.msc.
  • A group Managed Service Account (gMSA): the best choice in an Active Directory domain. The password rotates automatically and nobody needs to know it. It can only be set up with PowerShell (see Step 5), and the server must be allowed to retrieve the gMSA password.

Avoid running production tasks as your own admin account. When your password changes or the account is disabled, the task stops with a logon error.

Two more options on the General tab matter:

  • Run whether user is logged on or not: required for a task that runs on a server nobody is logged on to. The script runs in the background, so you will not see a window.
  • Do not store password: the task can only access local resources. Leave it unchecked if the script reads or writes network shares.
  • Run with highest privileges: runs the task elevated (UAC). Check it if the script needs admin rights, for example to manage services or write to protected folders.

Step 4: Handle the execution policy

On Windows Server, the default execution policy is RemoteSigned, so local unsigned scripts are allowed. Scripts copied from another machine or downloaded from the internet can still be blocked because they carry a “Mark of the Web”. You can check the effective policy with:

Get-ExecutionPolicy -List

You have two clean options:

  1. Pass -ExecutionPolicy Bypass (or RemoteSigned) in the task arguments. It only applies to that PowerShell process and does not change the machine setting.
  2. Sign your scripts and use the AllSigned policy. This is the stricter option for locked-down environments.

If a script was copied from another computer, remove the block once with:

Unblock-File -Path "C:\Scripts\Cleanup-Logs.ps1"

Note: if the execution policy is set by Group Policy (the MachinePolicy or UserPolicy scope), it takes precedence over the command-line value, and -ExecutionPolicy Bypass does not override it. In that case, sign the script or ask for the policy to be changed. Also remember that the execution policy is not a security boundary; it only prevents running scripts by accident.

Step 5: Create the task with PowerShell

Clicking through the GUI works, but creating the task with the ScheduledTasks module is repeatable and easy to put in source control. Run this in an elevated PowerShell window:

$action = New-ScheduledTaskAction -Execute "powershell.exe" `
    -Argument '-NoProfile -NonInteractive -ExecutionPolicy Bypass -File "C:\Scripts\Cleanup-Logs.ps1"' `
    -WorkingDirectory "C:\Scripts"

$trigger = New-ScheduledTaskTrigger -Daily -At 2am

$principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest

$settings = New-ScheduledTaskSettingsSet -StartWhenAvailable `
    -ExecutionTimeLimit (New-TimeSpan -Hours 1) `
    -MultipleInstances IgnoreNew `
    -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 10)

Register-ScheduledTask -TaskName "Cleanup-Logs" -TaskPath "\Admin\" `
    -Action $action -Trigger $trigger -Principal $principal -Settings $settings `
    -Description "Deletes app logs older than 30 days"

What the pieces do:

  • New-ScheduledTaskAction: the program, arguments and “Start in” folder (-WorkingDirectory) from Step 2.
  • New-ScheduledTaskTrigger -Daily -At 2am: runs every day at 02:00. Other options include -Weekly -DaysOfWeek Monday, -AtStartup and -Once.
  • New-ScheduledTaskPrincipal: runs as SYSTEM, elevated.
  • -StartWhenAvailable: runs a missed start as soon as possible, for example after a reboot during the scheduled time.
  • -ExecutionTimeLimit: stops a hung script. The default limit is 3 days, which is too long for most scripts.
  • -MultipleInstances IgnoreNew: does not start a second copy while the first is still running.
  • -TaskPath "\Admin\": puts the task in its own folder in Task Scheduler, which keeps your tasks apart from the built-in Microsoft ones.

To run as a gMSA instead of SYSTEM, change the principal. The account name ends with $ and the logon type is Password, even though you never enter a password:

$principal = New-ScheduledTaskPrincipal -UserId "CONTOSO\gmsa-tasks$" -LogonType Password -RunLevel Highest

To run as a normal service account with a password, leave out -Principal and pass -User "CONTOSO\svc-tasks" -Password "..." to Register-ScheduledTask instead. Do not save that password in a script file.

Step 6: Test and read the result

Start the task now instead of waiting for the trigger:

Start-ScheduledTask -TaskPath "\Admin\" -TaskName "Cleanup-Logs"
Get-ScheduledTaskInfo -TaskPath "\Admin\" -TaskName "Cleanup-Logs"

Get-ScheduledTaskInfo shows LastRunTime, LastTaskResult and NextRunTime. Common results:

  • 0 (0x0): the script finished with exit code 0.
  • 1 (0x1): PowerShell returned 1. Usually the script failed, or PowerShell could not start it (wrong path, blocked by execution policy, bad argument). Check your transcript log first.
  • 267009 (0x41301): the task is still running. If it stays like this, the script is probably waiting for input.
  • 267011 (0x41303): the task has not run yet.

If there is no log file at all, the script never started. Check the path in the arguments, the quotes, and the “Start in” folder.

Turn on task history

Task Scheduler history is off by default on many systems. Turn it on in Task Scheduler with Action > Enable All Tasks History, or from an elevated prompt:

wevtutil set-log Microsoft-Windows-TaskScheduler/Operational /enabled:true

Then read the events for your task:

Get-WinEvent -LogName "Microsoft-Windows-TaskScheduler/Operational" -MaxEvents 200 |
    Where-Object Message -like "*Cleanup-Logs*" |
    Select-Object TimeCreated, Id, Message -First 20

Useful event IDs: 100 task started, 101 task failed to start (often a logon or password problem), 102 task completed, 103 action failed to start, 111 task terminated (for example by the time limit), 200 action started, 201 action completed (the message includes the return code).

Common problems and fixes

  • Task “succeeds” but nothing happens: “Start in” is empty and the script uses relative paths, so it worked in C:\Windows\System32. Set “Start in” or use $PSScriptRoot.
  • Script works as you, fails as SYSTEM: SYSTEM has no mapped drives and reaches network shares as the computer account. Use UNC paths (\\server\share) and give the computer account or the service account access.
  • Error 0x1 right away, no log: wrong path, missing quotes around a path with spaces, or a blocked script. Run the exact command line from the action in a console as the same account to see the error.
  • Task never starts, event 101: the stored password is wrong or expired, or the account lacks “Log on as a batch job”. Update the password or switch to a gMSA.
  • Task stops after a while: it hit the execution time limit (event 111). Raise the limit or find out why the script is slow.
  • Script depends on a service that starts late after a reboot: for -AtStartup triggers, add a delay with the trigger’s Delay property, or make the script wait for the service. If the dependency is between services, see how to add a dependency to a Windows service.

Quick checklist

  1. Program/script is powershell.exe or the full path to pwsh.exe, never the .ps1 file.
  2. Arguments use -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "full path", with -File last.
  3. “Start in” is set to the script folder, or the script uses $PSScriptRoot.
  4. The task runs as SYSTEM, a gMSA or a dedicated service account, not your personal account.
  5. “Run whether user is logged on or not” is selected, and “Run with highest privileges” if the script needs admin rights.
  6. The script has no prompts, uses full or UNC paths, writes a transcript log and returns an exit code.
  7. The execution time limit is set to something realistic, and task history is enabled.
  8. You ran the task once with Start-ScheduledTask and checked LastTaskResult and the log file.

If your scheduled scripts open a lot of outbound connections, for example monitoring or API scripts that run often, also read MaxUserPort and TcpTimedWaitDelay: fixing port exhaustion on Windows Server.

Share this content:

Leave a Reply