
Daily Group Policy Failure Report from the Event Logs
Before you run this
This guide builds a scheduled PowerShell script that reads the Group Policy operational event log, collects the error events from the last 24 hours, and writes them to an HTML file (optionally emailed). It is a reporting tool: it never edits, links, or unlinks a GPO, and it makes no change to any policy or to the events themselves. The only thing it creates on the system is a report file and — if you follow the last section — a scheduled task.
- Privileges: Run the script in an elevated PowerShell session (Run as administrator). Reading the
Microsoft-Windows-GroupPolicy/Operationallog requires membership in the local Administrators or Event Log Readers group; reading it from a remote computer requires that right on the target plus a working remote path (Remote Event Log Management firewall rules / RPC). Registering the scheduled task at the end also requires admin. - Test first: Read the script before you run it. Run it interactively against your own workstation or a single test server first, confirm the HTML report looks right, and only then register it as a scheduled task or point it at a list of computers.
- What changes / undo: The report script itself is safe to re-run — it overwrites (or creates) a dated file and touches nothing else. The scheduled task is a persistent change to the machine; the last section shows exactly how to remove it with
Unregister-ScheduledTask.
Assumptions for this guide: Windows Server 2019/2022 (or Windows 10/11), domain-joined, PowerShell 5.1 (the version shipped in the box), running from an elevated console. The Get-WinEvent, ConvertTo-Html, and *-ScheduledTask* cmdlets used here are all in-box on those versions. On PowerShell 7 the same code runs, but the scheduled-task account and -ExecutionPolicy handling are unchanged so I'll keep to 5.1.
Where the failures live
Modern Group Policy client-side processing logs to the Microsoft-Windows-GroupPolicy/Operational log, with Level 2 = Error and Level 3 = Warning. Filtering by level rather than by a hand-picked list of event IDs is the honest choice here: it catches the whole family of processing errors without me guessing which numeric IDs your OS build emits.
Note that some classic Group Policy errors (the old Userenv-style "cannot access gpt.ini") still land in the System log under the provider Group Policy (event IDs 1030 and 1058 are the well-known ones). If you care about those too, run a second query against LogName = 'System' with ProviderName = 'Group Policy'. This guide focuses on the operational log, which is where current GP processing failures go.
The report script
Save this as C:\Scripts\Get-GPFailureReport.ps1. Replace the obvious placeholders — the output folder, and later the computer list and email details — with your own.
#Requires -Version 5.1
# Reads the GroupPolicy operational log and writes an HTML report of errors.
# Read-only: makes no change to any policy or event.
param(
# Default to the local machine. Pass one or more names to poll remotely.
[string[]]$ComputerName = $env:COMPUTERNAME,
[int]$Days = 1,
[string]$OutputPath = 'C:\Reports\GPReport' # <-- replace with your path
)
$start = (Get-Date).AddDays(-$Days)
$stamp = Get-Date -Format 'yyyy-MM-dd'
$reportFile = Join-Path $OutputPath "GP-Failures-$stamp.html"
if (-not (Test-Path $OutputPath)) {
New-Item -ItemType Directory -Path $OutputPath | Out-Null
}
# Level 2 = Error. Add 3 for Warnings: Level = 2,3
$filter = @{
LogName = 'Microsoft-Windows-GroupPolicy/Operational'
Level = 2
StartTime = $start
}
$results = foreach ($c in $ComputerName) {
try {
Get-WinEvent -ComputerName $c -FilterHashtable $filter -ErrorAction Stop |
Select-Object @{n='Computer';e={$c}}, TimeCreated, Id,
LevelDisplayName, Message
}
catch {
# "No events were found" is not an error worth reporting.
if ($_.Exception.Message -notmatch 'No events were found') {
Write-Warning "$c : $($_.Exception.Message)"
}
}
}
Get-WinEvent throws a terminating "No events were found…" when a machine is clean, so with -ErrorAction Stop the catch swallows that case and only surfaces real problems (unreachable host, access denied).
Now turn the results into a readable file:
$head = @'
<style>
body{font-family:Segoe UI,Arial,sans-serif;font-size:12px}
table{border-collapse:collapse}
th,td{border:1px solid #ccc;padding:4px;text-align:left;vertical-align:top}
th{background:#f2f2f2}
</style>
'@
if ($results) {
$html = $results | Sort-Object TimeCreated -Descending |
ConvertTo-Html -Head $head `
-PreContent "<h2>Group Policy errors since $start</h2>"
} else {
$html = ConvertTo-Html -Head $head `
-Body "<h2>No Group Policy errors since $start</h2>"
}
$html | Out-File -FilePath $reportFile -Encoding UTF8
Write-Host "Report written to $reportFile"
Run it once by hand from an elevated console:
C:\Scripts\Get-GPFailureReport.ps1
Open the HTML file. On a healthy machine you'll get the "No Group Policy errors" line; on one with problems you'll see a sortable table of time, event ID, and the full message.
Polling more than one machine
Pass a list to -ComputerName. Remote reads need the Remote Event Log Management firewall rules enabled on each target and your account holding the right there:
C:\Scripts\Get-GPFailureReport.ps1 -ComputerName 'DC01','FILE01','APP01'
For anything beyond a handful of hosts, feed the list from AD rather than hard-coding it — Get-ADComputer (RSAT / ActiveDirectory module) is the standard source. Keep the collection sequential and simple before you reach for parallel runspaces.
Emailing the report (optional)
If you want the file delivered rather than parked on disk, the simplest in-box option is Send-MailMessage. Be aware Microsoft has officially deprecated Send-MailMessage — it still works and is fine for internal relay to a server you control, but it is not recommended for new work over the public internet. Check Microsoft Learn for the current cmdlet's status before you rely on it. A minimal internal send:
Send-MailMessage -SmtpServer 'smtp.example.com' `
-From 'gp-report@example.com' -To 'admins@example.com' `
-Subject "GP failure report $stamp" `
-Body (Get-Content $reportFile -Raw) -BodyAsHtml
Schedule it to run daily
Register a task that runs the script every morning as SYSTEM. New-ScheduledTaskAction, New-ScheduledTaskTrigger, New-ScheduledTaskPrincipal, and Register-ScheduledTask are all part of the in-box ScheduledTasks module.
$action = New-ScheduledTaskAction -Execute 'powershell.exe' `
-Argument '-NoProfile -ExecutionPolicy Bypass -File "C:\Scripts\Get-GPFailureReport.ps1"'
$trigger = New-ScheduledTaskTrigger -Daily -At 7am
$principal = New-ScheduledTaskPrincipal -UserId 'NT AUTHORITY\SYSTEM' `
-LogonType ServiceAccount -RunLevel Highest
Register-ScheduledTask -TaskName 'Daily GP Failure Report' `
-Action $action -Trigger $trigger -Principal $principal
If you poll remote machines, SYSTEM has no network identity, so either run the task under a dedicated service account with rights on the targets, or keep the task local per-server. For a single-machine or per-server report, SYSTEM is fine.
Verify it worked
Confirm the task exists and its state:
Get-ScheduledTask -TaskName 'Daily GP Failure Report'
Trigger it once on demand and check the result, then confirm the file landed:
Start-ScheduledTask -TaskName 'Daily GP Failure Report'
Get-ScheduledTaskInfo -TaskName 'Daily GP Failure Report' # LastTaskResult 0 = success
Get-ChildItem C:\Reports\GPReport # today's HTML file present?
You can sanity-check the underlying data against the log directly in Event Viewer (Applications and Services Logs → Microsoft → Windows → GroupPolicy → Operational, filter Current Log by Error) — the count there should match your report.
Undo
The report files are just files; delete them if you no longer want them. To remove the schedule entirely:
Unregister-ScheduledTask -TaskName 'Daily GP Failure Report' -Confirm:$false
That leaves the log, the policies, and every past report untouched — you're simply back to where you started before scheduling. For the exact parameter set of any cmdlet above, see Microsoft Learn for Get-WinEvent, ConvertTo-Html, and the *-ScheduledTask* cmdlets.
Runs enterprise networks and security for a living, and writes Shore Up to turn two decades of hands-on Linux, Windows and mail-server work into guides you can actually use.
More about the author →Was this article helpful?
Tap a star — no sign-in needed.
Be the first to rate this article.
Related guides
A Daily Report of Failed RDP Logons Across Your Servers
This guide builds a read-only PowerShell script that pulls failed logon events (Security log Event ID 4625 ) from a list of servers, keeps the ones that look like RDP attempts, and writes them to a dated HTML/CSV report you can review each morning. It optionally emails that report. It creates nothing and deletes nothing on the target servers — it only reads their Security logs.
Automate DHCP Lease Reporting on Windows Server
This guide builds a read-only PowerShell script that enumerates your DHCP scopes, pulls the current IPv4 leases from each, writes them to a timestamped CSV, and (optionally) emails the file. It does not change any DHCP configuration — no scopes, reservations, options, or leases are created, modified, or deleted. The worst it can do is fill a disk with CSVs if you never prune them.
Automate BitLocker Status Reporting Across a Fleet with PowerShell
This guide builds a read-only report: it collects the BitLocker encryption status of every fixed volume on a set of domain computers and writes it to a CSV. It does not enable, disable, or change BitLocker on anything. The only thing it creates on your side is the output file.
Generate a Disk-Space Report Across Servers with PowerShell Remoting
This guide builds a read-only disk-space report. It uses PowerShell remoting ( Invoke-Command over WinRM) to query each server's local fixed disks and returns size, free space, and percent free as one combined table you can export to CSV or HTML. It does not write to, resize, or clean up any disk — it only reads WMI/CIM data.




