Public/eventlog/Get-ScheduledTaskFailure.ps1
|
#Requires -Version 5.1 function Get-ScheduledTaskFailure { <# .SYNOPSIS Report failed scheduled-task starts and actions from Task Scheduler .DESCRIPTION Queries the Microsoft-Windows-TaskScheduler/Operational event log for task-start and action-failure events over a configurable look-back window. Named EventData fields are parsed from event XML so localized message text does not drive extraction. Results retain the task path, action, user, raw and hexadecimal result codes, and the latest known run time. FailureCount is aggregated per task over the full window, results are newest first, and failures on one computer do not stop other targets. .PARAMETER ComputerName One or more computer names to target. Defaults to the local computer. Accepts pipeline input by value and by property name. .PARAMETER Days Look-back window in days. Valid values are 1 through 3650. Defaults to 7. .PARAMETER MaxEvents Maximum number of Task Scheduler events read per machine. Valid values are 1 through 10000. Defaults to 200. .PARAMETER TaskPath Optional case-insensitive filter on the extracted full task path. .PARAMETER TaskName Optional case-insensitive filter on the extracted task name. .PARAMETER Credential Optional PSCredential for authenticating to remote machines. Ignored for local machine queries. .EXAMPLE Get-ScheduledTaskFailure Returns scheduled-task start and action failures from the local computer over the last seven days. .EXAMPLE Get-ScheduledTaskFailure -ComputerName 'SRV01' -Days 30 -TaskPath '\Backup\' Returns failures for tasks under the Backup path on SRV01 over the last 30 days. .EXAMPLE 'SRV01', 'SRV02' | Get-ScheduledTaskFailure -MaxEvents 500 -TaskName 'NightlyBackup' Returns failures for the named task from multiple computers through pipeline input. .OUTPUTS PSWinOps.ScheduledTaskFailure One object per recognized Task Scheduler failure event, newest first, with the number of failures for the task across the selected look-back window. .NOTES Author: Franck SALLET Version: 1.0.0 Last Modified: 2026-09-04 Requires: PowerShell 5.1+ / Windows only Requires: Event Log Readers membership for remote or protected Task Scheduler log access Requires: WinRM enabled on target machines for remote queries Event IDs 101 and 103 represent task/action start failures; 202 and 203 represent action completion or launch failures. Classification uses the event ID and named XML fields, never localized message text. .LINK https://github.com/k9fr4n/PSWinOps .LINK https://learn.microsoft.com/en-us/windows/win32/taskschd/task-scheduler-2-0 #> [CmdletBinding(SupportsShouldProcess = $false, ConfirmImpact = 'None')] [OutputType('PSWinOps.ScheduledTaskFailure')] param( [Parameter(Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [ValidateNotNullOrEmpty()] [Alias('CN', 'Name', 'DNSHostName')] [string[]]$ComputerName = $env:COMPUTERNAME, [Parameter(Mandatory = $false)] [ValidateRange(1, 3650)] [int]$Days = 7, [Parameter(Mandatory = $false)] [ValidateRange(1, 10000)] [int]$MaxEvents = 200, [Parameter(Mandatory = $false)] [string]$TaskPath = '', [Parameter(Mandatory = $false)] [string]$TaskName = '', [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential]$Credential ) begin { Write-Verbose "[$($MyInvocation.MyCommand)] Starting scheduled task failure query" $startTime = (Get-Date).AddDays(-$Days) $scriptBlock = { param( [datetime]$ScanStartTime, [int]$MaxEvts, [string]$TaskPathFilter, [string]$TaskNameFilter ) $failureDefinitions = @{ 101 = 'TaskStartFailure' 103 = 'ActionStartFailure' 202 = 'ActionFailure' 203 = 'ActionLaunchFailure' } $filter = @{ LogName = 'Microsoft-Windows-TaskScheduler/Operational' Id = @(101, 103, 202, 203) StartTime = $ScanStartTime } $eventRecords = @() try { $eventRecords = @(Get-WinEvent -FilterHashtable $filter -MaxEvents $MaxEvts -ErrorAction Stop) } catch { if ($_.Exception.Message -notmatch 'No events were found') { throw } } if ($eventRecords.Count -eq 0) { return } function Get-EventDataMap { param( [Parameter(Mandatory = $true)] [object]$Event ) $map = @{} try { $xml = [xml]$Event.ToXml() foreach ($node in @($xml.SelectNodes("//*[local-name()='Data']"))) { $name = [string]$node.GetAttribute('Name') if ([string]::IsNullOrWhiteSpace($name)) { continue } $value = [string]$node.InnerText if (-not $map.ContainsKey($name) -or [string]::IsNullOrEmpty([string]$map[$name])) { $map[$name] = $value } } } catch { Write-Verbose "Could not parse Task Scheduler event $($Event.Id) as XML: $_" } return $map } function Get-EventField { param( [Parameter(Mandatory = $true)] [hashtable]$Data, [Parameter(Mandatory = $true)] [string[]]$Names ) foreach ($name in $Names) { if ($Data.ContainsKey($name) -and -not [string]::IsNullOrWhiteSpace([string]$Data[$name])) { return [string]$Data[$name] } } return '' } function ConvertTo-ResultCodeHex { param( [Parameter(Mandatory = $false)] [string]$Value ) if ([string]::IsNullOrWhiteSpace($Value)) { return '' } $number = [uint32]0 if ([uint32]::TryParse($Value.Trim(), [Globalization.NumberStyles]::Integer, [Globalization.CultureInfo]::InvariantCulture, [ref]$number)) { return ('0x{0:X8}' -f $number) } $hexValue = $Value.Trim() if ($hexValue.StartsWith('0x', [System.StringComparison]::OrdinalIgnoreCase)) { $hexValue = $hexValue.Substring(2) } if ([uint32]::TryParse($hexValue, [Globalization.NumberStyles]::AllowHexSpecifier, [Globalization.CultureInfo]::InvariantCulture, [ref]$number)) { return ('0x{0:X8}' -f $number) } return '' } function Resolve-TaskIdentity { param( [Parameter(Mandatory = $true)] [AllowEmptyString()] [string]$RawTaskName, [Parameter(Mandatory = $true)] [AllowEmptyString()] [string]$ExplicitTaskPath ) $taskPathValue = $ExplicitTaskPath $taskNameValue = $RawTaskName if (-not [string]::IsNullOrWhiteSpace($RawTaskName)) { $separatorIndex = $RawTaskName.LastIndexOf('\') if ($separatorIndex -ge 0) { $taskPathValue = $RawTaskName.Substring(0, $separatorIndex + 1) $taskNameValue = $RawTaskName.Substring($separatorIndex + 1) } } [PSCustomObject]@{ TaskName = $taskNameValue TaskPath = $taskPathValue } } $rows = [System.Collections.Generic.List[psobject]]::new() foreach ($eventRecord in @($eventRecords | Sort-Object -Property TimeCreated -Descending)) { $eventId = [int]$eventRecord.Id if (-not $failureDefinitions.ContainsKey($eventId)) { continue } $data = Get-EventDataMap -Event $eventRecord $rawTaskName = Get-EventField -Data $data -Names @('TaskName', 'Task') $explicitTaskPath = Get-EventField -Data $data -Names @('TaskPath', 'Path') $taskIdentity = Resolve-TaskIdentity -RawTaskName $rawTaskName -ExplicitTaskPath $explicitTaskPath $actionName = Get-EventField -Data $data -Names @('ActionName', 'Action', 'TaskAction', 'Command') $userName = Get-EventField -Data $data -Names @('UserName', 'User', 'UserContext', 'PrincipalName', 'AccountName') $resultCode = Get-EventField -Data $data -Names @('ResultCode', 'ErrorValue', 'ErrorCode', 'HRESULT', 'Status') $lastRunTimeValue = Get-EventField -Data $data -Names @('LastRunTime', 'RunTime', 'StartTime') $lastRunTime = $eventRecord.TimeCreated if (-not [string]::IsNullOrWhiteSpace($lastRunTimeValue)) { $parsedLastRunTime = [datetime]::MinValue if ([datetime]::TryParse($lastRunTimeValue, [Globalization.CultureInfo]::InvariantCulture, [Globalization.DateTimeStyles]::AssumeLocal, [ref]$parsedLastRunTime)) { $lastRunTime = $parsedLastRunTime } } $rows.Add([PSCustomObject]@{ EventTime = $eventRecord.TimeCreated EventId = $eventId TaskName = $taskIdentity.TaskName TaskPath = $taskIdentity.TaskPath ActionName = $actionName UserName = $userName ResultCode = $resultCode ResultCodeHex = ConvertTo-ResultCodeHex -Value $resultCode FailureReason = $failureDefinitions[$eventId] LastRunTime = $lastRunTime Message = [string]$eventRecord.Message }) } $failureTally = @{} foreach ($row in $rows) { $taskKey = '{0}|{1}' -f ([string]$row.TaskPath).ToLowerInvariant(), ([string]$row.TaskName).ToLowerInvariant() if ($failureTally.ContainsKey($taskKey)) { $failureTally[$taskKey]++ } else { $failureTally[$taskKey] = 1 } } foreach ($row in ($rows | Sort-Object -Property EventTime -Descending)) { if (-not [string]::IsNullOrWhiteSpace($TaskPathFilter) -and $row.TaskPath -ine $TaskPathFilter) { continue } if (-not [string]::IsNullOrWhiteSpace($TaskNameFilter) -and $row.TaskName -ine $TaskNameFilter) { continue } $taskKey = '{0}|{1}' -f ([string]$row.TaskPath).ToLowerInvariant(), ([string]$row.TaskName).ToLowerInvariant() [PSCustomObject]@{ PSTypeName = 'PSWinOps.ScheduledTaskFailure' ComputerName = $env:COMPUTERNAME EventTime = $row.EventTime.ToString('o') EventId = $row.EventId TaskName = $row.TaskName TaskPath = $row.TaskPath ActionName = $row.ActionName UserName = $row.UserName ResultCode = $row.ResultCode ResultCodeHex = $row.ResultCodeHex FailureReason = $row.FailureReason LastRunTime = $row.LastRunTime.ToString('o') FailureCount = $failureTally[$taskKey] Message = $row.Message Timestamp = Get-Date -Format 'o' } } } } process { foreach ($targetComputer in $ComputerName) { try { Write-Verbose "[$($MyInvocation.MyCommand)] Querying scheduled task failures on '$targetComputer'" Invoke-RemoteOrLocal -ComputerName $targetComputer -Credential $Credential ` -ScriptBlock $scriptBlock ` -ArgumentList @($startTime, $MaxEvents, $TaskPath, $TaskName) } catch { Write-Error "[$($MyInvocation.MyCommand)] Failed on '$targetComputer': $_" } } } end { Write-Verbose "[$($MyInvocation.MyCommand)] Completed scheduled task failure query" } } |