PowerShell’s event-driven programming capabilities enable you to create responsive, automated systems that react to changes in real-time. Whether monitoring file system changes, responding to WMI events, or scheduling periodic tasks, understanding events and timers is essential for building robust automation solutions.

Understanding PowerShell Event Architecture

PowerShell’s event system allows scripts to subscribe to and respond to events generated by .NET objects, WMI, and other event sources. This asynchronous model enables your scripts to wait for specific conditions without continuously polling.

Working with Events and Timers in PowerShell Automation: Complete Guide with Real-World Examples

Core Event Cmdlets

PowerShell provides four primary cmdlets for event management:

  • Register-ObjectEvent – Subscribe to .NET object events
  • Register-EngineEvent – Create custom PowerShell events
  • Register-WmiEvent – Monitor WMI events (legacy systems)
  • Register-CimIndicationEvent – Monitor CIM events (modern alternative)
  • Get-Event – Retrieve pending events from the event queue
  • Remove-Event – Clear events from the queue
  • Unregister-Event – Remove event subscriptions
  • Wait-Event – Block until an event occurs

File System Monitoring with FileSystemWatcher

One of the most common automation scenarios involves monitoring directories for file changes. The FileSystemWatcher class provides powerful real-time monitoring capabilities.

Basic File Monitoring Example

# Create a FileSystemWatcher object
$watcher = New-Object System.IO.FileSystemWatcher
$watcher.Path = "C:\Temp\Monitor"
$watcher.Filter = "*.txt"
$watcher.IncludeSubdirectories = $true
$watcher.EnableRaisingEvents = $true

# Define the action script block
$action = {
    $path = $Event.SourceEventArgs.FullPath
    $changeType = $Event.SourceEventArgs.ChangeType
    $timestamp = $Event.TimeGenerated
    
    Write-Host "[$timestamp] File $changeType: $path" -ForegroundColor Green
    
    # Log to file
    $logLine = "$timestamp - $changeType - $path"
    Add-Content -Path "C:\Temp\file-changes.log" -Value $logLine
}

# Register event handlers
$handlers = . {
    Register-ObjectEvent -InputObject $watcher -EventName Created -Action $action -SourceIdentifier FileCreated
    Register-ObjectEvent -InputObject $watcher -EventName Changed -Action $action -SourceIdentifier FileChanged
    Register-ObjectEvent -InputObject $watcher -EventName Deleted -Action $action -SourceIdentifier FileDeleted
    Register-ObjectEvent -InputObject $watcher -EventName Renamed -Action $action -SourceIdentifier FileRenamed
}

Write-Host "Monitoring C:\Temp\Monitor for changes. Press Ctrl+C to stop." -ForegroundColor Yellow

# Keep the script running
try {
    while ($true) {
        Start-Sleep -Seconds 1
    }
}
finally {
    # Cleanup
    Get-EventSubscriber | Where-Object { $_.SourceIdentifier -like "File*" } | Unregister-Event
    $watcher.Dispose()
    Write-Host "Monitoring stopped and resources cleaned up." -ForegroundColor Red
}

Expected Output:

Monitoring C:\Temp\Monitor for changes. Press Ctrl+C to stop.
[01/22/2025 15:30:45] File Created: C:\Temp\Monitor\test.txt
[01/22/2025 15:30:52] File Changed: C:\Temp\Monitor\test.txt
[01/22/2025 15:31:10] File Deleted: C:\Temp\Monitor\test.txt

Advanced File Monitoring with Email Notifications

