KB
Windows Server

Windows Services and Task Scheduler

7 min read1349 words15 code blocks

At a glance#

  • Purpose: Manage Windows services and scheduled tasks from the GUI and PowerShell, and diagnose ones that fail to run.
  • Applies to: Windows Server 2016, 2019, 2022 and Windows 10/11.
  • Risk: Medium — disabling the wrong service or running a task as the wrong account can break applications.
  • Time: 15–45 minutes.

Overview#

Windows has two scheduling and process-management systems that map closely onto their Linux equivalents:

WindowsLinux equivalentPurpose
Servicessystemd unitsLong-running background processes
Task Schedulercron / systemd timersJobs that run on a schedule or trigger

Both can be driven from PowerShell, which is what makes them scriptable and auditable. The GUI is fine for a one-off; PowerShell is what you use when the same change has to land on twelve servers.

Managing services#

Inspecting#

powershell
# all services, most useful columns
Get-Service | Select-Object Status, Name, DisplayName, StartType | Sort-Object Status

# one service
Get-Service -Name 'W3SVC'

# search by display name
Get-Service | Where-Object { $_.DisplayName -like '*SQL*' }

# everything currently running
Get-Service | Where-Object { $_.Status -eq 'Running' }

# set to start automatically but not actually running - the interesting list
Get-Service | Where-Object { $_.StartType -eq 'Automatic' -and $_.Status -ne 'Running' }

That last query is the Windows equivalent of systemctl --failed and is the first thing to run on a server behaving oddly after a reboot.

Controlling#

powershell
Start-Service   -Name 'W3SVC'
Stop-Service    -Name 'W3SVC' -Force
Restart-Service -Name 'W3SVC' -Force

# startup type
Set-Service -Name 'W3SVC' -StartupType Automatic
Set-Service -Name 'W3SVC' -StartupType Manual
Set-Service -Name 'W3SVC' -StartupType Disabled
Warning: Stop-Service -Force also stops every dependent service, and they are not restarted automatically when you start the parent again. Check dependencies first with Get-Service -Name X -DependentServices.

Dependencies#

powershell
# what this service needs
Get-Service -Name 'W3SVC' -RequiredServices

# what depends on this service
Get-Service -Name 'W3SVC' -DependentServices

Detail the cmdlets do not show#

Get-Service omits the logon account and binary path. Use CIM for those:

powershell
Get-CimInstance Win32_Service -Filter "Name='W3SVC'" |
  Select-Object Name, StartName, PathName, State, StartMode

Find every service not running as a built-in account — worth auditing, since these run with domain or local credentials:

powershell
Get-CimInstance Win32_Service |
  Where-Object { $_.StartName -notmatch '^(LocalSystem|NT AUTHORITY)' } |
  Select-Object Name, StartName, State | Format-Table -AutoSize

Recovery actions#

Configure a service to restart itself on failure:

powershell
sc.exe failure "W3SVC" reset= 86400 actions= restart/60000/restart/60000/restart/60000
sc.exe qfailure "W3SVC"

The spacing in sc.exe is unusual but required — the space goes after the =, not before.

Running an application as a service#

Windows has no direct equivalent of a systemd unit file. For an arbitrary executable, use NSSM (the Non-Sucking Service Manager) or sc.exe:

powershell
sc.exe create MyApp binPath= "C:\Apps\myapp\myapp.exe --config C:\Apps\myapp\config.yaml" start= auto DisplayName= "Internal Reporting API"
sc.exe description MyApp "Internal reporting API service"
Start-Service MyApp
powershell
# remove
Stop-Service MyApp -Force
sc.exe delete MyApp
Note: sc.exe create only works properly for applications written to behave as a service. A plain console executable will start and immediately be marked as failed. NSSM wraps ordinary executables and is the practical answer for those.

Task Scheduler#

Creating a scheduled task#

powershell
$action    = New-ScheduledTaskAction -Execute 'PowerShell.exe' `
             -Argument '-NoProfile -ExecutionPolicy Bypass -File "C:\Scripts\vsphere-daily-report.ps1"'

$trigger   = New-ScheduledTaskTrigger -Daily -At 2:00AM

$principal = New-ScheduledTaskPrincipal -UserId 'DOMAIN\svc_reports' `
             -LogonType Password -RunLevel Highest

$settings  = New-ScheduledTaskSettingsSet `
             -StartWhenAvailable `
             -DontStopOnIdleEnd `
             -ExecutionTimeLimit (New-TimeSpan -Hours 2) `
             -MultipleInstances IgnoreNew

Register-ScheduledTask -TaskName 'Daily vSphere Report' `
  -Action $action -Trigger $trigger -Principal $principal -Settings $settings `
  -Description 'Exports VM inventory, datastore usage and old snapshots to C:\Reports'

Key settings and why they matter:

