Loosely Decoupled
TroubleshootingWindows ServerAutomation

Task Scheduler Falsely Reports Success for a PowerShell Script That Works Manually

Task Scheduler records the exit code of powershell.exe, not whether the script worked, so a script that hit an error and carried on is recorded as a success.

A red-faced, cigar-chomping newspaper editor thrusts a cyan front page reading SCHEDULER LIES! across his desk, while a curly-haired young woman beside him raises one eyebrow over her mug of tea. The picture is a painted gouache illustration with the front page as its one cyan object.
On this page

TL;DR

When a PowerShell script works at your prompt but fails under Task Scheduler, the task can still report success. A Last Run Result of 0x0 and a history entry that says “successfully” don’t prove that the script worked. Three failures leave the result at 0x0: a non-terminating error, a failed Import-Module, and a native command’s exit code. A non-terminating error is one that PowerShell reports and then carries on past.

Either of two changes shows the failure. One change adds Start-Transcript to the script. The other change runs the script from a -Command action that redirects all output to a file. End the script with exit $LASTEXITCODE, so that a native command’s exit code reaches the task:

PowerShell
Start-Transcript -Path 'C:\Scripts\logs\job-transcript.log'
# ... the script ...
exit $LASTEXITCODE

With a -File action, the exit line passes the native command’s code to the task. A script that ends with cmd.exe /c exit 5 and then exit $LASTEXITCODE shows 0x5. A -Command action that calls the script with & reports 1 for any exit code from the script other than 0 or 1. A -Command action needs ; exit $LASTEXITCODE at the end of its own command string as well. Wrap the call to the script in try too. With the try, a script that throws, hits an -ErrorAction Stop error, doesn’t exist or doesn’t parse reports 0x1. The catch writes the error to the log:

Text
-NoProfile -Command "try { & 'C:\Scripts\job.ps1' *> C:\Scripts\logs\job.log } catch { $_ *>> C:\Scripts\logs\job.log; exit 1 }; exit $LASTEXITCODE"

Create C:\Scripts\logs first, and make sure that the task’s account can write to the folder. If the folder is missing or not writable, the script never runs, and the task still shows 0x0.

Then check the usual causes:

  • The task runs as a different account, with its own profile, module path and registry hive, and no mapped drives. Install modules in C:\Program Files\WindowsPowerShell\Modules, and use UNC paths.
  • A standard user’s task with a stored password failed to start when the user lacked the Log on as a batch job right. The failed task showed 0x41303, with an empty last run time. To confirm the cause, run Get-WinEvent -FilterHashtable @{ LogName = 'Security'; Id = 4625 } -MaxEvents 5 in an elevated session. The fix, untested here, is to add the account to Log on as a batch job (Local Policies, User Rights Assignment).
  • Without a Start in value, the working directory is C:\Windows\system32. Set Start in, or build paths from $PSScriptRoot.
  • A script path that contains spaces fails when it isn’t quoted. Quote the path: -File "C:\Scripts\My Scripts\job.ps1".
  • Under RemoteSigned, the zone mark that Windows puts on a downloaded file blocks the script. Check Get-ExecutionPolicy -List and the script’s Zone.Identifier stream. Run Unblock-File after you have read the script.
  • Without -NonInteractive, a Read-Host prompt leaves the task running. With -NonInteractive, the task quietly succeeds with an empty value. Take the prompt out of the script.

I ran each of these failures on short-lived Windows Server 2025 test VMs, all built from the same image. I registered the tasks with Register-ScheduledTask, started them with Start-ScheduledTask, and read the Last Run Result back with Get-ScheduledTaskInfo. The failure runs used SYSTEM; the account and working-directory checks also ran as a local administrator, and the batch logon case as a standard user.

Success is reported when the script failed

Task Scheduler reports whether the process exited, not whether your script did its work. Each of these script endings gives the same Last Run Result in Windows PowerShell 5.1 and in PowerShell 7, with -File or -Command, run as SYSTEM. The first three fail and still show 0x0:

Script ends withLast Run Result
Write-Error 'boom', then more output0x0
cmd.exe /c exit 5 as the last line0x0
Import-Module of a module that doesn’t exist, then a missing command0x0
throw "boom"0x1
exit 10x1

The task history is no better. With history turned on, a task whose script ends with exit 1 logs event 201 and event 102:

Error

Task Scheduler successfully completed task "\<task-name>" , instance "{<guid>}" , action "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" with return code 2147942401.
Task Scheduler successfully finished "{<guid>}" instance of the "\<task-name>" task for user "NT AUTHORITY\SYSTEM".

The return code 2147942401 is 0x80070001, the exit code 1 in HRESULT form.

Make the failure visible

Do this before you chase a cause. The first three ways in this section record or pass on what the script did; the fourth records only the exit code and run times, not the failure.

Pass a native command’s exit code to the task

A task reads the process exit code, and a script that ends on a native command doesn’t pass that command’s code on. A script that ends with cmd.exe /c exit 5 shows 0x0. Add one line and the task shows 0x5 with -File, in both editions:

PowerShell
cmd.exe /c exit 5
exit $LASTEXITCODE

That holds for a -File action. Under -Command, when the last command is a script or an external program that exits with a code other than 0 or 1, PowerShell converts that code to 1. about_powershell_exe (opens in a new tab) says so, and PowerShell -File vs. -Command: Which Exit Code Does the Caller Get? measures it case by case. The same script shows 0x1 from -Command "& 'job.ps1'", and so does a script that ends with exit 5. End the action’s own quotes with ; exit $LASTEXITCODE and the task shows 0x5 in both editions, even when the script itself has no exit line.

$LASTEXITCODE holds the code of the last native program, or of a script that ended with exit. A cmdlet never sets it; for why, see Why $LASTEXITCODE Doesn’t Work the Way You Expect in PowerShell.

Record a transcript inside the script

Put Start-Transcript first in the script. In my test, a script that ran whoami, a Write-Error, a failing Import-Module and a throw left a transcript that held the account (nt authority\system), both errors, and the throw. The task result was 0x1. In Windows PowerShell 5.1, each error appears twice in the transcript.

PowerShell
Start-Transcript -Path 'C:\Scripts\logs\job-transcript.log'
whoami

Redirect all output from the action

If you can’t edit the script, run it from a -Command action and redirect every stream to a file. A redirect creates the file but not a missing folder. If the folder is missing, or SYSTEM can’t write to it, the script doesn’t run at all, and the task still shows 0x0 in both editions, with or without the try. So create the folder first, make sure the task’s account can write to it, and check that the log exists after the first run:

PowerShell
New-Item -ItemType Directory -Force -Path 'C:\Scripts\logs'

Then set the action’s arguments:

Text
-NoProfile -Command "try { & 'C:\Scripts\job.ps1' *> C:\Scripts\logs\job.log } catch { $_ *>> C:\Scripts\logs\job.log; exit 1 }; exit $LASTEXITCODE"

To register it from PowerShell, keep the arguments in a single-quoted here-string so nothing expands:

PowerShell
$arguments = @'
-NoProfile -Command "try { & 'C:\Scripts\job.ps1' *> C:\Scripts\logs\job.log } catch { $_ *>> C:\Scripts\logs\job.log; exit 1 }; exit $LASTEXITCODE"
'@
$action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument $arguments

Read the registered action back to confirm it:

PowerShell
(Get-ScheduledTask -TaskName '<task-name>').Actions.Arguments

Leave ; exit $LASTEXITCODE off the end of the action’s arguments, and a plain & 'script' *> log action shows 0x1, even when the script itself ends with exit $LASTEXITCODE after a native code of 5. With it in the arguments, the task shows 0x5. If the script writes a non-terminating error and carries on, the Last Run Result stays 0x0, and the log holds the error and the line that comes after it. The redirect shows the failure; it doesn’t change the result.

The try is there for a script that stops on an error or never starts. Without the try/catch, four failures go wrong. A script path that doesn’t exist and a script with a syntax error each show 0x0, because no native command has set a code, and neither creates a log file. A script that prints a line and then hits a throw or an -ErrorAction Stop error shows 0x1, but its log holds only the printed line, not the error. With the catch writing $_ (the error it caught) to the log, all four show 0x1 in both editions. Each log holds the error: the throw message, Cannot find path, the parse error, or The term ... is not recognized. The earlier results don’t change.

Turn on task history

On the fresh image I tested, the Microsoft-Windows-TaskScheduler/Operational log was disabled, so a failed task left no history at all. This changes the event log configuration of the machine. In an elevated session, run:

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