function Start-FileMonitorWithAlert {
    param(
        [string]$Path = "C:\Important\Documents",
        [string[]]$Extensions = @("*.docx", "*.xlsx", "*.pdf"),
        [string]$SmtpServer = "smtp.company.com",
        [string]$To = "[email protected]"
    )
    
    $watcher = New-Object System.IO.FileSystemWatcher
    $watcher.Path = $Path
    $watcher.IncludeSubdirectories = $true
    $watcher.EnableRaisingEvents = $true
    
    # Filter for multiple extensions
    $watcher.NotifyFilter = [System.IO.NotifyFilters]::FileName -bor 
                           [System.IO.NotifyFilters]::LastWrite
    
    $action = {
        param($source, $e)
        
        $details = @{
            FileName = $e.Name
            FullPath = $e.FullPath
            ChangeType = $e.ChangeType
            Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
        }
        
        # Check if file matches our extensions
        $matchesFilter = $false
        foreach ($ext in $using:Extensions) {
            if ($details.FileName -like $ext) {
                $matchesFilter = $true
                break
            }
        }
        
        if ($matchesFilter) {
            $subject = "File Alert: $($details.ChangeType) - $($details.FileName)"
            $body = @"
A monitored file has been $($details.ChangeType.ToString().ToLower()).

File: $($details.FileName)
Full Path: $($details.FullPath)
Time: $($details.Timestamp)
Change Type: $($details.ChangeType)
"@
            
            Send-MailMessage -To $using:To -From "[email protected]" `
                           -Subject $subject -Body $body `
                           -SmtpServer $using:SmtpServer
            
            Write-Host "Alert sent for: $($details.FullPath)" -ForegroundColor Cyan
        }
    }
    
    Register-ObjectEvent -InputObject $watcher -EventName Created -Action $action
    Register-ObjectEvent -InputObject $watcher -EventName Changed -Action $action
    Register-ObjectEvent -InputObject $watcher -EventName Deleted -Action $action
    
    return $watcher
}

# Usage
$monitor = Start-FileMonitorWithAlert -Path "C:\Important\Documents" -Extensions @("*.docx", "*.pdf")

WMI and CIM Event Monitoring

WMI events allow you to monitor system-level changes such as process creation, service state changes, and hardware events.

Working with Events and Timers in PowerShell Automation: Complete Guide with Real-World Examples

Monitoring Process Creation

# Monitor when specific processes are started
$query = "SELECT * FROM __InstanceCreationEvent WITHIN 2 WHERE " +
         "TargetInstance ISA 'Win32_Process' AND " +
         "TargetInstance.Name = 'notepad.exe'"

$action = {
    $process = $Event.SourceEventArgs.NewEvent.TargetInstance
    $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    
    Write-Host "[$timestamp] Notepad started!" -ForegroundColor Yellow
    Write-Host "  Process ID: $($process.ProcessId)" -ForegroundColor Gray
    Write-Host "  Command Line: $($process.CommandLine)" -ForegroundColor Gray
    Write-Host "  User: $($process.GetOwner().User)" -ForegroundColor Gray
    
    # Optional: Take action
    # Stop-Process -Id $process.ProcessId -Force
}

# Register the event
Register-CimIndicationEvent -Query $query -SourceIdentifier ProcessMonitor -Action $action

Write-Host "Monitoring for Notepad launches. Press Ctrl+C to stop." -ForegroundColor Green

# Keep running
try {
    while ($true) { Start-Sleep -Seconds 1 }
}
finally {
    Unregister-Event -SourceIdentifier ProcessMonitor
}

Sample Output:

Monitoring for Notepad launches. Press Ctrl+C to stop.
[2025-01-22 15:45:12] Notepad started!
  Process ID: 8456
  Command Line: "C:\Windows\System32\notepad.exe" C:\test.txt
  User: DOMAIN\username

Service State Monitoring

function Watch-ServiceState {
    param(
        [string[]]$ServiceNames = @("Spooler", "W32Time"),
        [string]$LogPath = "C:\Logs\service-monitor.log"
    )
    
    # Build WMI query for multiple services
    $serviceFilter = ($ServiceNames | ForEach-Object { "Name='$_'" }) -join " OR "
    
    $query = "SELECT * FROM __InstanceModificationEvent WITHIN 5 WHERE " +
             "TargetInstance ISA 'Win32_Service' AND ($serviceFilter)"
    
    $action = {
        $service = $Event.SourceEventArgs.NewEvent.TargetInstance
        $prevService = $Event.SourceEventArgs.NewEvent.PreviousInstance
        
        if ($service.State -ne $prevService.State) {
            $logEntry = @{
                Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
                ServiceName = $service.Name
                DisplayName = $service.DisplayName
                OldState = $prevService.State
                NewState = $service.State
                Status = $service.Status
            }
            
            $message = "$($logEntry.Timestamp) - $($logEntry.DisplayName) changed from " +
                      "$($logEntry.OldState) to $($logEntry.NewState)"
            
            Write-Host $message -ForegroundColor $(if ($service.State -eq "Running") { "Green" } else { "Red" })
            Add-Content -Path $using:LogPath -Value $message
            
            # Alert if critical service stopped
            if ($service.State -eq "Stopped" -and $service.Name -in @("W32Time", "Spooler")) {
                Write-Host "  CRITICAL: Essential service stopped!" -ForegroundColor Red
                # Send alert (email, Teams, etc.)
            }
        }
    }
    
    Register-CimIndicationEvent -Query $query -SourceIdentifier ServiceStateMonitor -Action $action
    Write-Host "Monitoring services: $($ServiceNames -join ', ')" -ForegroundColor Cyan
}

