Scheduled Report of Failed Group Policy Applications by GPO and Computer
Group Policy failures are quiet. A client silently stops applying a GPO, the setting it was enforcing drifts, and nobody notices until something breaks. This guide builds a scheduled report that reads each computer's Group Policy error events, groups them by computer and (where the event names it) by GPO, and drops a CSV and HTML file somewhere you'll actually look.
Before you run this
What it does: The script connects to the System event log of each target computer, pulls the Group Policy Error-level events (provider Microsoft-Windows-GroupPolicy) from the last N days, and writes a report grouped by computer and GPO. It is read-only — it reads event logs and reads GPO names from AD. It changes nothing on the clients and nothing in Group Policy.
Privileges it needs:
- An account that can read the System event log remotely on the targets — local Administrator on each machine, or membership of the local Event Log Readers group (easiest to push via a restricted-group GPO).
- Rights to read GPO objects in the domain (any authenticated user can by default, but confirm).
- The GroupPolicy and ActiveDirectory RSAT modules installed on the box you run this from.
- Registering the scheduled task requires local administrator on the machine that hosts the task.
Test first. Read the script before running it. Run it interactively against a single test computer (or a small test OU) and confirm you get a sane CSV before you point it at the whole fleet or wrap it in a scheduled task. Remote Get-WinEvent depends on the Remote Event Log Management firewall rule being allowed to your management host; if that's blocked, you'll get RPC errors, not results.
Undo: Because this is read-only there is nothing destructive to reverse on the clients. The only artefacts it creates are the scheduled task and the report files — removing both is shown at the end.
What I'm assuming
- You're running this from a domain-joined Windows Server (or admin workstation) with RSAT installed, Windows PowerShell 5.1, and a session that can reach the clients over RPC.
- Targets are Windows 10/11 or Windows Server that log to the standard
Microsoft-Windows-GroupPolicyprovider. - You have a service account (gMSA is better, more on that below) that can read remote logs.
Replace every obvious placeholder: EXAMPLE\svc-gpreport, C:\Scripts, \\server\share\GPReports, and any OU/computer names.
The collection script
Save this as C:\Scripts\Get-GPFailureReport.ps1.
#Requires -Version 5.1
#Requires -Modules GroupPolicy
<#
Collects Group Policy application errors (System log, provider
Microsoft-Windows-GroupPolicy, Level = Error) from domain computers
and writes a report grouped by computer and GPO. Read-only.
#>
[CmdletBinding()]
param(
[int]$DaysBack = 7, # how far back to look
[string]$OutputFolder = 'C:\Reports\GPFailures', # where to write the report
[string]$ComputerListFile # optional: one host per line
)
# --- Build the target list -------------------------------------------------
if ($ComputerListFile) {
$computers = Get-Content -Path $ComputerListFile | Where-Object { $_.Trim() }
}
else {
Import-Module ActiveDirectory
# Start narrow: add -SearchBase 'OU=Test,DC=example,DC=com' for your first run.
$computers = Get-ADComputer -Filter 'Enabled -eq $true' |
Select-Object -ExpandProperty Name
}
$filter = @{
LogName = 'System'
ProviderName = 'Microsoft-Windows-GroupPolicy'
Level = 2 # 2 = Error
StartTime = (Get-Date).AddDays(-$DaysBack)
}
$gpoCache = @{} # cache GUID -> GPO name so we don't hammer AD
$results = foreach ($computer in $computers) {
try {
$events = Get-WinEvent -ComputerName $computer -FilterHashtable $filter -ErrorAction Stop
}
catch {
# "No events were found..." is normal; anything else is worth knowing.
if ($_.Exception.Message -notmatch 'No events were found') {
Write-Warning "Could not read logs on $computer : $($_.Exception.Message)"
}
continue
}
foreach ($e in $events) {
# Some GP error events name the affected GPO by GUID in their text.
# Pull any GUIDs and try to resolve each to a friendly GPO name.
$gpoName = 'See message / not specified'
$guids = [regex]::Matches($e.Message,
'\{[0-9A-Fa-f]{8}-([0-9A-Fa-f]{4}-){3}[0-9A-Fa-f]{12}\}')
foreach ($m in $guids) {
$guid = $m.Value.Trim('{','}')
if (-not $gpoCache.ContainsKey($guid)) {
try { $gpoCache[$guid] = (Get-GPO -Guid $guid -ErrorAction Stop).DisplayName }
catch { $gpoCache[$guid] = $null } # not a GPO GUID (e.g. a CSE GUID)
}
if ($gpoCache[$guid]) { $gpoName = $gpoCache[$guid]; break }
}
[pscustomobject]@{
Computer = $computer
TimeCreated = $e.TimeCreated
EventId = $e.Id
GPO = $gpoName
Message = ($e.Message -replace '\s+', ' ').Trim()
}
}
}
# --- Write the report ------------------------------------------------------
if (-not (Test-Path $OutputFolder)) {
New-Item -Path $OutputFolder -ItemType Directory -Force | Out-Null
}
$stamp = Get-Date -Format 'yyyyMMdd_HHmmss'
$csv = Join-Path $OutputFolder "GPFailures_$stamp.csv"
$html = Join-Path $OutputFolder "GPFailures_$stamp.html"
$sorted = $results | Sort-Object Computer, GPO, TimeCreated
$sorted | Export-Csv -Path $csv -NoTypeInformation -Encoding UTF8
$sorted | ConvertTo-Html -Property Computer, GPO, EventId, TimeCreated, Message `
-Title "Group Policy application failures (last $DaysBack days)" |
Out-File -FilePath $html -Encoding UTF8
Write-Output "Report written to:`n $csv`n $html`nRows: $($sorted.Count)"
An honest word about "by GPO"
The script filters on Error-level events from the Group Policy provider — that reliably captures the failure events you care about (you'll commonly see IDs such as 1085, 1058, 1030, and 1096; see Microsoft Learn's Group Policy event reference for what each means). Grouping those by GPO is the imperfect part: some events name the failing GPO by GUID, which the script resolves to its display name; others name only the client-side extension that failed, not a specific GPO. Those rows show See message / not specified, so always keep the Message column — it's where the real detail lives. Don't treat a blank GPO column as "no GPO involved."
Run it once by hand first
# Single test box, last 3 days, to a local folder:
.\Get-GPFailureReport.ps1 -DaysBack 3 -OutputFolder 'C:\Temp\GPReport' `
-ComputerListFile 'C:\Temp\onehost.txt'
Open the CSV. If you get RPC/access errors, fix the firewall rule and log-read permission before going wider — that's the usual first-run snag.
Schedule it
Run this once, from an elevated PowerShell session on the host that will run the report, to register a daily task. These are standard ScheduledTasks-module cmdlets.
$action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument @'
-NoProfile -ExecutionPolicy Bypass -File "C:\Scripts\Get-GPFailureReport.ps1" -DaysBack 7 -OutputFolder "\\server\share\GPReports"
'@
$trigger = New-ScheduledTaskTrigger -Daily -At 6:00AM
Register-ScheduledTask -TaskName 'GP Failure Report' `
-Action $action -Trigger $trigger `
-User 'EXAMPLE\svc-gpreport' -Password (Read-Host 'Service account password' -AsSecureString | `
ForEach-Object { [Runtime.InteropServices.Marshal]::PtrToStringAuto( `
[Runtime.InteropServices.Marshal]::SecureStringToBSTR($_)) }) `
-RunLevel Highest `
-Description 'Daily report of failed Group Policy applications'
If the password handling above feels fiddly, it is — that's the cost of storing a credential in Task Scheduler. The cleaner approach is a group Managed Service Account (gMSA): you register the task's principal as the gMSA with no stored password and let the OS handle it. I won't walk the full gMSA setup here; see Microsoft Learn for New-ScheduledTaskPrincipal and gMSA-with-Task-Scheduler guidance. The classic schtasks.exe command-line tool also works if you prefer it.
Have it write to a share you monitor. I'm deliberately not emailing from the script: Send-MailMessage is formally deprecated by Microsoft, and I won't build a production job on it. If you want delivery, drop the HTML on an internal page or hand it to a supported mail relay path your org already uses.
Verify it worked
Confirm the task exists and ran cleanly:
Get-ScheduledTask -TaskName 'GP Failure Report'
Start-ScheduledTask -TaskName 'GP Failure Report' # kick it off now
Get-ScheduledTaskInfo -TaskName 'GP Failure Report' # LastTaskResult 0 = success
LastTaskResult of 0 means the task ran. Then open the newest file in your output folder and sanity-check it against a machine you know has a GP problem — run gpresult /r on that client, or locally view Microsoft-Windows-GroupPolicy errors with:
Get-WinEvent -FilterHashtable @{
LogName='System'; ProviderName='Microsoft-Windows-GroupPolicy'; Level=2
} | Select-Object TimeCreated, Id, Message -First 5
If that machine's failures show up in your report, the pipeline is sound.
Undo / roll back
Nothing changed on the clients, so there's nothing to reverse there. To remove what this guide created:
Unregister-ScheduledTask -TaskName 'GP Failure Report' -Confirm:$false
Remove-Item -Path 'C:\Reports\GPFailures' -Recurse # or your share path
Delete C:\Scripts\Get-GPFailureReport.ps1 if you're done with it, and remove the Event Log Readers membership
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
Daily Group Policy Failure Report from the Event Logs
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.
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.




