System uptime and unexpected reboots are core indicators of server and workstation health in enterprise IT environments. Whether it’s a scheduled OS upgrade or a critical error that triggered a restart, understanding the reasons behind system reboots is essential for incident response, system diagnostics, and compliance audits. This is where automation meets accountability—delivering insights without requiring manual log inspection.
Why Reboot Logging Matters in Managed Environments
For Managed Service Providers (MSPs) and internal IT departments alike, tracking reboot reasons isn’t just helpful—it’s critical. A sudden system shutdown could be the sign of hardware failure, a software conflict, or an unapproved patch deployment. In high-availability setups, knowing the why behind a reboot directly supports uptime SLAs and disaster recovery strategies.
The provided PowerShell script automates the process of retrieving reboot reasons by parsing the Windows Event Log for specific event IDs (6008 and 1074), formatting the results, and optionally saving them to custom fields in NinjaOne—a widely used IT management platform. It’s a practical example of bridging system telemetry with actionable visibility.
The Script
#Requires -Version 5.1 <# .SYNOPSIS Retrieve the previous 14 reboot reasons and optionally save them to a WYSIWYG custom field, or save only the latest reboot reason to a text custom field. .DESCRIPTION Retrieve the previous 14 reboot reasons and optionally save them to a WYSIWYG custom field, or save only the latest reboot reason to a text custom field. By using this script, you indicate your acceptance of the following legal terms as well as our Terms of Use at https://www.ninjaone.com/terms-of-use. Ownership Rights: NinjaOne owns and will continue to own all right, title, and interest in and to the script (including the copyright). NinjaOne is giving you a limited license to use the script in accordance with these legal terms. Use Limitation: You may only use the script for your legitimate personal or internal business purposes, and you may not share the script with another party. Republication Prohibition: Under no circumstances are you permitted to re-publish the script in any script library or website belonging to or under the control of any other software provider. Warranty Disclaimer: The script is provided “as is” and “as available”, without warranty of any kind. NinjaOne makes no promise or guarantee that the script will be free from defects or that it will meet your specific needs or expectations. Assumption of Risk: Your use of the script is at your own risk. You acknowledge that there are certain inherent risks in using the script, and you understand and assume each of those risks. Waiver and Release: You will not hold NinjaOne responsible for any adverse or unintended consequences resulting from your use of the script, and you waive any legal or equitable rights or remedies you may have against NinjaOne relating to your use of the script. EULA: If you are a NinjaOne customer, your use of the script is subject to the End User License Agreement applicable to you (EULA). .EXAMPLE (No Parameters) Checking the event logs for possible reboot reasons. [Warning] Only the previous 5 reboot reasons were found. Translating the SIDs provided to usernames. ### Past Reboots ### FormattedDate : 12/18/2024 11:20 AM Id : 6008 User : N/A Message : The previous system shutdown at 11:20:06 AM on 12/18/2024 was unexpected. FormattedDate : 12/18/2024 11:01 AM Id : 1074 User : NT AUTHORITY\SYSTEM Message : The process C:\Windows\servicing\TrustedInstaller.exe (SRV16-TEST) has initiated the restart of computer SRV16-TEST on behalf of user NT AUTHORITY\SYSTEM for the following reason: Operating System: Upgrade (Planned) Reason Code: 0x80020003 Shutdown Type: restart Comment: FormattedDate : 12/18/2024 10:57 AM Id : 6008 User : N/A Message : The previous system shutdown at 10:56:15 AM on 12/18/2024 was unexpected. FormattedDate : 12/16/2024 5:25 PM Id : 1074 User : SRV16-TEST\Administrator Message : The process C:\Windows\system32\wbem\wmiprvse.exe (SRV16-TEST) has initiated the shutdown of computer SRV16-TEST on behalf of user SRV16-TEST\Administrator for the following reason: No title for this reason could be found Reason Code: 0x80070015 Shutdown Type: shutdown Comment: FormattedDate : 12/16/2024 5:22 PM Id : 1074 User : NT AUTHORITY\SYSTEM Message : The process C:\Windows\system32\winlogon.exe (MINWINPC) has initiated the restart of computer WIN-2686BKBDV33 on behalf of user NT AUTHORITY\SYSTEM for the following reason: Operating System: Upgrade (Planned) Reason Code: 0x80020003 Shutdown Type: restart Comment: PARAMETER: -TextCustomField "ExampleInput" Optionally save the latest reboot reason to a text custom field of your choosing. PARAMETER: -WysiwygCustomField "ReplaceMeWithAnyMultilineCustomField" Optionally save the previous 14 reboot reasons to a WYSIWYG custom field of your choosing. .NOTES Minimum OS Architecture Supported: Windows 10, Windows Server 2016 Release Notes: Initial Release #> [CmdletBinding()] param ( [Parameter()] [String]$TextCustomField, [Parameter()] [String]$WysiwygCustomField ) begin { # If script form variables are used, replace the command line parameters with their value. if ($env:lastRebootReasonTextCustomField -and $env:lastRebootReasonTextCustomField -notlike "null") { $TextCustomField = $env:lastRebootReasonTextCustomField } if ($env:last14RebootReasonsWysiwygCustomField -and $env:last14RebootReasonsWysiwygCustomField -notlike "null") { $WysiwygCustomField = $env:last14RebootReasonsWysiwygCustomField } # Check if a text custom field value was provided. if($TextCustomField){ # Remove any leading or trailing whitespace. $TextCustomField = $TextCustomField.Trim() # If, after trimming, the text custom field is empty, print an error and exit. if(!$TextCustomField){ Write-Host -Object "[Error] Please enter a valid text custom field." exit 1 } } # Check if a WYSIWYG custom field value was provided. if($WysiwygCustomField){ # Remove any leading or trailing whitespace. $WysiwygCustomField = $WysiwygCustomField.Trim() # If, after trimming, the WYSIWYG custom field is empty, print an error and exit. if(!$WysiwygCustomField){ Write-Host -Object "[Error] Please enter a valid WYSIWYG custom field." exit 1 } } function Set-NinjaProperty { [CmdletBinding()] Param( [Parameter(Mandatory = $True)] [String]$Name, [Parameter()] [String]$Type, [Parameter(Mandatory = $True, ValueFromPipeline = $True)] $Value, [Parameter()] [String]$DocumentName, [Parameter()] [Switch]$Piped ) # Remove the non-breaking space character if ($Type -eq "WYSIWYG") { $Value = $Value -replace ' ', ' ' } # Measure the number of characters in the provided value $Characters = $Value | ConvertTo-Json | Measure-Object -Character | Select-Object -ExpandProperty Characters # Throw an error if the value exceeds the character limit of 200,000 characters if ($Piped -and $Characters -ge 200000) { throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 200,000 characters.") } if (!$Piped -and $Characters -ge 45000) { throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 45,000 characters.") } # Initialize a hashtable for additional documentation parameters $DocumentationParams = @{} # If a document name is provided, add it to the documentation parameters if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } # Define a list of valid field types $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" # Warn the user if the provided type is not valid if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type. Please check here for valid types: https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } # Define types that require options to be retrieved $NeedsOptions = "Dropdown" # If the property is being set in a document or field and the type needs options, retrieve them if ($DocumentName) { if ($NeedsOptions -contains $Type) { $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 } } else { if ($NeedsOptions -contains $Type) { $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 } } # Throw an error if there was an issue retrieving the property options if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } # Process the property value based on its type switch ($Type) { "Checkbox" { # Convert the value to a boolean for Checkbox type $NinjaValue = [System.Convert]::ToBoolean($Value) } "Date or Date Time" { # Convert the value to a Unix timestamp for Date or Date Time type $Date = (Get-Date $Value).ToUniversalTime() $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date $NinjaValue = $TimeSpan.TotalSeconds } "Dropdown" { # Convert the dropdown value to its corresponding GUID $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID # Throw an error if the value is not present in the dropdown options if (!($Selection)) { throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown options.") } $NinjaValue = $Selection } default { # For other types, use the value as is $NinjaValue = $Value } } # Set the property value in the document if a document name is provided if ($DocumentName) { $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 } else { try { # Otherwise, set the standard property value if ($Piped) { $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 } else { $CustomField = Ninja-Property-Set -Name $Name -Value $NinjaValue 2>&1 } } catch { Write-Host -Object "[Error] Failed to set custom field." throw $_.Exception.Message } } # Throw an error if setting the property failed if ($CustomField.Exception) { throw $CustomField } } function Test-IsElevated { $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() $p = New-Object System.Security.Principal.WindowsPrincipal($id) $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) } if (!$ExitCode) { $ExitCode = 0 } } process { # Check if the current user is elevated (running as Administrator). if (!(Test-IsElevated)) { Write-Host -Object "[Error] Access Denied. Please run with Administrator privileges." exit 1 } # Inform the user that the script is checking the event logs for possible reboot reasons. Write-Host -Object "Checking the event logs for possible reboot reasons." # Create an XML query to filter for certain event IDs (6008 and 1074) within the System event log. [xml]$EventViewerXML = "<QueryList> <Query Id='0' Path='System'> <Select Path='System'>*[System[Provider[@Name='Microsoft-Windows-Eventlog' or @Name='EventLog' or @Name='Microsoft-Windows-Kernel-General'] and(EventID=6008)]]</Select> <Select Path='System'>*[System[(EventID=1074)]]</Select> </Query> </QueryList>" try { # Retrieve up to 14 recent matching events from the System log using the XML filter. # Stop on errors so exceptions can be caught. $MatchingEvents = Get-WinEvent -FilterXml $EventViewerXML -MaxEvents 14 -ErrorAction Stop } catch { Write-Host -Object "[Error] $($_.Exception.Message)" Write-Host -Object "[Error] Failed to search event log." exit 1 } # Count how many events were retrieved. $MatchingEventCount = $MatchingEvents | Measure-Object | Select-Object -ExpandProperty Count # If we found some events, but fewer than 14, warn the user that we have a limited number of reboot reasons. if ($MatchingEventCount -gt 0 -and $MatchingEventCount -lt 14) { Write-Host -Object "[Warning] Only the previous $MatchingEventCount reboot reasons were found." } # If no events were found, print an error and set the exit code to 1. if ($MatchingEventCount -lt 1) { Write-Host -Object "[Error] No reboot reasons were found." $ExitCode = 1 } # Inform the user that SIDs are being translated to usernames. Write-Host -Object "Translating the SIDs provided to usernames." # Retrieve user profile information from the registry for SID-to-username mapping. try { $AllUserProfiles = Get-ItemProperty "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\*" -ErrorAction Stop } catch { Write-Host -Object "[Error] $($_.Exception.Message)" Write-Host -Object "[Error] Failed to find any user profiles." $ExitCode = 1 } # Format the retrieved events into a custom object with a friendly date format, ID, user, and message. $FormattedResults = $MatchingEvents | Select-Object -Property "TimeCreated", "Id", "UserId", "Message" | ForEach-Object { $Username = $null if ($_.UserId) { try { # Temporarily stop on errors to ensure exceptions are caught for SID translation. $ErrorActionPreference = "Stop" $SID = New-Object System.Security.Principal.SecurityIdentifier($_.UserId) # Attempt to translate the SID to an NT account (username). $Username = $SID.Translate([System.Security.Principal.NTAccount]) | Select-Object -ExpandProperty Value -ErrorAction SilentlyContinue } catch { # If direct SID translation fails, look up the profile in the registry. $Sid = $_.UserId $ProfileKey = $AllUserProfiles | Where-Object { $_.PSChildName -eq $Sid } | Select-Object @{Name = "Username"; Expression = { "$($_.ProfileImagePath | Split-Path -Leaf -ErrorAction SilentlyContinue)" } } -ErrorAction SilentlyContinue Write-Host -Object "[Error] Failed to gather complete profile information for the SID '$($_.UserId)'." Write-Host -Object "[Error] $($_.Exception.Message)" $ExitCode = 1 } } else { # If there's no UserId, set the username to "N/A". $Username = "N/A" } # If no username was found directly, but we have a ProfileKey, use that as an approximation. if (!$Username -and $ProfileKey) { Write-Host -Object "Approximating the username for the SID '$Sid'." $Username = $ProfileKey } elseif (!$Username) { # If still no username is found, fall back to the SID itself. $Username = $SID } # Create a custom object with formatted data. [PSCustomObject]@{ TimeCreated = $_.TimeCreated FormattedDate = "$($_.TimeCreated.ToShortDateString()) $($_.TimeCreated.ToShortTimeString())" Id = $_.Id User = $Username Message = $_.Message } # Restore the error action preference to continue. $ErrorActionPreference = "Continue" } # If either text or WYSIWYG custom fields were provided, print a blank line for readability. if ($TextCustomField -or $WysiwygCustomField) { Write-Host -Object "" } # If a text custom field is specified, set it to display the most recent event (truncated if too long). if ($TextCustomField) { # Get the most recent event (first in the list). $MostRecentEvent = $FormattedResults | Select-Object -First 1 # If the message is longer than 100 characters, truncate it and add ellipsis. if (($MostRecentEvent.Message -replace '\r?\n', ' ').Length -gt 100) { $MostRecentEvent = $MostRecentEvent | Select-Object -Property FormattedDate, Id, User, @{Name = "Message"; Expression = { "$($_.Message.Substring(0,97))..." } } } # Construct the value to set in the text custom field. $TextCustomFieldValue = "$($MostRecentEvent.FormattedDate) | EventID: $($MostRecentEvent.Id) | Username: $($MostRecentEvent.User) | Reason: $($MostRecentEvent.Message -replace '\r?\n', ' ')" # Attempt to set the specified text custom field. try { Write-Host "Attempting to set the Custom Field '$TextCustomField'." Set-NinjaProperty -Name $TextCustomField -Value $TextCustomFieldValue Write-Host "Successfully set the Custom Field '$TextCustomField'!" } catch { Write-Host "[Error] $($_.Exception.Message)" $ExitCode = 1 } } # If a WYSIWYG custom field is specified, construct an HTML table of the results and set the field. if ($WysiwygCustomField) { # Convert the formatted results to an HTML fragment. $HTMLTable = $FormattedResults | Select-Object -Property FormattedDate, Id, User, Message | ConvertTo-Html -Fragment # Bold the table headers and adjust column widths. $HTMLTable = $HTMLTable -replace '<th>', '<th><b>' -replace '</th>', '</b></th>' $HTMLTable = $HTMLTable -replace '<th><b>FormattedDate', "<th style='width: 12em'><b>Date" -replace '<th><b>Id', "<th style='width: 6em'><b>Event Id" $HTMLTable = $HTMLTable -replace '<th><b>User', "<th style='width: 20em'><b>Username" -replace '<th><b>Message', "<th><b>Reason" # Attempt to set the WYSIWYG custom field with the constructed HTML table. try { Write-Host "Attempting to set the Custom Field '$WysiwygCustomField'." Set-NinjaProperty -Name $WysiwygCustomField -Value $HTMLTable Write-Host "Successfully set the Custom Field '$WysiwygCustomField'!" } catch { Write-Host "[Error] $($_.Exception.Message)" $ExitCode = 1 } } # Print a heading for past reboots and then display the formatted results as a list for reference. Write-Host -Object "`n### Past Reboots ###" ($FormattedResults | Format-List -Property FormattedDate, Id, User, UserId, Message | Out-String).Trim() | Write-Host # Exit with the previously set exit code (defaulting to 0 if not set). exit $ExitCode } end { }
Dissecting the Script’s Workflow
Let’s break down how this PowerShell script operates:
1. Parameter Handling and Environment Detection
The script supports two optional parameters:
- -TextCustomField to save the most recent reboot reason to a text field.
- -WysiwygCustomField to save the last 14 reboot reasons to an HTML-formatted WYSIWYG field.
It also detects values set via environment variables, offering flexibility whether it’s run interactively or via automation.
2. Administrator Rights Check
Before doing any heavy lifting, the script verifies that it’s running with elevated privileges. Without Administrator access, querying system logs will fail.
3. Event Log Querying
It uses a well-formed XML query to retrieve events with ID 6008 (unexpected shutdowns) and 1074 (user/system-initiated reboots). This approach ensures accuracy and performance compared to broader log scans.
4. SID to Username Mapping
The script cross-references user SIDs found in logs with local profile registry entries to resolve them into human-readable usernames—an often overlooked but essential feature for audit trails.
5. Data Formatting
Each reboot event is transformed into a standardized object with:
- FormattedDate
- Id
- User
- Message
6. Custom Field Output (Optional)
If specified, the latest reboot reason is sent to a text custom field. Alternatively, all 14 entries are presented in a neat HTML table for WYSIWYG display, making them suitable for use in NinjaOne reports or dashboards.
7. User Feedback and Output
The script provides real-time feedback for transparency, including warning messages if fewer than 14 events are found or if errors occur during SID resolution.
Real-World Application in a Managed IT Setting
Scenario: A mid-sized law firm experiences unscheduled server reboots during peak billing hours.
Solution: Their MSP deploys this script across all Windows servers via NinjaOne. For each device, the last 14 reboot reasons are logged to a WYSIWYG custom field and surfaced in a weekly health report.
Outcome: It turns out the reboots correlate with a third-party backup agent initiating system-level restarts. The MSP works with the vendor to adjust the backup schedule, preventing future disruption.
Comparing with Manual and GUI Methods
Manual Event Viewer Browsing
- Time-consuming
- No exportable formatting
- No automation potential
Basic PowerShell Commands (e.g., Get-EventLog)
- Slower for large log sizes
- Requires post-processing
This Script
- Fast, XML-optimized querying
- Automated output formatting
- Integrated into NinjaOne for central visibility
In short, this script significantly enhances repeatability and efficiency.
Frequently Asked Questions
Q: What permissions are required to run this script?
A: Administrator privileges are required to access the system event log and resolve SID-to-username mappings.
Q: What happens if no reboot events are found?
A: The script logs an error and exits gracefully, returning exit code 1.
Q: Can I run this on Windows 11 or Server 2022?
A: Yes, the script supports Windows 10 and Server 2016 or later, including newer OS versions.
Q: Will the script overwrite existing NinjaOne custom field data?
A: Yes, it replaces the contents with the latest data on each run.
Implications for IT Security and Operations
Monitoring reboot reasons provides early warnings of system instability, misconfigured policies, or even malicious activity. For example, unexpected 6008 shutdowns without corresponding 1074 entries might signal a forced power-off, potentially indicating hardware issues or tampering.
Automating this insight into a central management console like NinjaOne enhances accountability and ensures that remediation happens swiftly. It also bolsters change management and documentation for compliance frameworks such as SOC 2 or ISO 27001.
Best Practices When Using This Script
- Schedule Regular Runs: Use NinjaOne automation to execute the script weekly or after patch cycles.
- Pair with Alerts: Tie custom field updates to email or dashboard alerts for real-time visibility.
- Limit Output Length: Be mindful of field character limits (e.g., 45,000 for WYSIWYG).
- Audit Regularly: Review custom field data during quarterly maintenance for trend analysis.
Final Thoughts
NinjaOne users gain significant operational leverage by automating event log analysis with this PowerShell script. Rather than sifting through logs manually or reacting after the fact, IT teams can proactively document and respond to reboot patterns in real time. It’s a textbook example of how targeted automation can simplify complex diagnostics while improving oversight across distributed environments.
By bridging native Windows telemetry with custom NinjaOne fields, this script enables a smarter, more responsive approach to infrastructure management. Whether you’re managing 10 endpoints or 10,000, visibility into reboot reasons is a small but powerful step toward more resilient IT operations.