# Start monitoring
Watch-ServiceState -ServiceNames @("Spooler", "W32Time", "WinRM")

Creating Custom PowerShell Events

You can create your own events using Register-EngineEvent and New-Event, enabling inter-script communication and custom workflows.

# Create a custom event system for workflow orchestration
function Initialize-WorkflowEngine {
    # Register event handlers
    $dataProcessed = {
        $data = $Event.MessageData
        Write-Host "Stage 1 Complete: Data processed - $($data.RecordCount) records" -ForegroundColor Green
        
        # Trigger next stage
        New-Event -SourceIdentifier "DataValidation" -MessageData $data
    }
    
    $dataValidated = {
        $data = $Event.MessageData
        Write-Host "Stage 2 Complete: Data validated - $($data.RecordCount) records" -ForegroundColor Green
        
        # Simulate validation
        $data.IsValid = $true
        
        # Trigger final stage
        New-Event -SourceIdentifier "DataExport" -MessageData $data
    }
    
    $dataExported = {
        $data = $Event.MessageData
        Write-Host "Stage 3 Complete: Data exported to $($data.ExportPath)" -ForegroundColor Green
        Write-Host "Workflow finished successfully!" -ForegroundColor Cyan
    }
    
    # Register the workflow stages
    Register-EngineEvent -SourceIdentifier "DataProcessing" -Action $dataProcessed
    Register-EngineEvent -SourceIdentifier "DataValidation" -Action $dataValidated
    Register-EngineEvent -SourceIdentifier "DataExport" -Action $dataExported
}

# Initialize the workflow
Initialize-WorkflowEngine

# Start the workflow by triggering the first event
$workflowData = @{
    RecordCount = 1500
    ExportPath = "C:\Export\data.csv"
    StartTime = Get-Date
}

Write-Host "Starting workflow..." -ForegroundColor Yellow
New-Event -SourceIdentifier "DataProcessing" -MessageData $workflowData

# Wait for all events to process
Start-Sleep -Seconds 2

# Cleanup
Get-EventSubscriber | Where-Object { $_.SourceIdentifier -like "Data*" } | Unregister-Event

Output:

Starting workflow...
Stage 1 Complete: Data processed - 1500 records
Stage 2 Complete: Data validated - 1500 records
Stage 3 Complete: Data exported to C:\Export\data.csv
Workflow finished successfully!

Timer-Based Automation

Timers enable periodic task execution without external schedulers. PowerShell offers multiple approaches for timer-based automation.

Working with Events and Timers in PowerShell Automation: Complete Guide with Real-World Examples

System.Timers.Timer for Periodic Tasks

function Start-PeriodicHealthCheck {
    param(
        [int]$IntervalSeconds = 30,
        [string[]]$Services = @("W32Time", "Spooler"),
        [string]$LogPath = "C:\Logs\health-check.log"
    )
    
    # Create timer
    $timer = New-Object System.Timers.Timer
    $timer.Interval = $IntervalSeconds * 1000  # Convert to milliseconds
    $timer.AutoReset = $true
    
    # Define health check action
    $action = {
        $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
        $results = @()
        
        Write-Host "`n[$timestamp] Running health check..." -ForegroundColor Cyan
        
        foreach ($serviceName in $using:Services) {
            $service = Get-Service -Name $serviceName -ErrorAction SilentlyContinue
            
            if ($service) {
                $status = if ($service.Status -eq "Running") { "✓" } else { "✗" }
                $color = if ($service.Status -eq "Running") { "Green" } else { "Red" }
                
                Write-Host "  $status $($service.DisplayName): $($service.Status)" -ForegroundColor $color
                
                $results += "$timestamp - $($service.Name) - $($service.Status)"
                
                # Auto-remediation
                if ($service.Status -ne "Running" -and $service.StartType -ne "Disabled") {
                    Write-Host "    Attempting to restart $($service.Name)..." -ForegroundColor Yellow
                    try {
                        Start-Service -Name $service.Name -ErrorAction Stop
                        Write-Host "    Successfully restarted $($service.Name)" -ForegroundColor Green
                        $results += "$timestamp - $($service.Name) - RESTARTED"
                    }
                    catch {
                        Write-Host "    Failed to restart: $_" -ForegroundColor Red
                        $results += "$timestamp - $($service.Name) - RESTART_FAILED"
                    }
                }
            }
        }
        
        # Log results
        Add-Content -Path $using:LogPath -Value ($results -join "`n")
        
        # Memory check
        $memory = Get-Counter '\Memory\Available MBytes' | Select-Object -ExpandProperty CounterSamples
        $availMB = [math]::Round($memory.CookedValue, 2)
        Write-Host "  Available Memory: $availMB MB" -ForegroundColor Gray
        
        if ($availMB -lt 1024) {
            Write-Host "  WARNING: Low memory detected!" -ForegroundColor Red
        }
    }
    
    # Register timer event
    Register-ObjectEvent -InputObject $timer -EventName Elapsed -Action $action -SourceIdentifier HealthCheckTimer
    
    # Start timer
    $timer.Start()
    
    Write-Host "Health check started. Checking every $IntervalSeconds seconds." -ForegroundColor Green
    Write-Host "Press Ctrl+C to stop.`n" -ForegroundColor Yellow
    
    return $timer
}

# Start monitoring
$healthTimer = Start-PeriodicHealthCheck -IntervalSeconds 30 -Services @("W32Time", "Spooler", "WinRM")

# Keep running
try {
    while ($true) { Start-Sleep -Seconds 1 }
}
finally {
    $healthTimer.Stop()
    $healthTimer.Dispose()
    Unregister-Event -SourceIdentifier HealthCheckTimer
    Write-Host "`nHealth monitoring stopped." -ForegroundColor Red
}

Sample Output:

Health check started. Checking every 30 seconds.
Press Ctrl+C to stop.

[2025-01-22 16:00:00] Running health check...
  ✓ Windows Time: Running
  ✓ Print Spooler: Running
  ✓ Windows Remote Management (WS-Management): Running
  Available Memory: 8192.45 MB

[2025-01-22 16:00:30] Running health check...
  ✓ Windows Time: Running
  ✗ Print Spooler: Stopped
    Attempting to restart Spooler...
    Successfully restarted Spooler
  ✓ Windows Remote Management (WS-Management): Running
  Available Memory: 8156.32 MB

Countdown Timer with Progress

function Start-CountdownTimer {
    param(
        [int]$Minutes = 5,
        [scriptblock]$OnComplete = { Write-Host "Timer completed!" -ForegroundColor Green },
        [switch]$ShowProgress
    )
    
    $totalSeconds = $Minutes * 60
    $timer = New-Object System.Timers.Timer
    $timer.Interval = 1000  # 1 second
    $timer.AutoReset = $true
    
    $script:elapsedSeconds = 0
    
    $action = {
        $script:elapsedSeconds++
        $remaining = $using:totalSeconds - $script:elapsedSeconds
        
        if ($using:ShowProgress) {
            $percent = [math]::Round(($script:elapsedSeconds / $using:totalSeconds) * 100, 1)
            $timeLeft = [TimeSpan]::FromSeconds($remaining)
            
            Write-Progress -Activity "Countdown Timer" `
                         -Status "Time remaining: $($timeLeft.ToString('mm\:ss'))" `
                         -PercentComplete $percent
        }
        
        if ($script:elapsedSeconds -ge $using:totalSeconds) {
            $timer = $Event.MessageData
            $timer.Stop()
            
            if ($using:ShowProgress) {
                Write-Progress -Activity "Countdown Timer" -Completed
            }
            
            & $using:OnComplete
            
            Unregister-Event -SourceIdentifier CountdownTimer
        }
    }
    
    Register-ObjectEvent -InputObject $timer -EventName Elapsed `
                        -Action $action -MessageData $timer `
                        -SourceIdentifier CountdownTimer
    
    $timer.Start()
    Write-Host "Countdown started: $Minutes minutes" -ForegroundColor Cyan
    
    return $timer
}

