Skip to main content

How to Generate a Scheduled Tasks Report in Batch Script

Scheduled Tasks are the silent engines of a Windows machine, handling everything from system updates and disk cleanups to custom backup scripts. However, over time, a machine can accumulate dozens of forgotten tasks, some created by old software, others by previous administrators, that consume resources or present a security risk. A Batch script can query the Task Scheduler to list every registered task, its status, last result, and run-as account, providing an essential audit for system maintenance and security review.

This guide will explain how to audit scheduled tasks using schtasks and PowerShell.

Method 1: Comprehensive Task Audit Report

This method generates a detailed report of all scheduled tasks with the most important fields for security and operational review.

Implementation

@echo off
setlocal

for /f "delims=" %%t in (
'powershell -NoProfile -Command "Get-Date -Format ''yyyyMMdd_HHmmss''"'
) do set "Stamp=%%t"

set "ReportFile=%~dp0ScheduledTasks_%COMPUTERNAME%_%Stamp%.txt"

:: Verify admin privileges (required for full task visibility)
net session >nul 2>&1
if errorlevel 1 (
echo [WARNING] Running without admin rights. Some system tasks may not be visible. >&2
)

echo [INFO] Generating scheduled tasks audit report...

:: =============================================
:: Report Header
:: =============================================
(
echo ==================================================
echo SCHEDULED TASKS AUDIT REPORT
echo ==================================================
) > "%ReportFile%"

for /f "delims=" %%t in (
'powershell -NoProfile -Command "Get-Date -Format ''yyyy-MM-dd HH:mm:ss''"'
) do echo Generated: %%t >> "%ReportFile%"

echo Computer: %COMPUTERNAME% >> "%ReportFile%"
echo. >> "%ReportFile%"

:: =============================================
:: Section 1: Task Summary (Non-Microsoft Tasks)
:: =============================================
echo [1/3] Collecting custom (non-Microsoft^) tasks...
echo --- [1] CUSTOM / THIRD-PARTY TASKS --- >> "%ReportFile%"
echo. >> "%ReportFile%"

powershell -NoProfile -Command ^
"$tasks = Get-ScheduledTask -ErrorAction SilentlyContinue |" ^
" Where-Object { $_.TaskPath -notlike '\Microsoft\*' };" ^
"if (-not $tasks) {" ^
" Write-Output ' No custom tasks found.';" ^
" exit 0" ^
"};" ^
"$tasks | ForEach-Object {" ^
" $info = $_ | Get-ScheduledTaskInfo -ErrorAction SilentlyContinue;" ^
" $lastRun = if ($info.LastRunTime -and $info.LastRunTime.Year -gt 1999) {" ^
" $info.LastRunTime.ToString('yyyy-MM-dd HH:mm') } else { 'Never' };" ^
" $nextRun = if ($info.NextRunTime -and $info.NextRunTime.Year -gt 1999) {" ^
" $info.NextRunTime.ToString('yyyy-MM-dd HH:mm') } else { 'N/A' };" ^
" $result = if ($info.LastTaskResult -eq 0) { 'Success (0x0)' }" ^
" elseif ($null -eq $info.LastTaskResult) { 'N/A' }" ^
" else { '0x{0:X}' -f $info.LastTaskResult };" ^
" $principal = $_.Principal;" ^
" [PSCustomObject]@{" ^
" Name = $_.TaskName;" ^
" Path = $_.TaskPath;" ^
" State = [string]$_.State;" ^
" LastRun = $lastRun;" ^
" Result = $result;" ^
" NextRun = $nextRun;" ^
" RunAs = $principal.UserId" ^
" }" ^
"} | Format-Table -AutoSize -Wrap" >> "%ReportFile%"

echo. >> "%ReportFile%"

:: =============================================
:: Section 2: Failed Tasks
:: =============================================
echo [2/3] Identifying failed tasks...
echo --- [2] TASKS WITH NON-ZERO LAST RESULT --- >> "%ReportFile%"
echo. >> "%ReportFile%"

