Skip to content
  • There are no suggestions because the search field is empty.

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.
  • Checks output one number or throw (throwing → Unknown). Remediations return on success or throw on failure — never exit.

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 Stop
if (-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 Stop
if (-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 = $null
foreach ($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).LastSuccessTime
if ($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 Stop
if ($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 FullChargedCapacity
if (-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 = $null
foreach ($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 }

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 Stop
Remove-Item -Path "$env:SystemRoot\SoftwareDistribution\Download\*" -Recurse -Force -ErrorAction SilentlyContinue
Start-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 Stop
Remove-Item -Path "$env:SystemRoot\System32\spool\PRINTERS\*" -Force -ErrorAction SilentlyContinue
Start-Service -Name Spooler -ErrorAction Stop
Start-Sleep -Seconds 2
if ((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 SilentlyContinue
Start-Sleep -Seconds 1
& w32tm /resync /force | Out-Null
if ((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 Stop
Start-Sleep -Seconds 1
if ((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 Stop
Start-Sleep -Seconds 2
if ((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.