# Usage example
$myTimer = Start-CountdownTimer -Minutes 2 -ShowProgress -OnComplete {
    Write-Host "`nCountdown finished! Time to take a break." -ForegroundColor Green
    [System.Media.SystemSounds]::Beep.Play()
}

Advanced Event Patterns

Event Throttling and Debouncing

function Register-ThrottledFileWatcher {
    param(
        [string]$Path,
        [int]$ThrottleSeconds = 5,
        [scriptblock]$Action
    )
    
    $watcher = New-Object System.IO.FileSystemWatcher
    $watcher.Path = $Path
    $watcher.IncludeSubdirectories = $true
    $watcher.EnableRaisingEvents = $true
    
    $script:lastEventTime = @{}
    
    $throttledAction = {
        $filePath = $Event.SourceEventArgs.FullPath
        $currentTime = Get-Date
        
        # Check if enough time has passed since last event for this file
        if (-not $script:lastEventTime.ContainsKey($filePath) -or 
            ($currentTime - $script:lastEventTime[$filePath]).TotalSeconds -ge $using:ThrottleSeconds) {
            
            $script:lastEventTime[$filePath] = $currentTime
            
            # Execute the actual action
            & $using:Action
            
            Write-Host "Event processed for: $filePath" -ForegroundColor Green
        }
        else {
            Write-Host "Event throttled for: $filePath" -ForegroundColor Yellow
        }
    }
    
    Register-ObjectEvent -InputObject $watcher -EventName Changed `
                        -Action $throttledAction -SourceIdentifier ThrottledWatcher
    
    return $watcher
}

# Usage
$watcher = Register-ThrottledFileWatcher -Path "C:\Temp\Monitor" -ThrottleSeconds 5 -Action {
    Write-Host "  Processing file change..." -ForegroundColor Cyan
    # Your actual processing logic here
}

Event Queue Management

function Get-EventQueueStatus {
    $events = Get-Event
    $subscribers = Get-EventSubscriber
    
    Write-Host "`nEvent Subscriber Status:" -ForegroundColor Cyan
    Write-Host ("=" * 80) -ForegroundColor Gray
    
    foreach ($subscriber in $subscribers) {
        $eventCount = ($events | Where-Object { $_.SourceIdentifier -eq $subscriber.SourceIdentifier }).Count
        
        Write-Host "`nSubscriber: $($subscriber.SourceIdentifier)" -ForegroundColor Yellow
        Write-Host "  Event Name: $($subscriber.EventName)"
        Write-Host "  Pending Events: $eventCount"
        Write-Host "  Action: $(if ($subscriber.Action) { 'Yes' } else { 'No' })"
        Write-Host "  Forward: $(if ($subscriber.Forward) { 'Yes' } else { 'No' })"
    }
    
    Write-Host "`nTotal Pending Events: $($events.Count)" -ForegroundColor Cyan
    Write-Host "Active Subscriptions: $($subscribers.Count)" -ForegroundColor Cyan
}

function Clear-EventQueue {
    param([string]$SourceIdentifier)
    
    if ($SourceIdentifier) {
        Get-Event -SourceIdentifier $SourceIdentifier | Remove-Event
        Write-Host "Cleared events for: $SourceIdentifier" -ForegroundColor Green
    }
    else {
        Get-Event | Remove-Event
        Write-Host "Cleared all events from queue" -ForegroundColor Green
    }
}

# Check status
Get-EventQueueStatus

# Clear specific or all events
Clear-EventQueue -SourceIdentifier "FileCreated"

Scheduled Tasks Integration

While events handle real-time responses, scheduled tasks complement automation with time-based execution. PowerShell can create and manage Windows Task Scheduler tasks programmatically.

Working with Events and Timers in PowerShell Automation: Complete Guide with Real-World Examples

function New-MonitoringTask {
    param(
        [string]$TaskName = "FileSystemMonitor",
        [string]$ScriptPath = "C:\Scripts\monitor.ps1",
        [string]$Schedule = "Daily",
        [string]$StartTime = "00:00"
    )
    
    # Define the action
    $action = New-ScheduledTaskAction -Execute "PowerShell.exe" `
        -Argument "-NoProfile -ExecutionPolicy Bypass -File `"$ScriptPath`""
    
    # Define the trigger
    $trigger = switch ($Schedule) {
        "Daily" { 
            New-ScheduledTaskTrigger -Daily -At $StartTime 
        }
        "Startup" { 
            New-ScheduledTaskTrigger -AtStartup 
        }
        "Logon" { 
            New-ScheduledTaskTrigger -AtLogon 
        }
        default { 
            New-ScheduledTaskTrigger -Daily -At $StartTime 
        }
    }
    
    # Define settings
    $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries `
                                             -DontStopIfGoingOnBatteries `
                                             -StartWhenAvailable `
                                             -RunOnlyIfNetworkAvailable:$false
    
    # Register the task
    Register-ScheduledTask -TaskName $TaskName `
                          -Action $action `
                          -Trigger $trigger `
                          -Settings $settings `
                          -Description "Automated file system monitoring" `
                          -RunLevel Highest
    
    Write-Host "Scheduled task '$TaskName' created successfully" -ForegroundColor Green
}

# Create a monitoring task
New-MonitoringTask -TaskName "DailyFileMonitor" -ScriptPath "C:\Scripts\file-monitor.ps1" -Schedule "Daily" -StartTime "02:00"

Error Handling and Cleanup

Robust event-driven automation requires proper error handling and resource cleanup to prevent memory leaks and orphaned event subscriptions.

function Start-RobustEventMonitoring {
    param(
        [string]$Path = "C:\Temp\Monitor",
        [string]$LogPath = "C:\Logs\monitor-errors.log"
    )
    
    try {
        $watcher = New-Object System.IO.FileSystemWatcher
        $watcher.Path = $Path
        $watcher.EnableRaisingEvents = $true
        
        $action = {
            try {
                $file = $Event.SourceEventArgs.FullPath
                Write-Host "Processing: $file" -ForegroundColor Green
                
                # Simulate processing that might fail
                if ($file -match "\.tmp$") {
                    throw "Temporary files are not supported"
                }
                
                # Your actual logic here
            }
            catch {
                $errorMsg = "[$([datetime]::Now)] Error: $_ | File: $($Event.SourceEventArgs.FullPath)"
                Write-Host $errorMsg -ForegroundColor Red
                Add-Content -Path $using:LogPath -Value $errorMsg
                
                # Don't let errors stop the monitoring
            }
        }
        
        Register-ObjectEvent -InputObject $watcher -EventName Created -Action $action -SourceIdentifier RobustMonitor
        
        Write-Host "Monitoring started. Press Ctrl+C to stop." -ForegroundColor Cyan
        
        # Keep running with proper cleanup
        try {
            while ($true) { Start-Sleep -Seconds 1 }
        }
        finally {
            Write-Host "`nCleaning up..." -ForegroundColor Yellow
            
            # Unregister events
            Get-EventSubscriber -SourceIdentifier RobustMonitor -ErrorAction SilentlyContinue | Unregister-Event
            
            # Clear pending events
            Get-Event -SourceIdentifier RobustMonitor -ErrorAction SilentlyContinue | Remove-Event
            
            # Dispose watcher
            if ($watcher) {
                $watcher.EnableRaisingEvents = $false
                $watcher.Dispose()
            }
            
            Write-Host "Cleanup complete" -ForegroundColor Green
        }
    }
    catch {
        Write-Host "Fatal error: $_" -ForegroundColor Red
        throw
    }
}

# Start with error handling
Start-RobustEventMonitoring

Real-World Automation Scenario

Here’s a comprehensive example combining multiple concepts: a log aggregation system that monitors multiple sources, processes events, and sends periodic summaries.

class LogAggregator {
    [System.IO.FileSystemWatcher[]]$Watchers
    [System.Timers.Timer]$ReportTimer
    [System.Collections.ArrayList]$LogEntries
    [string]$ReportPath
    