powershell -NoProfile -Command ^
"$failed = Get-ScheduledTask -ErrorAction SilentlyContinue |" ^
" ForEach-Object {" ^
" $info = $_ | Get-ScheduledTaskInfo -ErrorAction SilentlyContinue;" ^
" if ($info.LastTaskResult -ne 0 -and $null -ne $info.LastTaskResult -and" ^
" $info.LastRunTime -and $info.LastRunTime.Year -gt 1999) {" ^
" [PSCustomObject]@{" ^
" Name = $_.TaskName;" ^
" Path = $_.TaskPath;" ^
" Result = '0x{0:X}' -f $info.LastTaskResult;" ^
" LastRun = $info.LastRunTime.ToString('yyyy-MM-dd HH:mm');" ^
" RunAs = $_.Principal.UserId" ^
" }" ^
" }" ^
" };" ^
"if ($failed) {" ^
" Write-Output \" Found $(@($failed).Count) task(s) with non-zero results:\";" ^
" Write-Output '';" ^
" $failed | Format-Table -AutoSize -Wrap" ^
"} else {" ^
" Write-Output ' All tasks completed successfully (or have not run yet).'" ^
"}" >> "%ReportFile%"

echo. >> "%ReportFile%"

:: =============================================
:: Section 3: Security Review
:: =============================================
echo [3/3] Security review...
echo --- [3] SECURITY REVIEW --- >> "%ReportFile%"
echo. >> "%ReportFile%"

powershell -NoProfile -Command ^
"$concerns = @();" ^
"$tasks = Get-ScheduledTask -ErrorAction SilentlyContinue;" ^
"foreach ($t in $tasks) {" ^
" $issues = @();" ^
" $principal = $t.Principal;" ^
" if ($principal.UserId -match 'SYSTEM|LocalSystem') {" ^
" if ($t.TaskPath -notlike '\Microsoft\*') {" ^
" $issues += 'Runs as SYSTEM (non-Microsoft task)'" ^
" }" ^
" };" ^
" if ($principal.RunLevel -eq 'Highest' -and $t.TaskPath -notlike '\Microsoft\*') {" ^
" $issues += 'Runs with highest privileges'" ^
" };" ^
" $actions = $t.Actions;" ^
" foreach ($a in $actions) {" ^
" if ($a.Execute -and $a.Execute -match '\\Temp\\|\\AppData\\|\\Downloads\\') {" ^
" $issues += \"Executes from user-writable path: $($a.Execute)\"" ^
" }" ^
" };" ^
" if ($t.State -eq 'Disabled' -and $t.TaskPath -notlike '\Microsoft\*') {" ^
" $info = $t | Get-ScheduledTaskInfo -ErrorAction SilentlyContinue;" ^
" if (-not $info.NextRunTime -or $info.NextRunTime.Year -le 1999) {" ^
" $issues += 'Disabled task (candidate for cleanup)'" ^
" }" ^
" };" ^
" if ($issues.Count -gt 0) {" ^
" $concerns += [PSCustomObject]@{" ^
" Task = $t.TaskName;" ^
" Path = $t.TaskPath;" ^
" Issues = $issues -join '; '" ^
" }" ^
" }" ^
"};" ^
"if ($concerns) {" ^
" Write-Output \" Found $($concerns.Count) task(s) with security concerns:\";" ^
" Write-Output '';" ^
" $concerns | ForEach-Object {" ^
" Write-Output \" [$($_.Task)] $($_.Issues)\";" ^
" Write-Output \" Path: $($_.Path)\"" ^
" }" ^
"} else {" ^
" Write-Output ' No security concerns found.'" ^
"}" >> "%ReportFile%"

echo. >> "%ReportFile%"
echo ================================================== >> "%ReportFile%"

echo [OK] Task audit report saved to: %ReportFile%

endlocal
exit /b 0

What the security review detects:

ConcernWhy It Matters
Non-Microsoft task running as SYSTEMA compromised task running as SYSTEM has complete control over the machine
Task with highest privilegesElevated tasks are high-value targets for privilege escalation
Task executing from user-writable paths (Temp, Downloads)An attacker who can write to these paths can replace the executable
Disabled non-Microsoft tasksForgotten tasks that should be cleaned up

Common Last Result codes:

CodeMeaning
0x0Success
0x1General failure (incorrect parameters, missing file)
0x41301Task is currently running
0x41303Task has not run yet
0x41325Task Scheduler service not available
0x80070002File not found (executable missing)
0x800710E0Operator or administrator refused the request

Method 2: Quick Failed Task Check

For rapid troubleshooting, "which tasks are broken right now?", without generating a full report.

@echo off
setlocal

echo [INFO] Checking for failed scheduled tasks...
echo --------------------------------------------------

powershell -NoProfile -Command ^
"$failed = Get-ScheduledTask |" ^
" ForEach-Object {" ^
" $info = $_ | Get-ScheduledTaskInfo -ErrorAction SilentlyContinue;" ^
" if ($info.LastTaskResult -ne 0 -and $null -ne $info.LastTaskResult -and" ^
" $info.LastRunTime -and $info.LastRunTime.Year -gt 1999) {" ^
" [PSCustomObject]@{" ^
" Task = $_.TaskName;" ^
" Result = '0x{0:X}' -f $info.LastTaskResult;" ^
" LastRun = $info.LastRunTime.ToString('yyyy-MM-dd HH:mm');" ^
" State = [string]$_.State" ^
" }" ^
" }" ^
" };" ^
"if ($failed) {" ^
" Write-Host \"Found $(@($failed).Count) failed task(s):`n\";" ^
" $failed | Format-Table -AutoSize" ^
"} else {" ^
" Write-Host 'All tasks completed successfully.'" ^
"}"

echo --------------------------------------------------

endlocal
exit /b 0

Method 3: CSV Export for Fleet Auditing

For auditing tasks across multiple machines, export key fields to a shared CSV.

@echo off
setlocal

set "CSVFile=%~dp0tasks_inventory_%COMPUTERNAME%.csv"

echo [INFO] Exporting scheduled tasks to CSV...

powershell -NoProfile -Command ^
"$results = Get-ScheduledTask -ErrorAction SilentlyContinue |" ^
" ForEach-Object {" ^
" $info = $_ | Get-ScheduledTaskInfo -ErrorAction SilentlyContinue;" ^
" $lastRun = if ($info.LastRunTime -and $info.LastRunTime.Year -gt 1999) {" ^
" $info.LastRunTime.ToString('yyyy-MM-dd HH:mm') } else { 'Never' };" ^
" $nextRun = if ($info.NextRunTime -and $info.NextRunTime.Year -gt 1999) {" ^
" $info.NextRunTime.ToString('yyyy-MM-dd HH:mm') } else { 'N/A' };" ^
" $action = if ($_.Actions.Count -gt 0) { $_.Actions[0].Execute } else { 'N/A' };" ^
" [PSCustomObject]@{" ^
" Computer = $env:COMPUTERNAME;" ^
" TaskName = $_.TaskName;" ^
" TaskPath = $_.TaskPath;" ^
" State = [string]$_.State;" ^
" LastRun = $lastRun;" ^
" LastResult = '0x{0:X}' -f $info.LastTaskResult;" ^
" NextRun = $nextRun;" ^
" RunAs = $_.Principal.UserId;" ^
" RunLevel = [string]$_.Principal.RunLevel;" ^
" Action = $action" ^
" }" ^
" };" ^
"$results | Export-Csv -Path '%CSVFile%' -NoTypeInformation;" ^
"Write-Host \"Exported $($results.Count) tasks to: %CSVFile%\";" ^
"$custom = ($results | Where-Object { $_.TaskPath -notlike '\Microsoft\*' }).Count;" ^
"$failed = ($results | Where-Object { $_.LastResult -ne '0x0' -and $_.LastRun -ne 'Never' }).Count;" ^
"Write-Host \" Total: $($results.Count) Custom: $custom Failed: $failed\""

