Skip to content

Keep your Microsoft Intune tenant clean and tidy /w Azure Automation & Graph API

Nowadays Microsoft provides us a lot of flexibility to empower end-users to be productive as never before. Users are able to register their devices in order to access corporate resources anytime, anywhere on devices they love. Provisioning of Windows 10 devices to your enterprise has never been easier for end-users. They are even able to join their brand new devices to the corporate from home taking benefit of Windows Autopilot & Azure AD MDM auto-enrollment.

From an end-user perspective this is great, productivity can be restored in minutes instead of hours or even days. However the flexibility we provide for the end-users has a downside from an IT Admin perspective. As we’re able to join or register devices to Microsoft Intune/Azure AD, it causes a lot of obsolete device objects in your tenants.

Currently Microsoft Intune/Azure AD doesn’t provide a mechanism to automaticaly delete obsolete/stale records (yet). Now it’s a manual task. This is a challenge for an IT Admin to keep up with a clean and tidy Microsoft Intune/Azure AD tenant. With the introduction of Graph API new capabilities were introduced to delete obsolete/stale device records by using automation. With Microsoft Intune PowerShell sample scripts (thanks again Dave!) we have great inspiration to automate any form of day-to-day operations such as “housekeeping”.

Solution outline

As mentioned the solution is based on Azure Automation, PowerShell and Microsoft Graph API. In order to perform actions to Microsoft Intune/Azure AD we need to unattended authenticate to Intune Graph API/Azure AD. In this blog post I’ll not explain how to set up the perquisites to use Azure Automation for this purpose as Oliver Kieselbach wrote a great and detailed blog post how to achieve this. Our starting point of the solution is to have this in place before we can continue and run the housekeeping script on a recurring base.

With the housekeeping script we can delete device objects based on their device state, device compliance state, management channel and the number of days devices hasn’t synced/connected to Microsoft Intune. Using these input parameters we have a fine grained filter to perform the housekeeping job in a recurring way.

WARNING! Using incorrect parameters can result in deleting all device objects in your tenant! For safety reason I have commented the invoke & delete actions.

To better understand the working of the PowerShell script hereby a brief outline.

  1. Get input parameters (criteria)
  2. Connect to Microsoft Intune
  3. Query Microsoft Intune Graph API
  4. Delete device objects in Intune
  5. Connect to Azure AD
  6. Delete corresponding device objects in Azure AD

Input parameters

In order to run the script we have to define the criteria of deleting device objects.

  • Number of days not connected/synced to Microsoft Intune (mandatory);
  • Device management channel (‘eas’, ‘mdm’, ‘easMdm’, ‘configurationManagerClientMdm’);
  • Device compliance state (‘compliant’, ‘noncompliant’, ‘unknown’, ‘configManager’);
  • Device management state (‘managed’, ‘wipePending’, ‘retireIssued’, ‘retirePending’):


In this example we will delete device objects which hasn’t connected/contacted for at least 60 days, compliance state noncompliant and management state managed.

First add a Runbook as part of Azure Automation, provide a descriptive name and select PowerShell as Runbook Type and provide a description per your convenience.

Now we created the Runbook we can paste the housekeeping script below and publish it.