    LogAggregator([string[]]$paths, [int]$reportIntervalMinutes) {
        $this.LogEntries = [System.Collections.ArrayList]::new()
        $this.Watchers = @()
        $this.ReportPath = "C:\Reports\log-summary.txt"
        
        # Setup watchers for each path
        foreach ($path in $paths) {
            $watcher = [System.IO.FileSystemWatcher]::new()
            $watcher.Path = $path
            $watcher.Filter = "*.log"
            $watcher.EnableRaisingEvents = $true
            
            # Store watcher
            $this.Watchers += $watcher
            
            # Register event
            $eventAction = {
                $file = $Event.SourceEventArgs.FullPath
                $content = Get-Content -Path $file -Tail 10 -ErrorAction SilentlyContinue
                
                foreach ($line in $content) {
                    if ($line -match "ERROR|CRITICAL|WARN") {
                        $entry = @{
                            Timestamp = [datetime]::Now
                            Source = $file
                            Message = $line
                            Level = if ($line -match "ERROR") { "ERROR" } 
                                   elseif ($line -match "CRITICAL") { "CRITICAL" }
                                   else { "WARN" }
                        }
                        
                        $this.LogEntries.Add($entry) | Out-Null
                        
                        Write-Host "[$($entry.Level)] $($entry.Message)" -ForegroundColor $(
                            switch ($entry.Level) {
                                "CRITICAL" { "Red" }
                                "ERROR" { "Yellow" }
                                default { "Gray" }
                            }
                        )
                    }
                }
            }.GetNewClosure()
            
            Register-ObjectEvent -InputObject $watcher -EventName Changed -Action $eventAction
        }
        
        # Setup report timer
        $this.ReportTimer = [System.Timers.Timer]::new()
        $this.ReportTimer.Interval = $reportIntervalMinutes * 60 * 1000
        $this.ReportTimer.AutoReset = $true
        
        $reportAction = {
            $this.GenerateReport()
        }.GetNewClosure()
        
        Register-ObjectEvent -InputObject $this.ReportTimer -EventName Elapsed -Action $reportAction
        $this.ReportTimer.Start()
    }
    
    [void]GenerateReport() {
        $report = @"
Log Aggregation Report - $([datetime]::Now.ToString('yyyy-MM-dd HH:mm:ss'))
$("=" * 80)

Total Entries: $($this.LogEntries.Count)

By Level:
"@
        
        $grouped = $this.LogEntries | Group-Object -Property Level
        foreach ($group in $grouped) {
            $report += "`n  $($group.Name): $($group.Count)"
        }
        
        $report += "`n`nRecent Critical/Errors (Last 10):`n"
        
        $recent = $this.LogEntries | 
                  Where-Object { $_.Level -in @("CRITICAL", "ERROR") } | 
                  Select-Object -Last 10
        
        foreach ($entry in $recent) {
            $report += "`n[$($entry.Timestamp.ToString('HH:mm:ss'))] [$($entry.Level)] $($entry.Message)"
        }
        
        Add-Content -Path $this.ReportPath -Value $report
        Write-Host "`nReport generated: $($this.ReportPath)" -ForegroundColor Cyan
        
        # Clear old entries (keep last 1000)
        if ($this.LogEntries.Count -gt 1000) {
            $this.LogEntries.RemoveRange(0, $this.LogEntries.Count - 1000)
        }
    }
    
    [void]Stop() {
        Write-Host "Stopping log aggregator..." -ForegroundColor Yellow
        
        # Stop timer
        $this.ReportTimer.Stop()
        $this.ReportTimer.Dispose()
        
        # Stop watchers
        foreach ($watcher in $this.Watchers) {
            $watcher.EnableRaisingEvents = $false
            $watcher.Dispose()
        }
        
        # Cleanup events
        Get-EventSubscriber | Unregister-Event
        
        # Generate final report
        $this.GenerateReport()
        
        Write-Host "Log aggregator stopped" -ForegroundColor Green
    }
}

# Usage
$paths = @("C:\Logs\App1", "C:\Logs\App2", "C:\Logs\System")
$aggregator = [LogAggregator]::new($paths, 15)  # 15-minute reports

Write-Host "Log aggregation started. Monitoring $($paths.Count) locations." -ForegroundColor Green
Write-Host "Press Ctrl+C to stop and generate final report.`n" -ForegroundColor Yellow