if errorlevel 1 (
echo [ERROR] Export failed. >&2
endlocal
exit /b 1
)

endlocal
exit /b 0

Filtering Microsoft tasks in the CSV:

The CSV includes all tasks (including Microsoft's built-in ones) for completeness. When reviewing in Excel:

  • Filter TaskPath to exclude \Microsoft\* to focus on custom/third-party tasks.
  • Sort by LastResult to find failures.
  • Filter by RunAs to find tasks running as SYSTEM or with high privileges.

How to Avoid Common Errors

Wrong Way: Parsing schtasks /v /fo CSV with findstr

:: BROKEN: findstr pattern is invalid, matches wrong fields
schtasks /query /v /fo CSV | findstr /v "0,\"

schtasks /v /fo CSV produces lines with 20+ comma-separated fields. Searching for "0," matches the number 0 in any field (month, day, hour) not just the result code. The trailing \" is invalid findstr syntax.

Correct Way: Use Get-ScheduledTask | Get-ScheduledTaskInfo in PowerShell, which returns the LastTaskResult as a typed integer that can be compared directly.

Wrong Way: Browsing the Tasks XML Folder

:: UNRELIABLE: shows file metadata, not runtime status
dir /s C:\Windows\System32\Tasks

The XML files in the Tasks folder define task configurations but do not contain runtime information (last run time, last result, current state). Only the Task Scheduler service has this data.

Correct Way: Use schtasks /query or Get-ScheduledTask / Get-ScheduledTaskInfo, which communicate with the Task Scheduler service directly.

Problem: Tasks Running Under Stale Credentials

If a task is configured to "Run whether user is logged on or not" with a specific user account, and that account's password is changed, the task will fail with 0x80070005 (Access Denied) on every execution, silently, without any notification.

Solution: After any password change, update the stored credentials for affected tasks:

schtasks /change /tn "TaskName" /rp "NewPassword"

Method 1's failed task detection (Section 2) will surface these failures.

Problem: System Tasks Overwhelm the Report

Windows has hundreds of built-in tasks under \Microsoft\Windows\*. Including them in a security audit produces a massive report where custom tasks are buried.

Solution: Method 1 filters custom tasks using $_.TaskPath -notlike '\Microsoft\*'. The CSV export (Method 3) includes all tasks but is designed for Excel filtering.

Best Practices and Rules

1. Focus on Custom (Non-Microsoft) Tasks

Microsoft's built-in tasks are generally well-tested and necessary. Your security and maintenance focus should be on custom tasks created by administrators, software installers, or scripts, as these are the ones most likely to be misconfigured, forgotten, or malicious.

2. Investigate Non-Zero Last Results

Any task with a non-zero LastTaskResult deserves investigation. Common causes include missing executables (the software was uninstalled), expired credentials, and incorrect working directories.

3. Review SYSTEM-Level Custom Tasks

A non-Microsoft task running as SYSTEM has unrestricted access to the entire machine. Verify that each such task is intentional, necessary, and executing a trusted binary.

4. Check for Tasks Executing from User-Writable Paths

A task running as SYSTEM that executes a script from C:\Users\...\AppData\Temp\ is a privilege escalation vulnerability. Any user who can write to that path can replace the script and gain SYSTEM access.

5. Clean Up Disabled and Orphaned Tasks

Disabled tasks from uninstalled software serve no purpose and clutter the scheduler. After confirming they are no longer needed:

schtasks /delete /tn "\Path\TaskName" /f

6. Run Audits Regularly

Tasks can be created by software installers, Group Policy, and scripts without administrator awareness. Schedule Method 3's CSV export monthly to detect newly created tasks.

Conclusions

Generating a scheduled tasks report transforms task management from a hidden, often-forgotten layer into a visible, auditable system component. By using PowerShell's Get-ScheduledTask and Get-ScheduledTaskInfo for structured data extraction, automated failure detection, and security concern identification, you gain the visibility needed to keep your task scheduler clean, secure, and reliable. This professional approach ensures that automated systems run as expected and that security gaps from forgotten or misconfigured tasks are promptly identified.