Automating Stale Device Cleanup and Alerts in Intune
A hands-on runbook showing junior engineers how to detect, alert on and safely remove stale Intune devices with rollback controls.

In this lesson
Table of Contents
Table of contents
Before you begin
- Microsoft Graph PowerShell SDK basics
- Azure Automation runbook fundamentals
- Intune device management concepts
Track this tutorial
Choose your current status and tick each safety check as you complete it. Sign in to sync progress between devices.
Current status
Before you apply the change
Confirm these production-safety controls during the tutorial.
#Operational requirement
You have been assigned ownership of stale device cleanup in Intune. This is not optional housekeeping. Inactive device records keep getting evaluated by Conditional Access and compliance policies, they consume licensed seats that show up on the next true-up invoice, and they become an audit finding the moment someone asks how many managed endpoints actually exist in production. Your task is to build a controlled pipeline that finds stale devices, reports them, alerts before anything is touched, and only deletes after a human approves. Treat this as a service operations control with evidence and a rollback path, not a one-off script you run once and forget.
#Prerequisites and permissions
Before you write anything, confirm the following is in place. Request an Entra ID app registration with the application permission DeviceManagementManagedDevices.ReadWrite.All, and get admin consent granted — this job runs unattended, so delegated permissions are not an option here. Provision an Azure Automation account with a system-assigned managed identity, and assign the Graph permission to that identity’s service principal, not to your own account. You will also need Contributor or Automation Operator rights on the Automation account itself to publish and schedule runbooks, plus write access to an alerting sink — a Teams incoming webhook is the minimum, an ITSM API token is preferred so tickets raise automatically rather than sitting in a chat log nobody rereads. Confirm the test tenant or scoped device group you will build against before you touch the production identity.
Do not build this against a personal delegated token. It will work fine in testing and then fail silently three months later when the token expires or the owner leaves the business, and that failure looks nothing like a permissions error inside a scheduled runbook log. Use the managed identity pattern from day one; there is no acceptable shortcut here.
#Staged implementation
Build this in four stages, in order, and do not skip ahead to the destructive step because the query stage looked easy. Each stage produces evidence you will need later, and each stage is a checkpoint where you decide whether to continue or stop and ask.
#Stage 1 — Agree the staleness threshold in writing
Get sign-off from your manager on a numeric threshold before touching PowerShell. Sixty days since last sync is the common baseline for laptop fleets; shared or kiosk devices often need a longer window because they legitimately go quiet over holiday periods. Write the number, the rationale and the approver’s name into a change ticket, not a chat message. A script built on an undocumented assumption gets argued over during the first incident review, and you will lose that argument without a ticket to point at.
#Stage 2 — Query candidates with Microsoft Graph
Connect using the managed identity and pull managed devices filtered on last sync and enrolment age. The 14-day enrolment floor stops a device that enrolled yesterday from being flagged before it has even had a chance to check in once.

1Connect-MgGraph -Identity
2
3$threshold = (Get-Date).AddDays(-60)
4$devices = Get-MgDeviceManagementManagedDevice -All |
5 Where-Object { $_.LastSyncDateTime -lt $threshold -and $_.EnrolledDateTime -lt (Get-Date).AddDays(-14) }
6
7$devices | Select-Object DeviceName, Id, UserPrincipalName, LastSyncDateTime, OperatingSystem |
8 Export-Csv -Path 'stale-devices-report.csv' -NoTypeInformationNothing is deleted at this stage. Store the CSV with a timestamp so you can compare runs later. On the first run, manually check the count against a device you already know is retired. If the candidate list is more than roughly five per cent of the fleet, stop before proceeding — that usually points to a policy failing to check in tenant-wide, which is a separate incident, not a cleanup job. Watch for Graph throttling on large tenants too; if pagination stalls, the -All switch will simply take longer rather than fail, so budget runtime accordingly.
#Stage 3 — Build the runbook with a mandatory dry-run default
Wrap the deletion call in a parameterised runbook that defaults to dry-run. Never let the default parameter value be false; that single flag is your primary safety control.
1param(
2 [bool]$DryRun = $true
3)
4
5Connect-MgGraph -Identity
6$threshold = (Get-Date).AddDays(-60)
7$stale = Get-MgDeviceManagementManagedDevice -All |
8 Where-Object { $_.LastSyncDateTime -lt $threshold }
9
10foreach ($device in $stale) {
11 if ($DryRun) {
12 Write-Output "[DRY-RUN] Would remove: $($device.DeviceName) / $($device.Id)"
13 } else {
14 Remove-MgDeviceManagementManagedDevice -ManagedDeviceId $device.Id
15 Write-Output "[EXECUTED] Removed: $($device.DeviceName) / $($device.Id)"
16 }
17}Schedule only the dry-run and report path. The execution path — the one that sets DryRun to false — must be started manually by an operator after reviewing the report, never by a schedule. This is where most people trip on this task: they automate detection correctly and then, almost by accident, automate the trigger for deletion as well, which removes the human gate entirely and turns a control into a liability.
#Stage 4 — Wire the alert and the ITSM record
On completion of every dry-run job, post a summary to Teams and, where your ITSM platform supports it, raise a change record automatically so the paper trail exists without anyone having to remember to create it.
1$body = @{
2 text = "Stale device sweep found $($stale.Count) candidates for removal. Review CSV before manual execution."
3} | ConvertTo-Json
4
5Invoke-RestMethod -Uri $env:TEAMS_WEBHOOK_URL -Method Post -Body $body -ContentType 'application/json'Test the webhook against a run that finds zero candidates. It must send “0 candidates found”, not stay silent. A silent zero-result run looks identical to a broken webhook from the operator’s chair, and that gap is exactly where trust in the automation erodes and people quietly go back to checking manually.
#Monitoring and change control
Add the dry-run job itself to your existing Automation account monitoring so a failed run raises its own alert — a stale device sweep that quietly stops running is worse than no sweep at all, because nobody notices until the licensing bill arrives with a number nobody can explain. Review job history weekly for the first month, then monthly once the pattern settles down. Every execution run, however small, goes against the change ticket opened in Stage 1; do not let this drift outside change control just because it feels like routine cleanup. If the threshold ever needs adjusting, route that adjustment through the same ticket process as the original approval, not a quiet edit to a script variable that nobody else sees.

#Verification
Confirm the following exists before you consider this complete: a published runbook with working dry-run and execution parameters, the documented threshold with a named approver, at least one clean dry-run log showing a plausible candidate count, a confirmed Teams alert delivery including the zero-result test, and one manually approved execution run against a small batch of genuinely retired test devices — never production hardware for your first live run. Keep the CSV exports and job logs alongside the change ticket reference so an auditor can trace any deleted device back to who approved its removal and when.
#Failure Modes
| Symptom | Likely cause | Fix |
|---|---|---|
| Runbook fails on the Graph call | Managed identity has no application permission, or admin consent was never completed | Grant the permission to the identity’s service principal and complete admin consent in Entra ID |
| Dry-run count stays near zero despite known stale hardware | Local time is being compared against a UTC field | Normalise both sides to UTC before filtering |
| Execution run removes devices still in active use | Threshold too aggressive, or a service outage inflated sync gaps fleet-wide | Check Intune service health before executing; widen the threshold temporarily if an outage is confirmed |
| Teams alert never arrives | Webhook URL rotated or the connector was removed from the channel | Regenerate the webhook and update the stored variable |
| Licence count does not drop after removal | Only the Intune object was deleted, not the underlying Entra ID device object | Also remove the corresponding Entra ID device record, or confirm how your licensing model counts enrolment |
#Rollback
A deleted managed device object cannot be restored — it has to re-enrol. Plan for this before you ever run the execution path for real. Keep the pre-execution CSV for a minimum of ninety days for every run, without exception. If a device is removed in error, re-enrol it through your standard Autopilot or bulk enrolment process, reapply its compliance and configuration policy assignments, and re-add it to any group-based licensing it previously held. Log the device ID and both timestamps — removal and re-enrolment — against the original change ticket. Disable the scheduled dry-run trigger while you investigate the cause, and only re-enable it once the threshold or filter logic responsible has been corrected and reviewed by someone other than you. Confirm the licence is genuinely reclaimed by checking the group assignment again after re-enrolment, not just the device status.
#Hands-on task
Build the four-stage pipeline above against a test tenant or a scoped test device group. Run the dry-run job twice on different days, confirm the Teams alert fires both times including one zero-result run, then manually execute removal against two devices you have deliberately unenrolled for the exercise. Capture the CSV, both job logs and the Teams alert record, then write a one-paragraph rollback note as if one of those two devices had been removed by mistake. That note is the artefact that proves you understand the control, not just the script.
#Operational Summary
The script is the easy part. The discipline is in the gate between detection and deletion, the alert that forces someone to look before anything is removed, and the evidence trail that lets you answer an audit question six months later without reconstructing events from memory. Keep the dry-run and execution paths separate, keep the threshold under change control rather than buried in a variable, and never let a schedule reach the destructive branch unattended. Apply the same detect, alert, approve, execute, evidence pattern the next time you are asked to clean up disabled service accounts or orphaned Conditional Access exclusions — it is the same control, just pointed at a different object type, and the operator who trusts your alerts today will trust the next one you build as well.
Comments
Add a thoughtful note on Automating Stale Device Cleanup and Alerts in Intune. Comments are checked for spam and held for moderation before appearing.
Related articles
Automation and Service Operations
How to Validate a systemd Automation and Service Operations Task
Learn to build, validate and safely recover a bounded systemd automation task using explicit evidence, safe exercises and clear rollback steps.
Security & Operations
Designing a Verifiable Security Workflow with Microsoft Defender
A bounded, five-stage Defender security operations workflow scoped to a test device group, with read-only checks, one reversible response, and a rehearsed rollback path.
Systems Engineering
Engineering Tech Fundamentals for Predictable Linux Operations
A bounded systemd service workflow on Linux: unit architecture, sequential implementation, observable validation, common failure modes, least-privilege security and a rehearsed rollback path.
Discover more
Graduate Learning
Learn More About KBY
About KBY
Learn about our mission, editorial standards, and commitment to trusted engineering knowledge.
Why Trust KBY
Explore the processes and policies that ensure our publications are accurate, useful, and responsible.
Newsletter
Get our latest editorial publications, research and practical insights sent directly to your inbox.
Was this useful?
Build practical engineering skills.
Receive new lessons, learning paths, practical exercises and early-career guidance.