SettingEffect
-StartWhenAvailableRuns a missed task as soon as possible — the equivalent of Persistent=true on a systemd timer
-ExecutionTimeLimitKills a task that hangs, preventing it blocking future runs
-MultipleInstances IgnoreNewSkips a run if the previous one is still going
-RunLevel HighestRuns elevated; required for most administrative scripts
-LogonType PasswordRuns whether or not the user is logged on
Warning: -ExecutionPolicy Bypass in the argument applies only to that PowerShell process. Do not change the machine-wide execution policy to make a task work — scope it to the task as shown.

Common trigger types#

powershell
New-ScheduledTaskTrigger -Daily -At 2:00AM
New-ScheduledTaskTrigger -Weekly -DaysOfWeek Sunday -At 3:30AM
New-ScheduledTaskTrigger -AtStartup
New-ScheduledTaskTrigger -AtLogOn

# every 15 minutes, indefinitely
$t = New-ScheduledTaskTrigger -Once -At (Get-Date)
$t.RepetitionInterval = (New-TimeSpan -Minutes 15)
$t.RepetitionDuration = ([TimeSpan]::MaxValue)

Managing tasks#

powershell
# list non-Microsoft tasks
Get-ScheduledTask | Where-Object { $_.TaskPath -notlike '\Microsoft\*' } |
  Select-Object TaskName, State, TaskPath

# last result and next run
Get-ScheduledTask -TaskName 'Daily vSphere Report' | Get-ScheduledTaskInfo

# run now, without waiting for the trigger
Start-ScheduledTask -TaskName 'Daily vSphere Report'

Disable-ScheduledTask   -TaskName 'Daily vSphere Report'
Enable-ScheduledTask    -TaskName 'Daily vSphere Report'
Unregister-ScheduledTask -TaskName 'Daily vSphere Report' -Confirm:$false

Reading the result code#

powershell
Get-ScheduledTask | Where-Object { $_.TaskPath -notlike '\Microsoft\*' } |
  Get-ScheduledTaskInfo |
  Select-Object TaskName, LastRunTime, LastTaskResult, NextRunTime |
  Sort-Object LastTaskResult -Descending | Format-Table -AutoSize
LastTaskResultMeaning
0Success
1Incorrect function — usually the script itself returned an error
2File not found
10Environment incorrect
267009Task is currently running
267011Task has not yet run
2147942401Access denied — check the run-as account's rights
2147943645The service cannot start because the user is not logged on — set LogonType Password

A LastTaskResult of 0 means the task started and exited cleanly. It does not mean the script did what it was supposed to. Have the script write its own log, exactly as with cron.

Event log#

powershell
# task scheduler operational log
Get-WinEvent -LogName 'Microsoft-Windows-TaskScheduler/Operational' -MaxEvents 50 |
  Select-Object TimeCreated, Id, Message | Format-List

# service control manager errors - service start failures land here
Get-WinEvent -FilterHashtable @{LogName='System'; ProviderName='Service Control Manager'; Level=2} -MaxEvents 20 |
  Select-Object TimeCreated, Message | Format-List

# system errors in the last day
Get-WinEvent -FilterHashtable @{LogName='System'; Level=1,2; StartTime=(Get-Date).AddDays(-1)} |
  Select-Object TimeCreated, ProviderName, Id, Message | Format-Table -AutoSize

The Task Scheduler operational log is disabled by default on some builds. Enable it before you need it:

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

Verification#

powershell
# service in the intended state
Get-Service -Name 'MyApp' | Select-Object Name, Status, StartType

# and will come back after a reboot
Get-CimInstance Win32_Service -Filter "Name='MyApp'" | Select-Object Name, StartMode, StartName

# task registered and scheduled
Get-ScheduledTask -TaskName 'Daily vSphere Report' | Get-ScheduledTaskInfo

# force a run and confirm the result
Start-ScheduledTask -TaskName 'Daily vSphere Report'
Start-Sleep -Seconds 30
(Get-ScheduledTask -TaskName 'Daily vSphere Report' | Get-ScheduledTaskInfo).LastTaskResult

Then confirm the task's own output — the report file, the log entry, the backup — rather than trusting a zero exit code.

For services, reboot and re-check. A service running now but set to Manual will silently not return.

Troubleshooting#

SymptomCause and fix
Task result 2147942401 (access denied)Run-as account lacks rights to the script or its output path. Grant them, or use a different account.
Task result 2147943645Account not logged on and LogonType is Interactive. Set -LogonType Password.
Task runs manually but not on scheduleTrigger not enabled, or the task is set to run only on AC power. Check -AllowStartIfOnBatteries.
Task shows success but nothing happenedScript failed internally. Have it write its own log and check that.
Script works in a console but fails as a taskDifferent working directory and environment. Use absolute paths and set -WorkingDirectory.
Service will not start, error 1053Application is not a real Windows service. Wrap it with NSSM.
Service starts then stops immediatelyThe application exited. Check the Application event log and the app's own log.
Service fails after a password changeThe run-as account's stored password is stale. Update it in the service properties.
Dependent services down after a restartStop-Service -Force stopped them. Start them explicitly.
Task Scheduler history is emptyOperational log disabled. Enable with wevtutil.