try {
    while ($true) { Start-Sleep -Seconds 1 }
}
finally {
    $aggregator.Stop()
}

Performance Considerations

When implementing event-driven automation, consider these performance best practices:

  • Event Action Efficiency: Keep action scriptblocks lightweight; offload heavy processing to background jobs
  • Memory Management: Clear processed events regularly with Remove-Event
  • Resource Cleanup: Always dispose of timers and watchers properly
  • Event Throttling: Implement debouncing for high-frequency events
  • Subscription Limits: Monitor active subscriptions; excessive subscriptions impact performance

Monitoring Event System Health

function Get-EventSystemMetrics {
    $subscribers = Get-EventSubscriber
    $events = Get-Event
    
    $metrics = [PSCustomObject]@{
        ActiveSubscriptions = $subscribers.Count
        PendingEvents = $events.Count
        SubscriptionsWithActions = ($subscribers | Where-Object { $_.Action }).Count
        OldestEventAge = if ($events) { 
            ((Get-Date) - ($events | Sort-Object TimeGenerated | Select-Object -First 1).TimeGenerated).TotalMinutes 
        } else { 0 }
        MemoryUsageMB = [math]::Round((Get-Process -Id $PID).WorkingSet64 / 1MB, 2)
    }
    
    Write-Host "`nEvent System Metrics:" -ForegroundColor Cyan
    Write-Host ("=" * 50) -ForegroundColor Gray
    
    $metrics.PSObject.Properties | ForEach-Object {
        Write-Host "$($_.Name): $($_.Value)" -ForegroundColor White
    }
    
    # Health warnings
    if ($metrics.PendingEvents -gt 100) {
        Write-Host "`nWARNING: High pending event count. Consider processing or clearing." -ForegroundColor Red
    }
    
    if ($metrics.OldestEventAge -gt 60) {
        Write-Host "WARNING: Events older than 1 hour detected. Check event processing." -ForegroundColor Red
    }
    
    return $metrics
}

# Check system health periodically
Get-EventSystemMetrics

Best Practices

  • Always Use Try-Finally: Ensure cleanup code runs even if errors occur
  • Unique SourceIdentifiers: Use descriptive, unique names for event subscriptions
  • Log Everything: Maintain detailed logs for debugging event-driven systems
  • Test Cleanup: Verify all resources are released when stopping monitoring
  • Use -Forward Sparingly: Forward events to the event queue only when necessary
  • Implement Timeouts: Add timeouts to prevent indefinite waiting
  • Monitor Resource Usage: Track memory and CPU usage of long-running event handlers
  • Handle Errors Gracefully: Don’t let single event failures stop entire monitoring

Troubleshooting Common Issues

Events Not Firing

# Verify event subscription
Get-EventSubscriber | Format-List *

# Check if events are queued
Get-Event

# Test watcher directly
$watcher = New-Object System.IO.FileSystemWatcher
$watcher.Path = "C:\Temp"
$watcher.EnableRaisingEvents = $true
# Create a test file and monitor console

Memory Leaks

# Monitor process memory
while ($true) {
    $mem = [math]::Round((Get-Process -Id $PID).WorkingSet64 / 1MB, 2)
    Write-Host "Memory: $mem MB | Events: $(Get-Event | Measure-Object).Count | Subscriptions: $(Get-EventSubscriber | Measure-Object).Count"
    Start-Sleep -Seconds 5
}

# Regular cleanup
Get-Event | Where-Object { $_.TimeGenerated -lt (Get-Date).AddHours(-1) } | Remove-Event

Conclusion

PowerShell’s event and timer capabilities provide powerful tools for building responsive, automated systems. By mastering event subscriptions, timer-based execution, and proper resource management, you can create robust automation solutions that react to system changes in real-time while maintaining scheduled operations.

The key to successful event-driven automation lies in understanding the event architecture, implementing proper error handling, and maintaining clean resource management. Whether monitoring file systems, responding to WMI events, or orchestrating complex workflows, these patterns enable you to build scalable, maintainable automation solutions.

Start with simple monitoring scripts, gradually incorporate timers and multiple event sources, and always prioritize proper cleanup and error handling. With practice, you’ll build sophisticated automation systems that handle real-world scenarios efficiently and reliably.