After that, registering and running one task logged event 106 (registered), then 325 (queued), 110 (launched), 129 (launched process), 100 (started), 200 (launched action), and then 201 and 102 as shown earlier.

Read the last run result

Find your code in the table. The meanings are what each value meant in these tests, not a full list.

Last Run ResultWhat it meant in these tests
0x0The process exited with code 0. The script might still have failed (see the table earlier).
0x1throw, exit 1, a script blocked by its zone mark, or any other non-zero exit code from a script that a -Command action calls with & (a script that ended with exit 5 gives this; see “Pass a native command’s exit code to the task”). A -Command action wrapped in try/catch also gives 0x1 for a missing script or one that doesn’t parse.
0x5exit $LASTEXITCODE after a native command that exited with 5.
0x41303The task has not yet run. A standard user’s task showed this and never started.
0x41301The task is running. A script waiting on Read-Host stayed here.
0x41306The task was stopped while running, by hand or by its execution time limit. A script waiting for input showed this both ways.
0xFFFD0000Windows PowerShell 5.1 could not find the -File script: the path doesn’t exist, or it contains a space and isn’t quoted.
0x40PowerShell 7 could not find the -File script: the path doesn’t exist, or it contains a space and isn’t quoted.

To read the value, and the time of the last run:

PowerShell
Get-ScheduledTaskInfo -TaskName '<task-name>' -TaskPath '\' |
    Select-Object LastRunTime, @{ Name = 'LastTaskResult'; Expression = { '0x{0:X}' -f $_.LastTaskResult } }

Change -TaskPath if the task is in a subfolder.

Check a task with a coding agent

Paste this into your own local coding agent, with the task name filled in. It reads the task and reports against the checklist in this article. It changes nothing and doesn’t start the task.

Text
You are checking one Windows scheduled task. Read only. Do not change anything:
no Set-ScheduledTask, no Register-ScheduledTask, no wevtutil set-log, no edits
to the script, no policy changes. Do not start, stop or enable the task, and do
not run the script.

Task name: <task-name>

1. Run Get-ScheduledTask and Get-ScheduledTaskInfo for the task. Report:
   - the principal: user, logon type and run level
   - the action: executable, arguments and the Start in (working directory)
   - Last Run Result (as hex) and last run time
2. Report whether the log Microsoft-Windows-TaskScheduler/Operational is
   enabled. If it is disabled, say so and propose the command to enable it as
   text. Do not run it.
3. If the principal is a user and not SYSTEM, report whether that account
   holds the "Log on as a batch job" right (SeBatchLogonRight), directly or
   through a group. Use a read-only method.

Then compare each finding with this checklist and say which items apply:
- 0x1: the script threw, called exit 1, or is blocked. Read the script path
  from the action and check it with
  Get-Item -LiteralPath <path> -Stream Zone.Identifier -ErrorAction SilentlyContinue.
  A ZoneId=3 stream under a RemoteSigned policy is the blocked-script cause.
  Do not run Unblock-File; propose it as text.
- Last Run Result 0x0 does not prove success. Errors that do not terminate,
  a failed Import-Module and a native command's exit code all leave 0x0.
  The script should write a transcript or the action should redirect output
  to a file, and the script should end with exit $LASTEXITCODE.
- The action uses -Command and calls a script with &: any exit code other
  than 0 or 1 from that script reaches the task as 0x1 unless the action's own quotes end with ; exit $LASTEXITCODE.
  If they do, the call should sit inside try { } catch { $_ *>> <log>;
  exit 1 }, or a script that is missing or doesn't parse reaches the task
  as 0x0, and an error that stops the script never reaches the log.
- The action redirects to a log file that doesn't exist after the task ran:
  the script never ran, whatever the Last Run Result says. Check that the
  folder exists and the task's account can write to it, then that the script
  path exists and the script parses. Without a catch that writes $_ to the
  log, a missing script or a syntax error also leaves no log.
- 0x41303 with no last run time: the task never started. For a non-SYSTEM
  user whose task stores a password, a missing batch logon right is the
  cause found in these tests.
- 0x41301 for a long time: the script may be waiting on Read-Host.
- 0xFFFD0000 (Windows PowerShell 5.1) or 0x40 (PowerShell 7): the -File
  script was not found: the path doesn't exist, or it contains a space and
  isn't quoted in the arguments.
