How to Generate Windows Reliability Index Reports with PowerShell

For IT professionals and managed service providers (MSPs), insight into system stability is critical. Monitoring reliability over time allows for proactive system maintenance, trend analysis, and better support outcomes. One underutilized but powerful tool for this purpose is the Windows Reliability Index, which provides a quantifiable stability score based on event logs. This post dives into how you can create a Windows Reliability Index report with PowerShell, enabling you to programmatically assess, log, and even publish these scores via custom fields in a platform like NinjaOne.

Background

The Windows Reliability Index has been around since Windows Vista and continues to serve as a helpful metric for determining overall system health. It analyzes critical events such as application crashes, hardware faults, and Windows updates, assigning a daily stability score ranging from 1 (unstable) to 10 (very stable).

Despite its usefulness, there’s no built-in automation to retrieve and display these values in a report-friendly format—especially one integrated into an endpoint management platform. That’s where this script steps in. Designed for environments using PowerShell 5.1 and NinjaOne, the script automates the retrieval, formatting, and optional publishing of Reliability Index data to custom fields—providing both immediate visibility and long-term tracking.

The Script

#Requires -Version 5.1

<#
.SYNOPSIS
    Returns the last score and a daily average report of the Windows Reliability Index.
.DESCRIPTION
    Returns the last score and a daily average report of the Windows Reliability Index.
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)

    [Info] Retrieving reliability index information for SRV19-TEST.
    [Info] Successfully retrieved reliability index information!

    [Info] Latest reliability index score for SRV19-TEST:
    2/19/2025 1:00:00 PM | 6.835

    [Info] Table of daily averages:

    Date      Average Reliability Index
    ----      -------------------------
    2/19/2025                      8.26
    2/18/2025                        10
    2/17/2025                        10
    2/16/2025                        10
    2/15/2025                        10
    2/14/2025                        10
    2/13/2025                        10
    2/12/2025                        10
    2/11/2025                        10
    2/10/2025                        10
    2/9/2025                         10
    2/8/2025                         10
    2/7/2025                         10
    2/6/2025                         10
    2/5/2025                         10
    2/4/2025                         10
    2/3/2025                         10
    2/2/2025                         10
    2/1/2025                         10
    1/31/2025                        10
    1/30/2025                        10
    1/29/2025                        10
    1/28/2025                        10
    1/27/2025                        10
    1/26/2025                        10
    1/25/2025                        10
    1/24/2025                        10
    1/23/2025                        10
    1/22/2025                        10
    1/21/2025                        10
    1/20/2025                        10
    1/19/2025                        10

.PARAMETER -WYSIWYGCustomFieldName "ReplaceMeWithAnyWYSIWYGCustomField"
    Name of the WYSIWYG custom field to populate with the report.

.EXAMPLE
    -WYSIWYGCustomFieldName "ExampleInput"

    [Info] Retrieving reliability index information for SRV19-TEST.
    [Info] Successfully retrieved reliability index information!

    [Info] Retrieving current value of WYSIWYG Custom Field 'WYSIWYG'.
    [Info] Successfully retrieved the current value of 'WYSIWYG'.

    [Info] Publishing Reliability Index daily average table to WYSIWYG Custom Field 'WYSIWYG'.
    [Info] Successfully set 'WYSIWYG' Custom Field!

    [Info] Latest reliability index score for SRV19-TEST:
    2/19/2025 1:00:00 PM | 6.835

    [Info] Table of daily averages:

    Date      Average Reliability Index
    ----      -------------------------
    2/19/2025                      8.26
    2/18/2025                        10
    2/17/2025                        10
    2/16/2025                        10
    2/15/2025                        10
    2/14/2025                        10
    2/13/2025                        10
    2/12/2025                        10
    2/11/2025                        10
    2/10/2025                        10
    2/9/2025                         10
    2/8/2025                         10
    2/7/2025                         10
    2/6/2025                         10
    2/5/2025                         10
    2/4/2025                         10
    2/3/2025                         10
    2/2/2025                         10
    2/1/2025                         10
    1/31/2025                        10
    1/30/2025                        10
    1/29/2025                        10
    1/28/2025                        10
    1/27/2025                        10
    1/26/2025                        10
    1/25/2025                        10
    1/24/2025                        10
    1/23/2025                        10
    1/22/2025                        10
    1/21/2025                        10
    1/20/2025                        10
    1/19/2025                        10

.PARAMETER -TextCustomFieldName "ReplaceMeWithAnyTextCustomField"
    Name of the Text custom field to populate with the most recent reliability index score.
    
.EXAMPLE
    -TextCustomFieldName "ReplaceMeWithAnyMultilineCustomField"

    [Info] Retrieving reliability index information for SRV19-TEST.
    [Info] Successfully retrieved reliability index information!

    [Info] Retrieving current value of Text Custom Field 'Text'.
    [Info] Successfully retrieved the current value of 'Text'.

    [Info] Publishing the latest reliability index score to Text Custom Field 'Text'.
    [Info] Successfully set 'Text' Custom Field to value '2/19/2025 1:00:00 PM | 6.835'!

    [Info] Latest reliability index score for SRV19-TEST:
    2/19/2025 1:00:00 PM | 6.835

    [Info] Table of daily averages:

    Date      Average Reliability Index
    ----      -------------------------
    2/19/2025                      8.26
    2/18/2025                        10
    2/17/2025                        10
    2/16/2025                        10
    2/15/2025                        10
    2/14/2025                        10
    2/13/2025                        10
    2/12/2025                        10
    2/11/2025                        10
    2/10/2025                        10
    2/9/2025                         10
    2/8/2025                         10
    2/7/2025                         10
    2/6/2025                         10
    2/5/2025                         10
    2/4/2025                         10
    2/3/2025                         10
    2/2/2025                         10
    2/1/2025                         10
    1/31/2025                        10
    1/30/2025                        10
    1/29/2025                        10
    1/28/2025                        10
    1/27/2025                        10
    1/26/2025                        10
    1/25/2025                        10
    1/24/2025                        10
    1/23/2025                        10
    1/22/2025                        10
    1/21/2025                        10
    1/20/2025                        10
    1/19/2025                        10
    
.NOTES
    Minimum OS Architecture Supported: Windows 10, Windows Server 2016
    Release Notes: Initial Release
#>

[CmdletBinding()]
param (
    [Parameter()]
    [string]$WYSIWYGCustomFieldName,

    [Parameter()]
    [string]$TextCustomFieldName
)

begin {

    if ($env:WYSIWYGCustomFieldName){
        $WYSIWYGCustomFieldName = $env:WYSIWYGCustomFieldName
    }

    if ($env:TextCustomFieldName){
        $TextCustomFieldName = $env:TextCustomFieldName
    }

    # if custom field input is only spaces or a blank string, set it to null
    # otherwise, trim any whitespace on the ends
    if ([string]::IsNullOrWhiteSpace($WYSIWYGCustomFieldName)){
        $WYSIWYGCustomFieldName = $null
    }
    else{
        $WYSIWYGCustomFieldName = $WYSIWYGCustomFieldName.Trim()
    }

    if ([string]::IsNullOrWhiteSpace($TextCustomFieldName)){
        $TextCustomFieldName = $null
    }
    else{
        $TextCustomFieldName = $TextCustomFieldName.Trim()
    }

    # test custom field for invalid characters
    if ($WYSIWYGCustomFieldName -match "[^0-9A-Z]"){
        Write-Host "[Error] WYSIWYG Custom Field Name contains invalid character(s). Writing to the WYSIWYG Custom Field will be skipped."
        Write-Host "[Error] https://ninjarmm.zendesk.com/hc/en-us/articles/360060920631-Custom-Field-Setup"
        $WYSIWYGCustomFieldName = $null
        $ExitCode = 1
        Write-Host ""
    }

    if ($TextCustomFieldName -match "[^0-9A-Z]"){
        Write-Host "[Error] Text Custom Field Name contains invalid character(s). Writing to the Text Custom Field will be skipped."
        Write-Host "[Error] https://ninjarmm.zendesk.com/hc/en-us/articles/360060920631-Custom-Field-Setup"
        $TextCustomFieldName = $null
        $ExitCode = 1
        Write-Host ""
    }

    function Get-CustomField {
        [CmdletBinding()]
        Param(
            [Parameter(Mandatory = $True, ValueFromPipeline = $True)]
            [String]$Name,
            [Parameter()]
            [String]$Type,
            [Parameter()]
            [String]$DocumentName
        )
        
        # Initialize a hashtable for documentation parameters
        $DocumentationParams = @{}
    
        # If a document name is provided, add it to the documentation parameters
        if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName }
    
        # Define types that require options to be retrieved
        $NeedsOptions = "DropDown", "MultiSelect"
        
        # If a document name is provided, retrieve the property value from the document
        if ($DocumentName) {
            # Throw an error if the type is "Secure", as it's not a valid type in this context
            if ($Type -Like "Secure") { throw [System.ArgumentOutOfRangeException]::New("$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") }
        
            # Notify the user that the value is being retrieved from a Ninja document
            Write-Host "Retrieving value from Ninja Document..."
            $NinjaPropertyValue = Ninja-Property-Docs-Get -AttributeName $Name @DocumentationParams 2>&1
        
            # If the property type requires options, retrieve them
            if ($NeedsOptions -contains $Type) {
                $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1
            }
        }
        else {
            # If no document name is provided, retrieve the property value directly
            $NinjaPropertyValue = Ninja-Property-Get -Name $Name 2>&1
    
            # If the property type requires options, retrieve them
            if ($NeedsOptions -contains $Type) {
                $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1
            }
        }
        
        # Throw an exception if there was an error retrieving the property value or options
        if ($NinjaPropertyValue.Exception) { throw $NinjaPropertyValue }
        if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions }
        
        # Handle the property value based on its type
        switch ($Type) {
            "Attachment" {
                # Convert JSON formatted property value to a PowerShell object
                $NinjaPropertyValue | ConvertFrom-Json
            }
            "Checkbox" {
                # Convert the value to a boolean
                [System.Convert]::ToBoolean([int]$NinjaPropertyValue)
            }
            "Date or Date Time" {
                # Convert a Unix timestamp to local date and time
                $UnixTimeStamp = $NinjaPropertyValue
                $UTC = (Get-Date "1970-01-01 00:00:00").AddSeconds($UnixTimeStamp)
                $TimeZone = [TimeZoneInfo]::Local
                [TimeZoneInfo]::ConvertTimeFromUtc($UTC, $TimeZone)
            }
            "Decimal" {
                # Convert the value to a double (floating-point number)
                [double]$NinjaPropertyValue
            }
            "Device Dropdown" {
                # Convert JSON formatted property value to a PowerShell object
                $NinjaPropertyValue | ConvertFrom-Json
            }
            "Device MultiSelect" {
                # Convert JSON formatted property value to a PowerShell object
                $NinjaPropertyValue | ConvertFrom-Json
            }
            "Dropdown" {
                # Convert options to a CSV format and match the GUID to retrieve the display name
                $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name"
                $Options | Where-Object { $_.GUID -eq $NinjaPropertyValue } | Select-Object -ExpandProperty Name
            }
            "Integer" {
                # Convert the value to an integer
                [int]$NinjaPropertyValue
            }
            "MultiSelect" {
                # Convert options to a CSV format, then match and return selected items
                $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name"
                $Selection = ($NinjaPropertyValue -split ',').trim()
        
                foreach ($Item in $Selection) {
                    $Options | Where-Object { $_.GUID -eq $Item } | Select-Object -ExpandProperty Name
                }
            }
            "Organization Dropdown" {
                # Convert JSON formatted property value to a PowerShell object
                $NinjaPropertyValue | ConvertFrom-Json
            }
            "Organization Location Dropdown" {
                # Convert JSON formatted property value to a PowerShell object
                $NinjaPropertyValue | ConvertFrom-Json
            }
            "Organization Location MultiSelect" {
                # Convert JSON formatted property value to a PowerShell object
                $NinjaPropertyValue | ConvertFrom-Json
            }
            "Organization MultiSelect" {
                # Convert JSON formatted property value to a PowerShell object
                $NinjaPropertyValue | ConvertFrom-Json
            }
            "Time" {
                # Convert the value from seconds to a time format in the local timezone
                $Seconds = $NinjaPropertyValue
                $UTC = ([timespan]::fromseconds($Seconds)).ToString("hh\:mm\:ss")
                $TimeZone = [TimeZoneInfo]::Local
                $ConvertedTime = [TimeZoneInfo]::ConvertTimeFromUtc($UTC, $TimeZone)
        
                Get-Date $ConvertedTime -DisplayHint Time
            }
            default {
                # For any other types, return the raw value
                $NinjaPropertyValue
            }
        }
    }

    function Set-CustomField {
        [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 ' ', '&nbsp;'
        }
        
        # 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)
    }
}
process {
    if (-not (Test-IsElevated)) {
        Write-Host -Object "[Error] Access Denied. Please run with Administrator privileges."
        exit 1
    }

    $ExitCode = 0

    # retrieve reliability index metrics
    try{
        Write-Host "[Info] Retrieving reliability index information for $env:computername."
        $reliabilityMetrics = Get-CimInstance Win32_ReliabilityStabilityMetrics | Sort-Object TimeGenerated -Descending
        Write-Host "[Info] Successfully retrieved reliability index information!`n"
    }
    catch{
        Write-Host "[Error] Error retrieving reliability index information."
        Write-Host "[Error] $($_.Exception.Message)"
        exit 1
    }

    # if any metrics are present, continue with script
    # otherwise, error out with a message that no information is available
    if ($reliabilityMetrics){
        # get unique dates from the metrics
        $dates = $reliabilityMetrics | Group-Object {$_.TimeGenerated.ToShortDateString()}

        # create table with averages of each dates' score, sorted by date (most recent to oldest)
        $reliabilityIndexTable = $(foreach ($currentDate in $dates){
            [PSCustomObject]@{
                Date = $currentDate.Name
                "Average Reliability Index" = [Math]::Round(($currentDate.Group.SystemStabilityIndex | Measure-Object -Average).Average,2)
            }
        }) | Sort-Object {[datetime]::Parse($_.Date)} -Descending

        # get the most recent time and score
        $currentIndexDate = ($reliabilityMetrics | Select-Object -First 1).EndMeasurementDate.ToString()
        $currentIndexScore = ($reliabilityMetrics | Select-Object -First 1).SystemStabilityIndex

        # above values will be written to the Text Custom Field and output to the activity feed using the below formatted string
        $latestIndexInfo = "$currentIndexDate | $currentIndexScore"

        # write to WYSIWYG Custom Field if provided
        if ($WYSIWYGCustomFieldName){
            # convert report to HTML table
            $report = $reliabilityIndexTable | ConvertTo-HTML -Fragment

            # attempt to get the current value of the field, to determine if it already matches the report
            try{
                Write-Host "[Info] Retrieving current value of WYSIWYG Custom Field '$WYSIWYGCustomFieldName'."
                # output will be in JSON format for this type of field, need to first convert it to an object, and we want to compare the HTML so that is extracted into the variable
                $currentValue = (Get-CustomField $WYSIWYGCustomFieldName | ConvertFrom-JSON).HTML
                Write-Host "[Info] Successfully retrieved the current value of '$WYSIWYGCustomFieldName'.`n"
            }
            catch{
                Write-Host "[Error] Error retrieving current value of '$WYSIWYGCustomFieldName'."
                Write-Host "[Error] $($_.Exception.Message)`n"
                # an error occurred while getting the current value of the custom field, so we will skip writing to it
                $skipWYSIWYGCFWrite = $true
                $ExitCode = 1
            }

            # check if the current value of the field is the same as the report. if so, output a message and do not try to write to the field again
            if ($currentValue -eq $report){
                Write-Host "[Info] WYSIWYG Custom Field '$WYSIWYGCustomFieldName' already has the latest information.`n"
            }
            # otherwise, continue with writing to the custom field unless there was an error retrieving the current value above
            elseif (-not ($skipWYSIWYGCFWrite)){
                try{
                    Write-Host "[Info] Publishing Reliability Index daily average table to WYSIWYG Custom Field '$WYSIWYGCustomFieldName'."
                    Set-CustomField -Name $WYSIWYGCustomFieldName -Value $report -Type "WYSIWYG"
                    Write-Host "[Info] Successfully set '$WYSIWYGCustomFieldName' Custom Field!`n"
                }
                catch{
                    Write-Host "[Error] Error setting '$WYSIWYGCustomFieldName'."
                    Write-Host "[Error] $($_.Exception.Message)`n"
                    $ExitCode = 1
                }
            }
        }

        # write to Text custom field if provided
        if ($TextCustomFieldName){
            # attempt to retrieve current value of the text custom field
            try{
                Write-Host "[Info] Retrieving current value of Text Custom Field '$TextCustomFieldName'."
                $currentValue = Get-CustomField $TextCustomFieldName
                Write-Host "[Info] Successfully retrieved the current value of '$TextCustomFieldName'.`n"
            }
            catch{
                Write-Host "[Error] Error retrieving current value of '$TextCustomFieldName'."
                Write-Host "[Error] $($_.Exception.Message)`n"
                # an error occurred while getting the current value of the custom field, so we will skip writing to it
                $skipTextCFWrite = $true
                $ExitCode = 1
            }

            if ($currentValue -eq $latestIndexInfo){
                Write-Host "[Info] Text Custom Field '$TextCustomFieldName' already has the latest information.`n"
            }
            elseif (-not ($skipTextCFWrite)){
                try{
                    Write-Host "[Info] Publishing the latest reliability index score to Text Custom Field '$TextCustomFieldName'."
                    Set-CustomField -Name $TextCustomFieldName -Value $latestIndexInfo -Type "Text"
                    Write-Host "[Info] Successfully set '$TextCustomFieldName' Custom Field to value '$latestIndexInfo'!`n"
                }
                catch{
                    Write-Host "[Error] Error setting '$TextCustomFieldName' to value '$latestIndexInfo'."
                    Write-Host "[Error] $($_.Exception.Message)`n"
                    $ExitCode = 1
                }
            }
        }

        # output information to the activity feed
        Write-Host "[Info] Latest reliability index score for $env:computername`:"
        $latestIndexInfo | Out-Host
        Write-Host "`n[Info] Table of daily averages:"
        $reliabilityIndexTable | Out-Host
    }
    else{
        Write-Host "[Error] Reliability index information could not be found on this machine."
        exit 1
    }
    
    exit $ExitCode
}
end {
    
    
    
}

 

Detailed Breakdown

Here’s how the script works step-by-step:

  1. Parameter Handling:
    • Accepts two optional parameters: -WYSIWYGCustomFieldName and -TextCustomFieldName.
    • These determine whether to publish the results to NinjaOne’s WYSIWYG (HTML-formatted) or Text custom fields.
  2. Environment Variable Handling:
    • If these parameters are passed as environment variables (common in RMM task scripts), they are captured and validated.
  3. Validation Logic:
    • Ensures that custom field names contain only valid characters.
    • Trims whitespace and gracefully skips invalid entries.
  4. Reliability Index Retrieval:
    • Uses Get-CimInstance to query Win32_ReliabilityStabilityMetrics.
    • Aggregates the metrics into a table of daily averages, sorted with the most recent at the top.
    • Extracts the latest recorded score and timestamp.
  5. Publishing Logic:
    • If a WYSIWYG field is specified:
      • Converts the table to an HTML fragment.
      • Compares current vs. new content before updating to avoid redundant writes.
    • If a Text field is specified:
      • Publishes the most recent score as a string (e.g., 2/19/2025 1:00:00 PM | 6.835).
  6. Logging and Output:
    • Detailed [Info] and [Error] messages are provided for transparency and troubleshooting.
    • Outputs the current score and reliability history to the activity feed.

