PowerShell WMI Event Subscription: React to System Events

Polling is the lazy approach to system monitoring: your script wakes up every 30 seconds, checks a condition, and goes back to sleep. It burns CPU, introduces latency, and misses events that start and finish between polls. WMI event subscriptions flip the model — you register interest in an event class and Windows calls your code the instant the condition is met. A process starts, a service stops, a USB drive is inserted, a registry key changes: all of these can drive reactive automation without a polling loop in sight.
Quick Answer
Use Register-CimIndicationEvent (preferred in PowerShell 5.1+) or Register-WmiEvent to subscribe to a WMI event class. Supply an -Action scriptblock to execute code when the event fires, and Unregister-Event to clean up when done.
Register-WmiEvent and Register-CimIndicationEvent Compared
Register-WmiEvent uses the legacy DCOM-based WMI stack and is available in Windows PowerShell 5.1. Register-CimIndicationEvent uses the modern CIM/WinRM stack introduced in PowerShell 3 and is the preferred choice because CIM is maintained and supports both local and remote subscriptions through CIM sessions. Both cmdlets return a PSEventJob object and share the same eventing infrastructure internally — the choice affects the underlying transport, not the programming model.
# Modern approach — preferred
$subscription = Register-CimIndicationEvent `
-ClassName "Win32_ProcessStartTrace" `
-SourceIdentifier "ProcessStarted" `
-Action {
$proc = $Event.SourceEventArgs.NewEvent
Write-Host "Process started: $($proc.ProcessName) PID=$($proc.ProcessId)"
}
# Legacy approach — Windows PowerShell only
$subscription = Register-WmiEvent `
-Class "Win32_ProcessStartTrace" `
-SourceIdentifier "ProcessStarted-Legacy" `
-Action { Write-Host "Process: $($Event.SourceEventArgs.NewEvent.ProcessName)" }
Subscribing to Process Creation Events with Win32_ProcessStartTrace
Win32_ProcessStartTrace fires immediately when any new process is created. You can narrow the scope using the -Query parameter with a WQL filter to avoid processing every process start on a busy system.
# Fire only when cmd.exe or powershell.exe starts
$query = "SELECT * FROM Win32_ProcessStartTrace WHERE ProcessName = 'cmd.exe' OR ProcessName = 'powershell.exe'"
Register-CimIndicationEvent `
-Query $query `
-SourceIdentifier "SuspiciousProcessAlert" `
-Action {
$proc = $Event.SourceEventArgs.NewEvent
$logEntry = "[{0}] ALERT: {1} started by PID {2}" -f `
(Get-Date -Format "u"), $proc.ProcessName, $proc.ParentProcessId
Add-Content -Path "C:\Logs\process-alerts.log" -Value $logEntry
}
The $Event automatic variable is available inside the -Action scriptblock and contains the full event data including SourceEventArgs.NewEvent which holds the WMI instance.
Reacting to Service State Changes
Subscribe to Win32_ServiceStopEvent or use a __InstanceModificationEvent query against Win32_Service to detect state transitions. The modification event is more flexible because it fires on any property change, letting you filter on TargetInstance.State.
# Alert when the Windows Update service stops unexpectedly
$svcQuery = "SELECT * FROM __InstanceModificationEvent WITHIN 5 " +
"WHERE TargetInstance ISA 'Win32_Service' " +
"AND TargetInstance.Name = 'wuauserv' " +
"AND TargetInstance.State = 'Stopped'"
Register-CimIndicationEvent `
-Query $svcQuery `
-SourceIdentifier "WuauservStopped" `
-Action {
$svc = $Event.SourceEventArgs.NewEvent.TargetInstance
Write-Warning "Service '$($svc.Name)' stopped at $(Get-Date)."
# Optionally restart it
Start-Service -Name $svc.Name -ErrorAction SilentlyContinue
}
The WITHIN 5 clause tells WMI to poll the Win32_Service instance every 5 seconds for changes. Lower the value for faster detection at the cost of slightly higher system load.
Using -Action ScriptBlock to Run Code on Event
The -Action scriptblock runs in a separate runspace from the parent session. This means it cannot access variables defined in the main script scope directly — it is completely isolated. Use $using: syntax is not available here; instead, pass data via the MessageData parameter and access it through $Event.MessageData inside the action.
# Pass configuration data to the action via MessageData
$config = @{ LogPath = "C:\Logs\events.log"; AlertEmail = "[email protected]" }
Register-CimIndicationEvent `
-ClassName "Win32_ProcessStartTrace" `
-SourceIdentifier "ProcessLog" `
-MessageData $config `
-Action {
$logPath = $Event.MessageData.LogPath
$procName = $Event.SourceEventArgs.NewEvent.ProcessName
Add-Content -Path $logPath -Value "$procName started at $(Get-Date -Format 'u')"
}
Persistent Subscriptions That Survive Reboots
In-memory subscriptions registered with Register-CimIndicationEvent or Register-WmiEvent live only as long as the PowerShell session. For persistent subscriptions that survive reboots, use the permanent WMI subscription model: create a __EventFilter, a CommandLineEventConsumer (or another consumer class), and a __FilterToConsumerBinding in the root\subscription namespace using New-CimInstance. This is a more complex setup but runs without any PowerShell session.
Cleaning Up Subscriptions with Unregister-Event
In-memory subscriptions accumulate in the session’s event queue and consume memory. Always unregister them when they are no longer needed. Use Get-EventSubscriber to list all active subscriptions by their SourceIdentifier, then call Unregister-Event with the matching identifier.
# List all active subscriptions
Get-EventSubscriber | Select-Object SourceIdentifier, Id, Action
# Remove a specific subscription
Unregister-Event -SourceIdentifier "ProcessStarted"
# Remove all subscriptions in the session
Get-EventSubscriber | Unregister-Event
Common Errors
- In-memory subscriptions lost after session ends: Any subscription registered with
Register-CimIndicationEventorRegister-WmiEventis tied to the PowerShell runspace that created it. When the session closes, the subscription disappears. Use the permanent WMI subscription model inroot\subscriptionif you need the subscription to survive reboots and session termination. - Action scriptblock cannot access outer variables: The action runs in an isolated runspace. Attempts to reference
$myLocalVariabledefined in the parent session will return$nullsilently. Pass all required data through the-MessageDataparameter and retrieve it via$Event.MessageDatainside the action block.
Related Cmdlets / See Also
Wrapping Up
WMI event subscriptions replace polling loops with instant, event-driven reactions. Use Register-CimIndicationEvent for modern in-session subscriptions, pass data to action blocks via MessageData, and always call Unregister-Event to clean up. For production monitoring that survives reboots, build a permanent subscription in root\subscription.


