Detect Stale Devices in Your Intune Estate · CloudEngineerHub
Detection & Remediation10 min read4 views
Detecting Stale Devices in Your Intune Estate with PowerShell
An honest two-sided approach to stale Intune devices: a read-only local evidence script plus an admin-side Graph query, so cleanup decisions rest on evidence, not guesswork.
C
CloudEngineerHub Team
Published: September 15, 2026· Last Updated: September 28, 2026·10 min read
Every Intune tenant accumulates devices that stop being real. Laptops that were wiped and reassigned, test machines that never made it into a group, devices retired by procurement without IT knowing, VMs paused since last year. They linger in reports, inflate compliance percentages, hold licenses, and occasionally trigger incidents when someone trusts a "compliant" device that nobody has touched in eight months. Cleaning them up is one of the highest-value hygiene tasks in Intune administration, and one of the easiest to do dangerously.
Visual Walkthrough
The engine behind every “Detect with PowerShell” runbook on this site.
Exit 1 flips the device into the remediation queue for this package.
remediate
3
Remediation executes
Fix script runs silently as SYSTEM with a 5-minute budget.
verify
4
Re-run detection
The same detection script must now exit 0 — proof, not hope.
telemetry
5
Results report
Per-device outcome lands in Reports → Remediations for trend analysis.
6
Repeat on schedule
The loop continues — drift gets caught the day it appears.
Continuous cycle
Proactive Remediations — The Detect → Fix → Verify Loop. 1. Detection script runs: Scheduled (daily/hourly): exit 0 healthy, exit 1 unhealthy — nothing more. — 2. Verdict: unhealthy: Exit 1 flips the device into the remediation queue for this package. — 3. Remediation executes: Fix script runs silently as SYSTEM with a 5-minute budget. — 4. Re-run detection: The same detection script must now exit 0 — proof, not hope. — 5. Results report: Per-device outcome lands in Reports → Remediations for trend analysis. — 6. Repeat on schedule: The loop continues — drift gets caught the day it appears.
C
CloudEngineerHub Team
Author
The CloudEngineerHub editorial team — Microsoft endpoint management and cloud engineers writing hands-on guides on Intune, Azure, Entra ID, Autopilot, Defender and PowerShell. Every article is reviewed against current Microsoft documentation.
This article handles the topic honestly from both sides. The device script here is detection-only and deliberately scoped: a device cannot see the tenant, so it cannot declare itself stale in the global sense. What it can do is produce the local half of the evidence: when its EnterpriseMgmt sync tasks last ran, when its Intune Management Extension log was last written, and when it last booted. The tenant half comes from an admin-side Microsoft Graph query against the device objects, included in this article, which you run from a management workstation. Together, the two views are what a defensible cleanup decision looks like.
Neither script deletes anything. The cleanup workflow itself, disable first, wait, then delete, belongs to your device lifecycle process; this package makes sure that process starts from evidence rather than guesswork.
Symptoms
Stale devices rarely complain; they are noticed by their absence of complaints:
Device counts in Intune exceed what the hardware asset register says you own.
Compliance and app deployment reports include devices that have not checked in for weeks or months.
Licenses and group memberships are consumed by machines nobody can locate.
The help desk finds records for devices that were reimaged or retired long ago.
Security review asks for the last activity of every managed device and the answer requires archaeology.
Possible Causes
Why devices go stale in the tenant:
Decommissioning without offboarding
Devices are wiped, sold, or scrapped, and the Intune device record survives because nobody ran a retire action. The record keeps its last-reported state forever.
Enrollment attempts that never finished
A device that enrolled partway, or an Autopilot registration that never became a real enrollment, leaves a device object that will never check in again.
Paused or archived virtual machines
A VM that is checkpointed and shut down is not stale in any technical sense, but from the tenant it looks exactly like a device that vanished. These accumulate fast in labs.
Network isolation
Devices moved to networks that block Microsoft endpoints, or taken offline into storage, stop checking in while remaining perfectly functional hardware. Cleanup here would be wrong; the local evidence matters precisely for this case.
User absence
Long-term leave, seasonal equipment, and docked devices that only power on occasionally produce low-activity devices that are not stale at all, which is why thresholds need business context, not just numbers.
Detection Script
The device script below is read-only and completes in a few seconds. It reports the three local activity signals with timestamps and ages, applies configurable thresholds to the two that can genuinely indicate staleness, and treats missing enrollment artifacts as its own distinct result because that is a different problem from inactivity.
PowerShell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
#Requires -RunAsAdministrator
[CmdletBinding()]
param(
[ValidateRange(1, 60)]
[int] $MaxSyncAgeDays = 3,
[ValidateRange(1, 90)]
[int] $MaxLogWriteAgeDays = 3,
[ValidateRange(1, 365)]
[int] $MaxBootAgeDays = 30
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
<#
.SYNOPSIS
Detection script: gathers LOCAL last-activity evidence that supports (or
contradicts) a stale-device verdict for this machine.
.DESCRIPTION
"Stale" is fundamentally a tenant-side question, answered by reporting
tools and Microsoft Graph. A device script cannot see the tenant, but it
can produce the local half of the evidence:
1. The newest LastRunTime across the EnterpriseMgmt sync tasks
(\Microsoft\Windows\EnterpriseMgmt\<GUID>\*) - the strongest local
signal of a recent Intune check-in.
2. The last write time of the IME log, when the IME is installed.
3. The last boot time - recorded as context only, since long uptime
says nothing about check-in health.
This script NEVER changes the system. Exit 0 = local activity is recent,
1 = local signals are stale beyond the thresholds, 2 = no EnterpriseMgmt
enrollment artifacts exist (cannot assess; enrollment-side problem),
3 = unexpected error.
#>$logDir = 'C:\ProgramData\Company\Logs'$logFile = Join-Path $logDir'StaleDeviceDetection.log'$imeLogPath = 'C:\ProgramData\Microsoft\IntuneManagementExtension\Logs\IntuneManagementExtension.log'function Write-DetectionLog {
param([Parameter(Mandatory)][string] $Level, [Parameter(Mandatory)][string] $Message)
if (-not (Test-Path $logDir)) { New-Item -Path$logDir -ItemType Directory -Force | Out-Null }
"$([DateTime]::UtcNow.ToString('o')) [$Level] $Message" | Out-File -FilePath$logFile -Append -Encoding utf8
}
try {
Write-DetectionLog 'INFO'"Stale device evidence collection started. Thresholds: sync=${MaxSyncAgeDays}d log=${MaxLogWriteAgeDays}d boot=${MaxBootAgeDays}d." # ---- Signal 1: newest EnterpriseMgmt sync task run ----$mgmtTasks = Get-ScheduledTask -TaskPath'\Microsoft\Windows\EnterpriseMgmt\*' -ErrorAction SilentlyContinue
if ($null -eq$mgmtTasks -or @($mgmtTasks).Count -eq0) {
Write-DetectionLog 'ERROR''No EnterpriseMgmt scheduled tasks exist on this device. Without enrollment artifacts there is no local check-in history to evaluate.'
exit 2
}
$newestRun = [DateTime]::MinValue
$newestTask = 'unknown'foreach ($taskin @($mgmtTasks)) {
$info = Get-ScheduledTaskInfo -TaskName$task.TaskName -TaskPath$task.TaskPath -ErrorAction SilentlyContinue
if ($null -ne$info -and$info.LastRunTime -gt$newestRun) {
$newestRun = $info.LastRunTime
$newestTask = "$($task.TaskPath)$($task.TaskName)"
}
}
$syncAgeDays = [math]::Round(((Get-Date) - $newestRun).TotalDays, 2)
Write-DetectionLog 'INFO'"Newest EnterpriseMgmt run: $newestRun (task: $newestTask), $syncAgeDays day(s) ago." # ---- Signal 2: IME log recency ----$logAgeDays = $nullif (Test-Path -LiteralPath$imeLogPath) {
$lastWrite = (Get-Item -LiteralPath$imeLogPath).LastWriteTime
$logAgeDays = [math]::Round(((Get-Date) - $lastWrite).TotalDays, 2)
Write-DetectionLog 'INFO'"IME log last write: $lastWrite, $logAgeDays day(s) ago."
}
else {
Write-DetectionLog 'INFO''IME log not present. The IME installs only when a Win32 app or required script targets the device; absence is not staleness evidence on its own.'
}
# ---- Signal 3: last boot (context only, never a verdict) ----$os = Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction SilentlyContinue
if ($null -ne$os) {
$bootAgeDays = [math]::Round(((Get-Date) - $os.LastBootUpTime).TotalDays, 2)
Write-DetectionLog 'INFO'"Last boot: $($os.LastBootUpTime), $bootAgeDays day(s) ago."if ($bootAgeDays -gt$MaxBootAgeDays) {
Write-DetectionLog 'WARN'"Uptime exceeds ${MaxBootAgeDays} days. Recorded as context: uptime does not indicate check-in health, but very long uptimes usually mean patching is not completing."
}
}
# ---- Verdict from the two activity signals ----$syncCutoff = (Get-Date).AddDays(-$MaxSyncAgeDays)
$logCutoff = (Get-Date).AddDays(-$MaxLogWriteAgeDays)
$staleReasons = @()
if ($newestRun -lt$syncCutoff) {
$staleReasons += "no EnterpriseMgmt sync task ran in the last $MaxSyncAgeDays day(s)"
}
if ($null -ne$logAgeDays -and$logAgeDays -gt$MaxLogWriteAgeDays) {
$staleReasons += "the IME log has not been written in the last $MaxLogWriteAgeDays day(s)"
}
if ($staleReasons.Count -eq0) {
Write-DetectionLog 'INFO''Local activity is recent. This device does not look stale from its own evidence.'
exit 0
}
Write-DetectionLog 'ERROR'"Local signals look stale: $($staleReasons -join '; '). Correlate with the tenant-side last check-in before any cleanup decision."
exit 1
}
catch {
Write-DetectionLog 'ERROR'"Unexpected failure: $($_.Exception.Message)"
exit 3
}
finally {
Write-DetectionLog 'INFO''Stale device evidence collection finished.'
}
Detection Logic
Three signals, evaluated with different weights on purpose:
Newest EnterpriseMgmt sync run. The script enumerates all scheduled tasks under \Microsoft\Windows\EnterpriseMgmt\* and takes the newest LastRunTime via Get-ScheduledTaskInfo. Every Intune check-in runs through these tasks, so this is the strongest local evidence of recent management activity. Older than $MaxSyncAgeDays (default 3) counts as stale.
IME log last write. When the IME is installed, the last write time of C:\ProgramData\Microsoft\IntuneManagementExtension\Logs\IntuneManagementExtension.log shows the last time the agent did any app or script work. Older than $MaxLogWriteAgeDays (default 3) counts as stale. When the log does not exist, the script says so explicitly and counts it as neutral, because a device never targeted with Win32 content has no IME and is not stale for that reason.
Last boot. Recorded from Win32_OperatingSystem.LastBootUpTime and compared to $MaxBootAgeDays (default 30) for a warning only. Long uptime is context, not staleness: a machine can be up for a year and still check in every eight hours, and uptime never determines the exit code.
Zero EnterpriseMgmt tasks produces exit code 2, which means the device cannot be assessed at all: there is no enrollment to be stale. Both activity signals recent is exit code 0; either signal beyond its threshold is exit code 1 with the reasons spelled out in the log.
The tenant-side companion: last check-in via Microsoft Graph
The device script cannot answer "when did this device last talk to Intune?" for the whole estate. That answer lives in Entra and Intune reporting. The following snippet is admin-side: run it interactively on a management workstation with the Microsoft Graph PowerShell SDK installed, with least-privilege consent (Device.Read.All is sufficient for reading device objects). The device resource property approximateLastSignInDateTime records roughly when the device last authenticated to Entra:
PowerShell
1
2
3
4
5
6
7
# Admin-side: run interactively on a management workstation, NOT on the device.# Requires: Microsoft.Graph.Authentication and Microsoft.Graph.Identity.DirectoryManagement SDK modules.
Connect-MgGraph -Scopes'Device.Read.All'$cutoff = (Get-Date).AddDays(-30).ToString('yyyy-MM-ddTHH:mm:ssZ')
Get-MgDevice -Filter"approximateLastSignInDateTime le $cutoff" -ConsistencyLevel eventual -All |
Select-Object DisplayName, OperatingSystem, OperatingSystemVersion, ApproximateLastSignInDateTime |
Sort-Object ApproximateLastSignInDateTime
Interpret this number for what it is: the device's last Entra authentication, not its last Intune sync. A device that signs in but does not sync has a management problem; a device that does neither is a cleanup candidate. Intune's own device list shows a last-check-in column, and for the automated end of the lifecycle, Intune's Device clean-up rules (in the admin center under Devices) can delete devices that have been inactive for a configured number of days. Use the Graph query and the device script's local evidence to sanity-check anything the automation is about to remove.
Deploying Through Intune
This is a detection-only package; choose No remediation script.
In the Microsoft Intune admin center, go to Devices → Scripts and remediations and select + Add on the Windows 10/11 tab.
On Basics, name it Audit - local device activity evidence, and describe it as a read-only staleness evidence collector.
On Detection script, upload detect.ps1. Set Run script in 64-bit PowerShell: Yes and Run this script using signed-on credentials: No; everything it reads is machine-scoped.
On the Remediation script step, choose No remediation script. A device that looks stale needs investigation, not local modification.
Leave Enforce script signature check: No unless you sign internal scripts.
On Schedule, Weekly fits the problem's tempo; staleness develops over weeks, and daily runs add noise without information.
On Assignments, target the full Windows fleet or, more usefully, the device groups your cleanup candidates live in. Exclude kiosk and lab device groups if their low activity is expected and documented.
Review and create.
Post-specific notes:
Tune thresholds to your fleet's reality before drawing conclusions; a field workforce with weekly office visits legitimately trips a 3-day threshold.
The Graph snippet is an admin workflow, not device content; never deploy Graph SDK code through a device script, because device scripts run as SYSTEM with no user tokens and no installed modules.
Devices reporting exit code 2 need enrollment repair flows, not cleanup.
Visual Walkthrough
Cleanup with evidence and audit trails — never rush deletion.
Lifecycle LoopStale Device Hygiene — Detect to Archive6/6
1
Detect last check-in
Devices silent > N days flagged by script/report.
verify
2
Verify via Graph
Cross-check lastSyncDateTime + user association.
classify
3
Classify
Lost / retired / re-imaged — each has its own action.
act
4
Retire or disable
Retire removes org data; disabled blocks sign-in.
archive
5
Archive + audit
Records retained for asset register + audit trail.
6
Re-run monthly
Hygiene is a schedule, not a project.
Continuous cycle
Stale Device Hygiene — Detect to Archive. 1. Detect last check-in: Devices silent > N days flagged by script/report. — 2. Verify via Graph: Cross-check lastSyncDateTime + user association. — 3. Classify: Lost / retired / re-imaged — each has its own action. — 4. Retire or disable: Retire removes org data; disabled blocks sign-in. — 5. Archive + audit: Records retained for asset register + audit trail. — 6. Re-run monthly: Hygiene is a schedule, not a project.
Exit Codes
Intune evaluates the detection script; any non-zero marks the device as an issue in the package's device status view.
Code
Meaning
0
Local activity signals are recent; the device does not look stale from its own evidence
1
One or more local activity signals are beyond the configured thresholds
2
No EnterpriseMgmt enrollment artifacts exist; the device cannot be assessed
3
Unexpected script error; inspect the log
Logging
The script writes to C:\ProgramData\Company\Logs\StaleDeviceDetection.log. Each line carries a UTC timestamp, a level (INFO, WARN, ERROR), and a message. Every signal logs its own line with the absolute timestamp and its age in days, so the file reads as a complete evidence record rather than a verdict; the task name behind the newest sync run is included so you can trace it in Task Scheduler. The file grows by around eight lines per run, which a weekly schedule keeps small. When you move to the cleanup stage, export this log next to the Graph query output; the pair is your audit trail for every device you retire.
Troubleshooting the Script
Exit code 2 on a device that is definitely enrolled. Confirm the script ran in 64-bit PowerShell and that \Microsoft\Windows\EnterpriseMgmt\ really is empty in Task Scheduler. An empty folder tree with a populated Enrollments key is a partial enrollment, and a different post in this series covers exactly that repair.
Every device flagged stale, including ones you used yesterday. Check the device's clock. A wrong system clock makes every age calculation wrong and can mask or fabricate staleness.
LastRunTime looks ancient but the device checks in. Some sync paths bypass the scheduled tasks, for example portal-triggered syncs that use push notifications. The task run time is evidence, not gospel; correlate with the tenant-side last check-in before judging.
The Graph query returns fewer devices than expected. Filtering on approximateLastSignInDateTime requires the property to be present; devices registered through Autopilot without ever enrolling may lack a usable value, and -ConsistencyLevel eventual is required for the filter to behave predictably on directory objects.
No log file appears. The script never ran; verify assignment, last check-in, and that the device is a Windows platform.
How to Validate the Fix
Since the deliverable is evidence quality, validation is a comparison exercise:
On an actively used device, run the script manually in an elevated 64-bit PowerShell console. Expect exit code 0 with sync and log ages measured in hours.
On a known-inactive device (or a test VM you stop for a week), expect exit code 1 with the stale reasons naming which signal expired.
Run the Graph snippet on your admin workstation and cross-check three devices: their ApproximateLastSignInDateTime values should be consistent with the local evidence the script reported.
Confirm one device with no IME installed produces a neutral IME line and a verdict based only on the sync signal, as designed.
In the admin center, compare the package's device status view against your manual results on a pilot group before trusting it fleet-wide.
Production Considerations
Execution context. The device script runs as SYSTEM in 64-bit Windows PowerShell and reads only machine-scoped state: scheduled task metadata, one file timestamp, and one CIM query.
Idempotency. Detection-only; nothing on the device changes between runs except time itself.
Side effects. None. The Graph snippet performs read queries only, but note that Graph queries run under your admin identity and are subject to your tenant's logging; run them from a workstation, not from a shared automation account without review.
Security. No credentials appear in the device script, and no Graph code runs on devices. The Graph snippet uses a least-privilege scope and no -Force style destructive parameters. Have your PowerShell automation SME and security team review both, and validate in a test ring before broad production rollout.
Cleanup discipline. Never delete device records based on one signal. The defensible workflow is tenant evidence plus local evidence, disable before delete, and a waiting period long enough to catch devices on extended leave.
Why can't the device script just ask Intune whether it is stale?
A device script has no tenant view, no user tokens, and no modules for cloud queries; it runs as SYSTEM in a constrained context by design. What it can do honestly is report its own last activity. The tenant-side half comes from Graph or the admin center, run by an admin, and the two together support a cleanup decision that survives review.
What exactly is approximateLastSignInDateTime?
It is a property on the Entra device resource that records the approximate time the device last authenticated to Entra ID. It is approximate by definition and updates only on authentication activity, so it complements, but does not replace, Intune's own last check-in reporting. Use both when deciding whether a device is genuinely gone.
Isn't Intune supposed to clean up stale devices automatically?
Intune offers Device clean-up rules that delete devices inactive for a configured number of days, and they work well when configured deliberately. The risk is configuration by accident: a rule set too aggressive deletes devices that are simply on long leave or in storage, along with their recovery keys and inventory history. Evidence-based review, which this package supports, is what keeps automated cleanup safe.
Comments
0No comments yet — be the first to share your thoughts.