Delete obsolete/stale device objects from Microsoft Intune/Azure AD
Based on input parameters ('management agent', 'compliance state' and 'management state', 'Days last synced') the script is used to perform "housekeeping" to keep your Microsoft Intune/Azure AD clean and tidy of obsolete/stale device objects.
The script deletes device objects based on their device state, device compliance state, management channel and the number of days devices hasn't synced/connected to Microsoft Intune.
Name: Ronny de Jong
Version: 1.0
Dave Falkus (Microsoft) – Thanks for the inspiration provided on GitHub
Dennis van den Akker (InSpark) – A highly valued colleague and a great PowerShell hero
This posting is provided "AS IS" with no warranties, and confers no rights. Misuse can have great impact and lead to (unintential) removal of all device objects.
Using incorrect parameters can result in deleting all device objects in your tenant! For safety reason I have commented the invoke & delete actions.
#Provide input parameters
param (
[Parameter (Mandatory=$True)]
[Parameter (Mandatory=$False)]
[ValidateSet('eas', 'mdm', 'easMdm', 'configurationManagerClientMdm', ignorecase=$True)]
[Parameter (Mandatory=$False)]
[ValidateSet('compliant', 'noncompliant', 'unknown', 'configManager', ignorecase=$True)]
[Parameter (Mandatory=$False)]
[ValidateSet('managed', 'wipePending', 'retireIssued', 'retirePending', ignorecase=$True)]
#Retrieve Microsoft Intune tenant information
$intuneAutomationCredential = Get-AutomationPSCredential -Name IntuneAutomation
$intuneAutomationAppId = Get-AutomationVariable -Name IntuneClientId
$tenant = Get-AutomationVariable -Name AzureADTenantId
#Import Azure AD PowerShell for Graph (GA)
$AadModule = Import-Module -Name AzureAD -ErrorAction Stop -PassThru
#Filter for the minimum number of days where the device hasn't checked in
$days = $DaysLastSyncDate
$daysago = "{0:s}" -f (get-date).AddDays(-$days) + "Z"
$CurrentTime = [System.DateTimeOffset]::Now
#Authenticate with the Graph API REST interface
$adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll"
$adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll"
[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null
[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null
$redirectUri = "urn:ietf:wg:oauth:2.0:oob"
$resourceAppIdURI = ";
$authority = "$tenant&quot;
try {
$authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority
# Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession
$platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto"
$userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($intuneAutomationCredential.Username, "OptionalDisplayableId")
$userCredentials = New-Object Microsoft.IdentityModel.Clients.ActiveDirectory.UserPasswordCredential -ArgumentList $intuneAutomationCredential.Username, $intuneAutomationCredential.Password
$authResult = [Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContextIntegratedAuthExtensions]::AcquireTokenAsync($authContext, $resourceAppIdURI, $intuneAutomationAppId, $userCredentials);
if ($authResult.Result.AccessToken) {
$authHeader = @{
'Content-Type' = 'application/json'
'Authorization' = "Bearer " + $authResult.Result.AccessToken
'ExpiresOn' = $authResult.Result.ExpiresOn
elseif ($authResult.Exception) {
throw "An error occured getting access token: $($authResult.Exception.InnerException)"
catch {
throw $_.Exception.Message
$Filters = "";
# Days Last sync
if (![System.String]::IsNullOrEmpty($DaysLastSyncDate)) {
if ($Filters.Length -gt 0) {
$Filters = "$($Filters) and (lastSyncDateTime le '$($daysago)')";
} else {
$Filters = "lastSyncDateTime le $($daysago)";
# Management agent
if (![System.String]::IsNullOrEmpty($managementAgent)) {
if ($Filters.Length -gt 0) {
$Filters = "$($Filters) and (managementAgent eq '$($managementAgent)')";
} else {
$Filters = "(managementAgent eq '$($managementAgent)')";
# Compliance state
if (![System.String]::IsNullOrEmpty($complianceState)) {
if ($Filters.Length -gt 0) {
$Filters = "$($Filters) and (complianceState eq '$($complianceState)')";
} else {
$Filters = "(complianceState eq '$($complianceState)')";
# Management state
if (![System.String]::IsNullOrEmpty($managementState)) {
if ($Filters.Length -gt 0) {
$Filters = "$($Filters) and (managementState eq '$($managementState)')";
} else {
$Filters = "(managementState eq '$($managementState)')";
$Url = ";;
if ($Filters.Length -gt 0) {
$Url += "?`$filter=$($Filters)";
#Write-Output $url
$Results = (Invoke-RestMethod -Headers $authHeader -Method Get -Uri $Url).value;
if ($Filters.Length -eq 0) {
Write-Output "Found $($Results.Count) devices";
} else {
Write-Output "Found $($Results.Count) devices with the following filters";
Write-Output " Days last synced: $($daysago)";
Write-Output " Management agent: $($managementAgent)";
Write-Output " Compliance state: $($complianceState)";
Write-Output " Management state: $($managementState)";
if ($Null -ne $Results) {
Write-Output "Loading Azure AD module";
#try {
# Refresh the credentials as the password is probably empty
$intuneAutomationCredential = Get-AutomationPSCredential -Name IntuneAutomation
$AadModule = Import-Module -Name AzureAD
Connect-AzureAd -Credential $intuneAutomationCredential
#catch {
# throw 'AzureAD PowerShell module is not installed!'
Write-Output "Azure AD module loaded";
#$Results | Select userPrincipalName,lastSyncDateTime,managementagent,managementstate,complianceState,deviceName,operatingSystem,deviceType,deviceEnrollmentType | Format-Table -AutoSize;
#$Response = Read-Host -Prompt "Remove the device(s) (y/n)?";
$Response = "y";
if ($Null -ne $Response -and $Response.Length -gt 0 -and $Response -eq 'y') {
$ResultsGrid = @();
#Write-Output "Removing $($Results.Count) device objects from Microsoft Intune/Azure AD:";
foreach ($Item in $Results) {
$GVResults = New-Object -TypeName PSObject;
#$GVResults | Add-Member NoteProperty "userPrincipalName" -Value $Item.userPrincipalName;
$GVResults | Add-Member NoteProperty "lastSyncDateTime" -Value $Item.lastSyncDateTime;
$GVResults | Add-Member NoteProperty "managementagent" -Value $Item.managementagent;
$GVResults | Add-Member NoteProperty "managementstate" -Value $Item.managementstate;
$GVResults | Add-Member NoteProperty "complianceState" -Value $Item.complianceState;
#$GVResults | Add-Member NoteProperty "deviceName" -Value $Item.deviceName;
#$GVResults | Add-Member NoteProperty "operatingSystem" -Value $Item.operatingSystem;
$GVResults | Add-Member NoteProperty "deviceType" -Value $Item.deviceType;
$GVResults | Add-Member NoteProperty "deviceEnrollmentType" -Value $Item.deviceEnrollmentType;
#Write-Output "$($Item.userPrincipalName)";
#Invoke-RestMethod -Headers $authHeader -Method Post -Uri "$($; | Out-Null;
#Invoke-RestMethod -Headers $authHeader -Method Delete -Uri "'$($;)" | Out-Null;
try {
if (($aadDevice = Get-AzureADDevice -SearchString $Item.deviceName | Where-Object -FilterScript {$_.DeviceId -eq $Item.azureADDeviceId}) -ne $null) {
#Write-Output "Found AAD device '$($aadDevice.DisplayName)' with device id: $($aadDevice.DeviceId)";
$GVResults | Add-Member NoteProperty "DisplayName" -Value $aadDevice.DisplayName;
#Remove-AzureADDevice -ObjectId $aadDevice.ObjectId
#Write-Output "=> deleted AAD device '$($aadDevice.DisplayName)'";
} else {
$GVResults | Add-Member NoteProperty "DisplayName" -Value "";
Write-Output "No corresponding Azure AD Device object(s) found with DisplayName '$($Item.deviceName)' and DeviceId '$($Item.azureADDeviceId)'";
catch {
throw $_.Exception.Message
$ResultsGrid += $GVResults;
Write-Output "Removing $($Results.Count) device objects from Microsoft Intune/Azure AD:";
$ResultsGrid | Format-Table;
} else {
Write-Output "Device(s) will not be removed";
} else {
Write-Output "No device(s) selected";

After the Runbook has been published we can schedule it. By scheduling the runbook, obsolete device objects can be deleted on a recurring base. Your Intune tenant hasn’t be that clean and tidy as never before!

The schedule can be defined based on your needs and in this example we will schedule the housekeeping script once a day.

After completing the schedule we can define the parameters for the housekeeping script. The DaysLastSyncDate is mandatory and be aware to provide the correct value. A value of 0 will delete all device objects in Intune!

The result

Now we have implemented the solution it is time to show the results. Therefore we start the runbook manually and provide the parameters mentioned in our example (DaysLastSyncDate=60, ManagementAgentState=mdm, CompliantState=noncompliant, ManagementState=managed). Once the runbook complete successfully the results speak for themselves. 51 device objects were found which met our criteria and got deleted from both Microsoft Intune & Azure AD.

Housekeeping wasn’t that easy & fun before!

Now we have validated the solution we can schedule one or multiple runbooks with different criteria and just monitor the jobs to keep your Microsoft Intune tenant(s) clean & tidy.


We are aiming to enable end-users to do more and keep them happy. At the same time we want to keep our IT Admins happy as well to ease their jobs. In this blog post we used Azure Automation to schedule & execute PowerShell runbooks. The runbook contains PowerShell script to query Microsoft Intune & based on  the input parameters, device objects got deleted from both Microsoft Intune & Azure AD. It is just an example of the almost unlimited possibilities and taking advantage to bring the mentioned technology together.


Microsoft Intune Graph API sample scripts

Olivier Kieselbach how to connect interactively to Azure AD/Microsoft Intune

Process automation for Intune and Azure AD with Azure Automation

Special thanks to my colleague Dennis van Akker who helped me out to do some PowerShell magic. Fun it was Dennis!


5 thoughts on “Keep your Microsoft Intune tenant clean and tidy /w Azure Automation & Graph API Leave a comment

  1. Ronny, thanks for sharing the script and for the demo at techsummit Amsterdam of the Intune cleanup script and building out this script to a total (azure ad & intune) cleanup script. (and for the heads-up ;) )

      • I’ve customized it to our needs and the script provided a good basis. Thanks for sharing Ronny.

Leave a Reply to RkastCancel Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: