---
title: "PC Health Built-in Catalog: Checks & Remediations"
description: This is the reference list of every built-in PC Health check and remediation K12Panel ships with, including the exact PowerShell each one runs.
---

[Skip to content](https://k12panel.com/kb/pc-health-and-soar/built-in-catalog-checks-and-remediations#main-content)

![test-full-word-logo.png\]](https://k12panel.com/hs-fs/hubfs/test-full-word-logo.png?height=35&name=test-full-word-logo.png)

Open main navigation

Close main navigation

- [Go to k12panel.com](https://k12panel.com)

[Go to k12panel.com](https://k12panel.com?hsLang=en)

 Hello. How can we help you?

- There are no suggestions because the search field is empty.

1. [Online Help and Knowledge Base](https://k12panel.com/kb?hsLang=en)
2. [PC Health and SOAR](https://k12panel.com/kb/pc-health-and-soar?hsLang=en)

# PC Health Built-in Catalog: Checks & Remediations

## This is the reference list of every built-in PC Health check and remediation K12Panel ships with, including the exact PowerShell each one runs. Use it to see precisely what a built-in measures or does before you bind it to a policy — and as a starting point for your own custom checks and fixes. For the concepts see About PC Health and Self-Healing; for the workflow see Using PC Health in K12Panel; to write your own see Writing Custom Health Checks and Writing Custom Remediations.

This is the reference list of every **built-in** PC Health check and remediation K12Panel ships with, including the exact PowerShell each one runs. Use it to see precisely what a built-in measures or does before you bind it to a policy — and as a starting point for your own custom checks and fixes. For the concepts see *About PC Health and Self-Healing*; for the workflow see *Using PC Health in K12Panel*; to write your own see *Writing Custom Health Checks* and *Writing Custom Remediations*.

A few things that apply to everything below:

- **Windows only.** All built-ins target Windows devices running the K12Panel agent, and ride the agent's normal check-in — nothing to install, no agent update.
- **Built-ins are locked.** You can't edit a built-in's script. To tweak one, copy its PowerShell into a **new custom** check or remediation and adjust that. (Authoring definitions needs the **Architect** role.)
- **Thresholds live in the policy, not the check.** The *default bands* listed here are just sensible starting points; each policy can override them per check. One built-in — **CPU utilization (average)** — ships with *no* bands on purpose: it's a measurement, not an alert.
- **Checks output one number or throw** (throwing → **Unknown**). **Remediations** return on success or `throw` on failure — never `exit`. (The one exception is **CPU utilization (average)**, a built-in that reports a pair of raw counters for the server to convert; custom checks always output a single number.)

 

### --- **Built-in checks**

### *Storage & disk*

**Disk free (system drive)** — slug `disk_free` · unit `%` · lower is worse · gauge · default bands `critical < 5 (clears at 8)`, `warning < 15 (clears at 18)` · timeout 15s.

```
$disk = Get-CimInstance Win32_LogicalDisk -Filter "DeviceID='$env:SystemDrive'" -ErrorAction Stopif (-not $disk -or [int64]$disk.Size -le 0) { throw "system drive $env:SystemDrive not found or zero size" }[math]::Round(([double]$disk.FreeSpace / [double]$disk.Size) * 100, 2)
```

**Data drive free space** — slug `data_drive_free` · unit `%` · lower is worse · gauge · default bands `critical < 5 (clears at 8)`, `warning < 15 (clears at 18)` · timeout 15s. Lowest free % across internal fixed data drives (D:, E:, …), excluding the system drive and USB/network drives; returns `100` on single-drive machines so they stay quiet.

```
$sys = $env:SystemDrive.TrimEnd(':')[0]$vals = Get-Volume | Where-Object {    $_.DriveType -eq 'Fixed' -and $_.DriveLetter -and $_.DriveLetter -ne $sys -and $_.Size -gt 0} | ForEach-Object { [math]::Round($_.SizeRemaining / $_.Size * 100, 2) }if (-not $vals) { 100 } else { ($vals | Measure-Object -Minimum).Minimum }
```

**Predictive disk health (SMART)** — slug `disk_smart_health` · `1` healthy / `0` failing · lower is worse · stoplight · default band `critical < 1` · timeout 15s. Returns `0` only when a disk reports `Warning`/`Unhealthy`; `Unknown` (VMs, USB enclosures, Storage Spaces) is treated as healthy to avoid false alarms.

```
if ((Get-PhysicalDisk | Where-Object { $_.HealthStatus -in @('Warning','Unhealthy') }).Count -gt 0) { 0 } else { 1 }
```

### *Windows servicing*

**Uptime since reboot** — slug `uptime_days` · unit `days` · higher is worse · default bands `critical > 30 (clears at 28)`, `warning > 14 (clears at 12)` · timeout 15s.

```
$os = Get-CimInstance Win32_OperatingSystem -ErrorAction Stopif (-not $os.LastBootUpTime) { throw "LastBootUpTime unavailable" }[math]::Round((New-TimeSpan -Start $os.LastBootUpTime -End (Get-Date)).TotalDays, 3)
```

**Pending-reboot age** — slug `pending_reboot_age` · unit `days` (0 = none) · higher is worse · default bands `critical > 7`, `warning > 3` · timeout 20s. Reads the *age* of a pending reboot from the registry key's last-write timestamp (via a small P/Invoke, since PowerShell doesn't expose key write times).

```
$keys = @(  'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending',  'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired')$pending = @($keys | Where-Object { Test-Path -LiteralPath $_ })if ($pending.Count -eq 0) { return 0.0 }if (-not ('K12.Reg' -as [type])) {  Add-Type -Namespace K12 -Name Reg -MemberDefinition @'[DllImport("advapi32.dll", CharSet=CharSet.Unicode)] public static extern int RegOpenKeyEx(System.UIntPtr hKey, string subKey, int options, int samDesired, out System.UIntPtr result);[DllImport("advapi32.dll")] public static extern int RegQueryInfoKey(System.UIntPtr hKey, System.IntPtr a, System.IntPtr b, System.IntPtr c, System.IntPtr d, System.IntPtr e, System.IntPtr f, System.IntPtr g, System.IntPtr h, System.IntPtr i, System.IntPtr j, out long lastWriteFiletime);[DllImport("advapi32.dll")] public static extern int RegCloseKey(System.UIntPtr hKey);'@}$HKLM = [System.UIntPtr]::new([uint64]0x80000002)$KEY_READ = 0x20019$oldest = $nullforeach ($p in $pending) {  $sub = $p -replace '^HKLM:\\', ''  $h = [System.UIntPtr]::Zero  if ([K12.Reg]::RegOpenKeyEx($HKLM, $sub, 0, $KEY_READ, [ref]$h) -eq 0) {    $ft = [long]0    $ok = [K12.Reg]::RegQueryInfoKey($h,            [System.IntPtr]::Zero,[System.IntPtr]::Zero,[System.IntPtr]::Zero,[System.IntPtr]::Zero,[System.IntPtr]::Zero,            [System.IntPtr]::Zero,[System.IntPtr]::Zero,[System.IntPtr]::Zero,[System.IntPtr]::Zero,[System.IntPtr]::Zero,            [ref]$ft)    if ($ok -eq 0 -and $ft -gt 0) {      $dt = [DateTime]::FromFileTimeUtc($ft)      if ($null -eq $oldest -or $dt -lt $oldest) { $oldest = $dt }    }    [void][K12.Reg]::RegCloseKey($h)  }}if ($null -eq $oldest) { throw "pending reboot detected but key timestamp unavailable" }[math]::Round(((Get-Date).ToUniversalTime() - $oldest).TotalDays, 3)
```

**Windows Update age** — slug `windows_update_age` · unit `days` · higher is worse · default bands `critical > 60`, `warning > 30` · timeout 20s · pairs with **Reset Windows Update cache**. Days since the newest installed hotfix (`Win32_QuickFixEngineering`), with the legacy registry key as a fallback (that key is absent on modern Win10/11, so a registry-only check would read Unknown fleet-wide).

```
$dates = @(Get-CimInstance Win32_QuickFixEngineering -ErrorAction SilentlyContinue |    Where-Object { $_.InstalledOn } | ForEach-Object { [datetime]$_.InstalledOn })$k = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\Results\Install'$reg = (Get-ItemProperty -Path $k -Name LastSuccessTime -ErrorAction SilentlyContinue).LastSuccessTimeif ($reg) { $dates += [datetime]$reg }if (-not $dates) { throw 'no update-install timestamp from QFE or registry' }$latest = ($dates | Measure-Object -Maximum).Maximum[math]::Round(((Get-Date) - $latest).TotalDays, 1)
```

### *Security & compliance*

**Windows Firewall on (all profiles)** — slug `firewall_enabled` · `1` on / `0` off · lower is worse · default band `critical < 1` · timeout 15s · pairs with **Enable Windows Firewall**. Returns `0` if any of Domain/Private/Public is disabled.

```
if ((Get-NetFirewallProfile | Where-Object { -not $_.Enabled }).Count -gt 0) { 0 } else { 1 }
```

**System drive BitLocker on** — slug `bitlocker_on` · `1` protected / `0` not · lower is worse · default band `warning < 1` · timeout 15s. Throws → **Unknown** on Windows Home (no BitLocker) and when not elevated (the agent runs as SYSTEM, so it's fine in the field).

```
$v = Get-BitLockerVolume -MountPoint $env:SystemDrive -ErrorAction Stopif ($v.ProtectionStatus -eq 'On') { 1 } else { 0 }
```

### *Hardware & time*

**Battery health (capacity vs design)** — slug `battery_health` · unit `%` · lower is worse · gauge · default bands `critical < 40 (clears at 45)`, `warning < 60 (clears at 65)` · timeout 20s. Full-charge capacity as a share of the original design capacity (100 = new). Returns `100` on devices with no battery (desktops) so they stay quiet.

```
$design = Get-CimInstance -Namespace root\wmi -ClassName BatteryStaticData -ErrorAction SilentlyContinue |    Select-Object -First 1 -ExpandProperty DesignedCapacity$full = Get-CimInstance -Namespace root\wmi -ClassName BatteryFullChargedCapacity -ErrorAction SilentlyContinue |    Select-Object -First 1 -ExpandProperty FullChargedCapacityif (-not $design -or -not $full) { 100 } else { [math]::Round($full / $design * 100, 1) }
```

**System clock drift** — slug `time_drift` · unit `seconds` · higher is worse · default bands `critical > 300`, `warning > 60` · timeout 30s · pairs with **Resync system time**. Absolute clock offset in seconds vs an external source; works on non-domain and Entra-ID-joined devices. Measures via NTP against a first-responder list of vendor-neutral servers (`pool.ntp.org`, then `time.cloudflare.com`), falling back to an HTTPS `Date`-header read for networks that block UDP/123.

```
$off = $nullforeach ($srv in @('pool.ntp.org','time.cloudflare.com')) {    $raw = w32tm /stripchart /computer:$srv /samples:1 /dataonly 2>$null    foreach ($line in $raw) {        if ($line -match '([+-]?\d+\.\d+)s') { $off = [math]::Abs([double]$Matches[1]); break }    }    if ($null -ne $off) { break }}if ($null -eq $off) {    try {        $r = Invoke-WebRequest -Uri 'https://www.google.com' -Method Head -UseBasicParsing -TimeoutSec 8        $server = [datetime]::Parse($r.Headers['Date']).ToUniversalTime()        $off = [math]::Abs(((Get-Date).ToUniversalTime() - $server).TotalSeconds)    } catch { }}if ($null -eq $off) { throw 'could not measure time drift (NTP and HTTPS both unreachable)' }[math]::Round($off, 2)
```

**Print Spooler running** — slug `service_spooler` · `1` running / `0` stopped · lower is worse · default band `critical < 1` · timeout 15s · pairs with **Restart Print Spooler + clear queue**.

```
if ((Get-Service -Name 'Spooler' -ErrorAction Stop).Status -eq 'Running') { 1 } else { 0 }
```

### *Performance*

**CPU utilization (average)** — slug `cpu_avg` · unit `%` · higher is worse · gauge · **no default bands** · timeout 15s. Average CPU utilization over the device's most recent sample window (an hour on the default policy), matching the number Task Manager reports.

This one behaves differently from every other built-in, in two ways worth understanding before you enable it.

**It is a measurement, not an alert.** It ships with *no bands*, so it records a value and always reads **OK** — it will never raise a PC Health alert, and it never counts toward a device's health issues. That's deliberate: a PC legitimately pegs at 100% during a Windows update, a big spreadsheet, or a video export, so alerting on high CPU mostly cries wolf. The value is in *seeing* it across the fleet:

- Turn on the **CPU %** column on the **Assets** table (Columns → CPU %).
- Ask AI Search for it in plain language — *"PCs above 85% CPU"*, *"slow computers"*, *"which machines are maxed out"*.

That's how you find the machines that are genuinely undersized — the evidence behind a replacement request, and the answer to the most common ticket in any school: *"my computer is slow."* If you do want it to alert, add a band to the check in your policy (e.g. `warning > 90`) — you're opting in with a threshold you chose.

**Two samples are needed before a value appears.** CPU utilization can't be read as a single instant value the way disk space can — Windows only exposes counters that accumulate. So K12Panel reads the counters each cycle and works out the average *between* the last two readings. The practical consequences:

- After you enable the check, the **first** check-in shows **Unknown**; a number appears on the **second**. On the default hourly policy, that's about an hour.
- A **reboot** resets the counters, so the cycle spanning a restart reads Unknown, then recovers on the next one.
- A device that's been off for days reads Unknown rather than reporting a stale multi-day average.

None of these are faults — they're the check telling you honestly that it doesn't have two readings to compare yet.

```
$p = Get-CimInstance Win32_PerfRawData_Counters_ProcessorInformation -Filter "Name='_Total'" -ErrorAction Stopif (-not $p) { throw "ProcessorInformation counters unavailable" }[double]$p.PercentProcessorUtility[double]$p.PercentProcessorUtility_Base
```

> **This is the one built-in you can't copy into a custom check.** Notice it outputs **two** numbers, not one — a pair of raw counters that the server turns into a percentage. Custom checks must output a single number (see *Writing Custom Health Checks*), so pasting this script into one won't work; it would read as Unknown. Bind the built-in to your policy instead.

### *Delivery (server-derived)*

**Agent offline** — slug `agent_offline` · unit `hours` · higher is worse · default band `warning > 4`. This one has **no PowerShell** — K12Panel computes it on the server from the device's last check-in time, so it still works when the agent is dead or offline.

---

### Built-in check templates

A **template** is a check with a placeholder you fill in to stamp out a concrete, editable check. Find them in the **Templates** section of **PC Health → Checks** and click **Use template**.

**Service running** — slug `service_running` · parameter **Service name** (``) · `1` running / `0` stopped · default band `critical < 1`. Produces an "is that service running" check for any service you name (e.g. `wuauserv`, `W32Time`). When you stamp one, you can optionally create the matching **Restart a service** remediation in the same step.

```
if ((Get-Service -Name '' -ErrorAction Stop).Status -eq 'Running') { 1 } else { 0 }
```

---

### Built-in remediations

All non-disruptive fixes run whenever they're needed; disruptive fixes auto-run only inside the policy's **maintenance window**.

**Disk Cleanup** — slug `disk_cleanup` · non-disruptive · timeout 300s. Clears user and Windows temp folders, the Windows Update download cache, and the recycle bin.

```
$ErrorActionPreference = 'SilentlyContinue'$paths = @("$env:TEMP\*", "$env:WINDIR\Temp\*", "$env:WINDIR\SoftwareDistribution\Download\*")foreach ($p in $paths) { Remove-Item -Path $p -Recurse -Force -ErrorAction SilentlyContinue }Clear-RecycleBin -Force -ErrorAction SilentlyContinue
```

**Reset Windows Update cache** — slug `wu_reset` · non-disruptive · timeout 300s · pairs with **Windows Update age**. Stops Windows Update, clears the download cache so stuck/partial updates re-fetch, and restarts it. No reboot.

```
Stop-Service -Name wuauserv -Force -ErrorAction StopRemove-Item -Path "$env:SystemRoot\SoftwareDistribution\Download\*" -Recurse -Force -ErrorAction SilentlyContinueStart-Service -Name wuauserv -ErrorAction Stop
```

**Restart Print Spooler + clear queue** — slug `spooler_restart` · non-disruptive · timeout 120s · pairs with **Print Spooler running**. Restarts the spooler and clears stuck print jobs, self-verifying it comes back running. (Clears queued jobs — users may need to reprint.)

```
Stop-Service -Name Spooler -Force -ErrorAction StopRemove-Item -Path "$env:SystemRoot\System32\spool\PRINTERS\*" -Force -ErrorAction SilentlyContinueStart-Service -Name Spooler -ErrorAction StopStart-Sleep -Seconds 2if ((Get-Service -Name Spooler).Status -ne 'Running') { throw 'spooler not running after restart' }
```

**Resync system time** — slug `time_resync` · non-disruptive · timeout 120s · pairs with **System clock drift**. Ensures the Windows Time service is running and forces a resync against its source.

```
Start-Service -Name w32time -ErrorAction SilentlyContinueStart-Sleep -Seconds 1& w32tm /resync /force | Out-Nullif ((Get-Service -Name w32time).Status -ne 'Running') { throw 'w32time not running after resync' }
```

**Enable Windows Firewall (all profiles)** — slug `firewall_enable` · non-disruptive · timeout 60s · pairs with **Windows Firewall on**. Turns the firewall back on for Domain/Public/Private and self-verifies none remain disabled. (Enforces the secure default — may block traffic that was flowing with the firewall off.)

```
Set-NetFirewallProfile -Profile Domain,Public,Private -Enabled True -ErrorAction StopStart-Sleep -Seconds 1if ((Get-NetFirewallProfile | Where-Object { -not $_.Enabled }).Count -gt 0) { throw 'a firewall profile is still disabled' }
```

**Reboot Device** — slug `reboot_device` · **disruptive** · timeout 60s. A built-in command (not a script) — K12Panel dispatches its native Reboot action.

**Restart Agent** — slug `restart_agent` · **disruptive** · timeout 60s. A built-in command (not a script) — K12Panel restarts its own agent service.

---

### Built-in remediation templates

**Restart a service** — slug `restart_service` · parameter **Service name** (``) · non-disruptive · timeout 120s. The partner to the **Service running** check template: restarts the named service and verifies it came back running. Find it under **PC Health → Remediations → Templates → Use template**.

```
Restart-Service -Name '' -Force -ErrorAction StopStart-Sleep -Seconds 2if ((Get-Service -Name '').Status -ne 'Running') { throw "'' not running after restart" }
```

---

#### Frequently Asked Questions

**Can I edit a built-in's script?** No — built-ins are locked. To change one, copy its PowerShell above into a **New Custom Check** (or **New Custom Remediation**), edit it, and bind that instead. Authoring definitions needs the **Architect** role.

**Why doesn't "Agent offline" have a script?** It's *server-derived*. K12Panel calculates it from the device's last check-in time on the server, so it can flag a device precisely when the agent isn't running to answer for itself.

**These bands don't match what I want.** The bands listed are the built-in **defaults**. Thresholds live in the policy, so open the policy, find the check's row, and set the bands you want there — the same check can be strict for one group and relaxed for another.

**Do I need to update the agent to get these?** No. Checks and remediations are delivered on the agent's normal check-in. Nothing to install, no agent update.

**Which checks pair with a fix out of the box?** Windows Update age ↔ Reset Windows Update cache; Print Spooler running ↔ Restart Print Spooler; System clock drift ↔ Resync system time; Windows Firewall on ↔ Enable Windows Firewall; and any **Service running** instance ↔ its **Restart a service** partner. Wire them together on the check's row in the policy editor. The rest are notify-only by default.

- [Getting started](https://k12panel.com/kb/getting-started?hsLang=en)
- [Menu Functions](https://k12panel.com/kb/menu-functions?hsLang=en)
- [Settings](https://k12panel.com/kb/settings?hsLang=en)
- [3rd Party Integrations](https://k12panel.com/kb/3rd-party-integrations?hsLang=en)
- [PC Health and SOAR](https://k12panel.com/kb/pc-health-and-soar?hsLang=en)
- [FAQ](https://k12panel.com/kb/faq?hsLang=en)
- [K12Panel Agent](https://k12panel.com/kb/k12panel-agent?hsLang=en)
- [Blueprints Catalog](https://k12panel.com/kb/blueprints-catalog?hsLang=en)
- [Writing your own Blueprint](https://k12panel.com/kb/writing-your-own-blueprint?hsLang=en)
- [MSP Tools](https://k12panel.com/kb/msp-tools?hsLang=en)
- [Developer API](https://k12panel.com/kb/developer-api?hsLang=en)

# K12Panel Inc

k12panel.com Help Center

Copyright © 2026, K12Panel Inc