- Start in empty: the working directory is C:\Windows\system32.
- The principal differs from the account the script was tested as: profile,
  module path, registry hive and mapped drives differ. Mapped drives do not
  carry from one task to the next.
- The action's executable is powershell.exe or pwsh.exe: the two editions
  read different module folders.

Report each finding, then propose each fix as text for me to review.

Common failures

Module, drive or setting missing under the task

The task runs as a different account, and each account has its own profile, registry hive and module folders. I read these from a probe script that ran as a task in each context:

Task runs asProfile folderCurrent directoryHKCU value set by the adminModule installed for the current user in 5.1
SYSTEM, Windows PowerShell 5.1C:\Windows\system32\config\systemprofileC:\Windows\system32not foundnot found
SYSTEM, PowerShell 7C:\Windows\system32\config\systemprofileC:\Windows\System32not foundnot found
Local administrator, 5.1C:\Users\<user>C:\Windows\system32foundfound
Local administrator, PowerShell 7C:\Users\<user>C:\Windows\System32foundnot found

Three things follow:

  • SYSTEM has no Documents folder. The first entry on SYSTEM’s PSModulePath in 5.1 is the relative path WindowsPowerShell\Modules.
  • A module is tied to an edition and a user. That user’s 5.1 task finds a module installed for the current user in 5.1 (Documents\WindowsPowerShell\Modules). The same user’s PowerShell 7 task doesn’t, and neither does SYSTEM in either edition. C:\Program Files\WindowsPowerShell\Modules is on every list, so install there for a task. For the steps, see Install AWS Tools for PowerShell for All Users on Windows.
  • A mapped drive doesn’t carry over. I mapped Z: with net use Z: \\localhost\tsshare /persistent:yes inside one task. It was gone in every later task, including a later task of the same user, although that user’s HKCU:\Network\Z entry was still present. Use UNC paths (\\server\share) in the script. I didn’t test share access from a task account, so confirm that the account can read the share.

In my tests, Run with highest privileges didn’t change the group list or integrity level of the task’s process, for a task registered with a stored password. The integrity level is the trust level Windows gives a process. whoami /groups showed the same groups and the same High Mandatory Level with the setting off and on. I didn’t test “Run only when user is logged on” or S4U logons (the “Do not store password” option), so do not assume it holds there.

Standard user’s task never starts and shows 0x41303

The task registers without an error, Start-ScheduledTask returns, and nothing happens. The Last Run Result is 0x41303, the last run time is empty and the script leaves no output. On my test machine the Operational log held nothing, because it was disabled.

The cause shows up in the Security log: the standard user (a member of Users only) isn’t allowed to log on as a batch job. On my test machine, Administrators, Backup Operators and Performance Log Users held the right. Two events of ID 4625 recorded it:

Error

Logon Type: 4
Account Name: <user>
The user has not been granted the requested logon type at this machine.
Status: 0xC000015B

To look for them, in an elevated session:

PowerShell
Get-WinEvent -FilterHashtable @{ LogName = 'Security'; Id = 4625 } -MaxEvents 5

The right is named Log on as a batch job (SeBatchLogonRight). Per Microsoft’s documentation of the setting (opens in a new tab), it lives under Computer Configuration\Windows Settings\Security Settings\Local Policies\User Rights Assignment. A change takes effect the next time the account logs on. The same page says that when the Add Scheduled Task Wizard sets up a task with a user name and password, the user gets the right automatically, and that domain-based Group Policy for the right prevents that. I registered my task with Register-ScheduledTask and a stored password, not with the wizard, and the account didn’t get the right. I didn’t test the wizard. The accounts were local, and I didn’t examine Group Policy on the machine. The fix is to add the account to Log on as a batch job (Local Policies, User Rights Assignment). I didn’t test granting the right, so confirm the result by starting the task afterwards and reading the Last Run Result.

Relative paths resolve to the wrong folder

Without a Start in value, the working directory is C:\Windows\system32 in both editions, for SYSTEM and for the local administrator, so a relative path resolves there. Set Start in to C:\ts and the working directory is C:\ts in both. $PSScriptRoot holds the running script’s own folder in every context, with or without Start in, so build paths from it:

PowerShell
$logPath = Join-Path $PSScriptRoot 'job.log'