Potential Use Cases

Case Study: Jane, an MSP Systems Engineer

Jane manages over 500 endpoints across various client environments. To proactively identify machines experiencing stability issues, she deploys this PowerShell script via NinjaOne’s automation module:

  • Systems scoring below 8 over consecutive days trigger a ticket via webhooks.
  • WYSIWYG fields hold formatted reports for executive summaries.
  • Text fields capture the latest score, which integrates with dashboards.

This approach enables Jane to detect declining stability trends before they impact users, improving SLA compliance and client satisfaction.

Comparisons

MethodProsCons
Manual Use of Reliability Monitor GUISimple, no scripting knowledge requiredTime-consuming, no export or automation
Event Log FilteringGranular controlComplex and not summarized
This PowerShell ScriptAutomated, report-friendly, integrated with NinjaOneRequires PowerShell 5.1+, administrator rights

Compared to traditional methods, this script offers a streamlined, reproducible, and integrated approach to monitoring reliability across many endpoints.

Implications

By centralizing and automating the tracking of system reliability, IT professionals gain an early warning system for system degradation. Consistently low scores can signal larger issues—such as driver conflicts or failing hardware—that might otherwise go unnoticed. This script also promotes data-driven decision making, especially when tied into ticketing and alerting systems.

Recommendations

  • Schedule regular runs via task scheduler or RMM automation to maintain updated records.
  • Set thresholds to trigger alerts or support actions.
  • Use meaningful naming conventions for your custom fields to avoid misconfiguration.
  • Test on a few systems before scaling to production environments.

Final Thoughts

Creating a Windows Reliability Index report with PowerShell is an efficient way to bring visibility into the stability of managed endpoints. This script, when coupled with NinjaOne, allows you to store, format, and monitor reliability scores in a way that aligns with modern IT automation practices. For MSPs aiming to deliver proactive, high-touch service, this solution adds a low-effort, high-value monitoring tool to the toolbox.

By enabling automated reliability reporting through custom fields, NinjaOne helps IT teams close the visibility gap and improve operational outcomes—all from a single pane of glass.

FAQs

It aggregates Windows event logs, crashes, updates, and hardware errors.

Yes, it will still generate and print the reliability report, but custom field publishing is specific to NinjaOne environments.

Daily or weekly, depending on the volatility of your environment.

The script exits with an error message noting that no data is available.

Next Steps

Building an efficient and effective IT team requires a centralized solution that acts as your core service delivery tool. NinjaOne enables IT teams to monitor, manage, secure, and support all their devices, wherever they are, without the need for complex on-premises infrastructure.

Learn more about NinjaOne Remote Script Deployment, check out a live tour, or start your free trial of the NinjaOne platform.

Categories:

You might also like