Windows Services and Task Scheduler
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:
| Windows | Linux equivalent | Purpose |
|---|---|---|
| Services | systemd units | Long-running background processes |
| Task Scheduler | cron / systemd timers | Jobs 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#
# 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#
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 DisabledWarning:Stop-Service -Forcealso stops every dependent service, and they are not restarted automatically when you start the parent again. Check dependencies first withGet-Service -Name X -DependentServices.
Dependencies#
# what this service needs
Get-Service -Name 'W3SVC' -RequiredServices
# what depends on this service
Get-Service -Name 'W3SVC' -DependentServicesDetail the cmdlets do not show#
Get-Service omits the logon account and binary path. Use CIM for those:
Get-CimInstance Win32_Service -Filter "Name='W3SVC'" |
Select-Object Name, StartName, PathName, State, StartModeFind every service not running as a built-in account — worth auditing, since these run with domain or local credentials:
Get-CimInstance Win32_Service |
Where-Object { $_.StartName -notmatch '^(LocalSystem|NT AUTHORITY)' } |
Select-Object Name, StartName, State | Format-Table -AutoSizeRecovery actions#
Configure a service to restart itself on failure:
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:
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# remove
Stop-Service MyApp -Force
sc.exe delete MyAppNote: 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#
$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:
| Setting | Effect |
|---|---|
-StartWhenAvailable | Runs a missed task as soon as possible — the equivalent of Persistent=true on a systemd timer |
-ExecutionTimeLimit | Kills a task that hangs, preventing it blocking future runs |
-MultipleInstances IgnoreNew | Skips a run if the previous one is still going |
-RunLevel Highest | Runs elevated; required for most administrative scripts |
-LogonType Password | Runs 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#
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#
# 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:$falseReading the result code#
Get-ScheduledTask | Where-Object { $_.TaskPath -notlike '\Microsoft\*' } |
Get-ScheduledTaskInfo |
Select-Object TaskName, LastRunTime, LastTaskResult, NextRunTime |
Sort-Object LastTaskResult -Descending | Format-Table -AutoSizeLastTaskResult | Meaning |
|---|---|
0 | Success |
1 | Incorrect function — usually the script itself returned an error |
2 | File not found |
10 | Environment incorrect |
267009 | Task is currently running |
267011 | Task has not yet run |
2147942401 | Access denied — check the run-as account's rights |
2147943645 | The 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#
# 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 -AutoSizeThe Task Scheduler operational log is disabled by default on some builds. Enable it before you need it:
wevtutil set-log Microsoft-Windows-TaskScheduler/Operational /enabled:trueVerification#
# 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).LastTaskResultThen 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#
| Symptom | Cause 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 2147943645 | Account not logged on and LogonType is Interactive. Set -LogonType Password. |
| Task runs manually but not on schedule | Trigger not enabled, or the task is set to run only on AC power. Check -AllowStartIfOnBatteries. |
| Task shows success but nothing happened | Script failed internally. Have it write its own log and check that. |
| Script works in a console but fails as a task | Different working directory and environment. Use absolute paths and set -WorkingDirectory. |
| Service will not start, error 1053 | Application is not a real Windows service. Wrap it with NSSM. |
| Service starts then stops immediately | The application exited. Check the Application event log and the app's own log. |
| Service fails after a password change | The run-as account's stored password is stale. Update it in the service properties. |
| Dependent services down after a restart | Stop-Service -Force stopped them. Start them explicitly. |
| Task Scheduler history is empty | Operational log disabled. Enable with wevtutil. |