For a task registered in PowerShell, the -WorkingDirectory parameter of New-ScheduledTaskAction sets Start in. The Start a 7 Days to Die Dedicated Server at Boot on Windows and Stop It Cleanly registers a task that way.

Script path not found or unquoted gives 0xFFFD0000 or 0x40

A -File path that doesn’t exist at all gives the same two exit codes. The case I tested here is a path that contains a space with the -File argument unquoted. With -File C:\ts\My Scripts\ok.ps1, Windows PowerShell 5.1 shows 0xFFFD0000 and PowerShell 7 shows 0x40. The console messages differ. Windows PowerShell 5.1 prints:

Error

Processing -File 'C:\ts\My' failed because the file does not have a '.ps1' extension. Specify a valid Windows PowerShell script file name, and then try again.

PowerShell 7 prints:

Error

The argument 'C:\ts\My' is not recognized as the name of a script file. Check the spelling of the name, or if a path was included, verify that the path is correct and try again.

Quote the path in the action’s arguments. -File "C:\ts\My Scripts\ok.ps1" gives 0x0 on both:

Text
-NoProfile -File "C:\Scripts\My Scripts\job.ps1"

Downloaded script is blocked and gives 0x1

A script that carries a Zone.Identifier stream with ZoneId=3 fails with 0x1 under the RemoteSigned policy, in both editions and with both -File and -Command. The same content without the stream gives 0x0, which is the control. Windows PowerShell 5.1 prints the message below; PowerShell 7 prints the same text after SecurityError:.

Error

File C:\ts\f\zoned.ps1 cannot be loaded. The file C:\ts\f\zoned.ps1 is not digitally signed. You cannot run this script on the current system.

The test image shipped Windows PowerShell 5.1 with the LocalMachine policy set to Unrestricted, which warns before it runs a downloaded file rather than refusing it. I didn’t test a zoned script under it. I set it to RemoteSigned first, which per about_Execution_Policies is the Windows Server default.

To check whether a script carries the mark:

PowerShell
Get-Item -LiteralPath 'C:\Scripts\job.ps1' -Stream Zone.Identifier -ErrorAction SilentlyContinue

Only if the stream is present and you have read the script:

PowerShell
Unblock-File -LiteralPath 'C:\Scripts\job.ps1'

Unblock-File removes the Zone.Identifier stream, per its documentation (opens in a new tab). I didn’t run it in these tests.

Task stays running, or finishes with nothing

Both outcomes come from the same cause: the script was written for a person at a prompt. With a Read-Host line, the task is still in State Running (0x41301) after 30 seconds, in both editions. The default execution time limit of a settings set from New-ScheduledTaskSettingsSet is 72 hours (PT72H), so the task can stay that way for days. When I stopped the stuck task after 30 seconds, its Last Run Result became 0x41306.

Adding -NonInteractive to the action changes the failure and hides it. The task finishes in under 30 seconds with 0x0, and the output file reads answered: with nothing after it. The error ends only that statement, not the script, so the script carried on with an empty value:

Error

Read-Host : Windows PowerShell is in NonInteractive mode. Read and Prompt functionality is not available.

Take the prompt out of the script, and pass what it asked for as a parameter or a value in the script. Do not use -NonInteractive to make a prompt go away.

What these tests did not cover

The numbers in this article come from short-lived test VMs of one Windows Server 2025 image, and they are not a general result.

  • Not tested: “Run only when user is logged on”, “Do not store password” (S4U), domain accounts and group managed service accounts, network share access as SYSTEM or as a user, and arguments passed to the script from the action.
  • Not tested: Windows Server 2022 and Windows 11.
  • Not tested: the fix for the batch logon right, and the fix for the blocked script.

Tested with

OS
Windows Server 2025 Datacenter 24H2, build 26100.33650
PowerShell
Windows PowerShell 5.1.26100.33648 and PowerShell 7.6.8
Accounts
Local administrator and local standard user, stored-password logon, plus SYSTEM
Execution policy
LocalMachine set to RemoteSigned for the failure runs
Redirect wrapper
Rerun on build 26100.33674 with PowerShell 7.6.9, as SYSTEM
Missing -File script
Build 26100.33674 with PowerShell 7.6.9, as SYSTEM
Not tested
Logged-on-only and S4U logons, domain accounts, network shares, Windows Server 2022, Windows 11
Date
October 2026

New articles, by email.

A short note when something new is published.