Reinstall Android Apps in Intune using PowerShell and Graph API
|

Reinstall Android Apps in Intune using PowerShell and Graph API – Problem Fixed

Reading Time: 10 minutes

Learn how to bulk reinstall Android apps in Microsoft Intune using the changeAssignments Graph API action and PowerShell – with a real-world explanation of why the app comes back after removal.



The Observation That Started This

A while back Microsoft made a very useful feature available in Intune: the Remove apps and configuration remote action on Android devices (and iOS/iPadOS). With this action you can target an app specifically – or a configuration profile – and the app gets uninstalled… and after a while, it quietly comes back on its own.

At first glance this looks like a bug, since the remote action also has a Restore button. It is not. It is by design, and once you understand the mechanic behind it, you can actually use it intentionally to force a clean reinstall of an Android app – single device or in bulk.

That is exactly what this post is about.


What Is “Remove Apps and Configuration”?

Remove apps and configuration is a remote action in Intune that lets you temporarily uninstall apps and remove configuration profiles from a device. It is designed for troubleshooting – for example, when an app is stuck, misconfigured, or behaving unexpectedly and you want a clean slate without permanently touching the device’s assignments.

You can find it in the Intune admin center by navigating to:

Devices -> All devices -> [select Android or iOS device] -> Remove apps and configuration (at the top, at remote actions options/buttons)

From there you pick what you want to remove – apps, profiles, or both – and send the action.

Remote device action: remove apps and configuration - Reinstall Android Apps
Select App: remove apps and configuration - Reinstall Android Apps
Select App: remove apps and configuration - Reinstall Android Apps
Waiting: remove apps and configuration - Reinstall Android Apps

It supports the following platforms:

  • Android Enterprise corporate-owned dedicated (COSU)
  • Android Enterprise corporate-owned fully managed (COBO)
  • Android Enterprise corporate-owned work profile (COPE)
  • iOS/iPadOS

Note: This is not the same as permanently uninstalling an app by changing its assignment to “Uninstall”. The removal is temporary by design – which is exactly the mechanic we will use here.


Why Does the App Come Back? The Auto-Restore Mechanic

This is the part that trips people up – and also the part that makes this trick work.

According to Microsoft’s documentation:

“If no restore is initiated, Intune automatically reapplies the apps and configurations within 8-24 hours to ensure the device remains aligned with assignment intent.”

So the action is fundamentally temporary. Intune removes the app, marks it with a status of Removed in the monitoring page, and then – unless you manually restore it – re-evaluates the device’s assignments and reinstalls the app within 8-24 hours.

The device still belongs to the same groups, the app is still assigned as Required. Intune’s assignment engine eventually enforces that intent again, which is why the app “comes back.”

Practically speaking, you can use this intentionally to trigger a clean reinstall:

  1. Send the remove action for the app
  2. The app uninstalls from the device
  3. Intune auto-reapplies the assignment within 8-24h, reinstalling the app fresh
  4. If you need it faster, try to restart the device or manually restore from the monitoring page in the Intune portal

For a single device this is manageable from the portal. For multiple devices, you need automation.


The Graph API Behind It – changeAssignments

The “Remove apps and configuration” portal action is powered under the hood by the changeAssignments Microsoft Graph beta API action.

The endpoint is:

POST https://graph.microsoft.com/beta/deviceManagement/managedDevices/{managedDeviceId}/changeAssignments

The request body includes a deviceAssignmentItems array where each item specifies the itemId (the app or policy ID) and the itemType. For apps, itemType is "application".

A successful call returns HTTP 204 No Content – meaning the request was accepted by Intune and the action is queued.

The full API reference is here: changeAssignments action – Microsoft Graph beta


Graph Permissions Required

To call changeAssignments you need the following permission:

Permission typeRequired permission
Delegated (work or school account)DeviceManagementManagedDevices.PrivilegedOperations.All
ApplicationDeviceManagementManagedDevices.PrivilegedOperations.All

This is a privileged operation permission – not a standard read/write scope. Make sure you connect with it explicitly:

Connect-MgGraph -Scopes @(
    "DeviceManagementManagedDevices.ReadWrite.All",
    "DeviceManagementManagedDevices.PrivilegedOperations.All"
)

! Bulk Actions – Proceed Carefully

Stop and read this before running anything at scale.

Bulk PowerShell actions against production devices are powerful – and that cuts both ways. A wrong serial number file, a misconfigured scope, or targeting the wrong app can cause disruption across many devices simultaneously.

A few non-negotiable guardrails before you run this:

Use least privilege – always. The changeAssignments Graph action requires DeviceManagementManagedDevices.PrivilegedOperations.All. That is an elevated scope for a reason – do not connect with a Global Administrator account for routine operations. Create a dedicated service account with only the permissions this task actually needs.

For more on configuring RBAC in general: Intune – RBAC Custom roles

Test on one device first. Always. Run the script against a single known test device, verify the app uninstalls and confirm it reinstalls within the auto-restore window. Only then expand to your production list.

Review the results table after every run. The script collects per-device status and prints a summary at the end. Do not close the terminal without checking it. Keep the timestamped log file – it is your audit trail if something goes wrong.

Be deliberate about what you target. As noted in Microsoft’s documentation, removing apps tied to VPN or Wi-Fi connectivity could break the device’s path back to Intune, leaving it unable to receive the auto-restore. For standard Android Enterprise app reinstalls this is generally not a concern – but know what you are targeting before you run.


The Script – Reinstall Android Apps

The script is a clean, standalone PowerShell terminal script. You point it at a text file containing serial numbers (one per line), it fetches all Android apps from your tenant and lets you pick one, resolves the serials to Intune device IDs, and fires changeAssignments for each device.

You can also find it in my GitHub page here.

Set the two variables at the top before running:

$SerialNumberFilePath = "C:\Temp\serials.txt"   # One serial number per line
$LogPath              = "C:\Temp\AndroidAppReinstall_$(Get-Date -Format 'yyyyMMdd_HHmmss').log"

Here is the full script:

#Requires -Modules Microsoft.Graph.Authentication, Microsoft.Graph.DeviceManagement

<#
.SYNOPSIS
    Bulk reinstall an Android app on Intune-managed devices by triggering the
    changeAssignments remote action (equivalent to "Remove apps and configuration" in the portal).

.DESCRIPTION
    Reads a list of device serial numbers from a text file, resolves each to an
    Intune managed device ID, fetches all Android apps from the tenant, prompts
    the admin to pick one, and fires the changeAssignments Graph API call for
    every resolved device.

    Intune automatically reapplies the app assignment within 8-24 hours after
    removal, effectively reinstalling the app fresh.

.NOTES
    Required Graph permission: DeviceManagementManagedDevices.PrivilegedOperations.All,
    DeviceManagementApps.Read.All, DeviceManagementManagedDevices.ReadWrite.All
    Author : Paris Petsanas / systunation.com
#>

# ----------------------------------------------------------------
# SETTINGS - adjust before running
# ----------------------------------------------------------------
$SerialNumberFilePath = "C:\Temp\serials.txt"   # One serial number per line
$LogPath              = "C:\Temp\AndroidAppReinstall_$(Get-Date -Format 'yyyyMMdd_HHmmss').log"
# ----------------------------------------------------------------

#region Helpers

function Write-Log {
    param([string]$Message, [string]$Level = "INFO")
    $entry = "[$(Get-Date -Format 'u')] [$Level] $Message"
    Write-Host $entry -ForegroundColor $(switch ($Level) {
        "INFO"    { "Cyan"   }
        "SUCCESS" { "Green"  }
        "WARN"    { "Yellow" }
        "ERROR"   { "Red"    }
        default   { "White"  }
    })
    Add-Content -Path $LogPath -Value $entry -Encoding UTF8
}

function ConvertTo-SafeFilterString {
    param([string]$InputString)
    # Strip single quotes to prevent OData filter injection
    return $InputString -replace "'", ""
}

#endregion

#region Graph Connection

Write-Log "Checking Microsoft Graph connection..."
try {
    $ctx = Get-MgContext
    if ($null -eq $ctx) {
        Write-Log "No active session found - connecting..."
        Connect-MgGraph -Scopes @(
            "DeviceManagementManagedDevices.ReadWrite.All",
            "DeviceManagementManagedDevices.PrivilegedOperations.All",
            "DeviceManagementApps.Read.All"
        ) | Out-Null
        Write-Log "Connected to Microsoft Graph." "SUCCESS"
    } else {
        Write-Log "Already connected as: $($ctx.Account)" "SUCCESS"
    }
} catch {
    Write-Log "Failed to connect to Microsoft Graph: $($_.Exception.Message)" "ERROR"
    exit 1
}

#endregion

#region Validate serial file

if (-not (Test-Path -Path $SerialNumberFilePath)) {
    Write-Log "Serial number file not found: $SerialNumberFilePath" "ERROR"
    exit 1
}

$serials = Get-Content -Path $SerialNumberFilePath -Encoding UTF8 |
           Where-Object { $_ -match '\S' } |
           ForEach-Object { $_.Trim() }

if ($serials.Count -eq 0) {
    Write-Log "Serial number file is empty." "ERROR"
    exit 1
}

Write-Log "Loaded $($serials.Count) serial number(s) from file."

#endregion

#region Fetch Android apps

Write-Log "Fetching Android apps from Intune..."
try {
    $appsResponse = Invoke-MgGraphRequest -Method GET `
        -Uri "https://graph.microsoft.com/beta/deviceAppManagement/mobileApps" `
        -ErrorAction Stop

    $androidApps = $appsResponse.value | Where-Object {
        $_.'@odata.type' -match 'android'
    } | Select-Object displayName, id | Sort-Object displayName

    if ($androidApps.Count -eq 0) {
        Write-Log "No Android apps found in this tenant." "WARN"
        exit 1
    }

    Write-Log "Found $($androidApps.Count) Android app(s)." "SUCCESS"
} catch {
    Write-Log "Failed to fetch Android apps: $($_.Exception.Message)" "ERROR"
    exit 1
}

#endregion

#region App selection menu

Write-Host ""
Write-Host "=====================================================" -ForegroundColor Cyan
Write-Host "  Available Android Apps" -ForegroundColor Cyan
Write-Host "=====================================================" -ForegroundColor Cyan

for ($i = 0; $i -lt $androidApps.Count; $i++) {
    Write-Host "  [$($i + 1)] $($androidApps[$i].displayName)"
}

Write-Host ""
$selection = Read-Host "Enter the number of the app to reinstall"

if (-not ($selection -match '^\d+$') -or
    [int]$selection -lt 1 -or
    [int]$selection -gt $androidApps.Count) {
    Write-Log "Invalid selection: '$selection'" "ERROR"
    exit 1
}

$selectedApp = $androidApps[[int]$selection - 1]
Write-Log "Selected app: '$($selectedApp.displayName)' | ID: $($selectedApp.id)"

#endregion

#region Resolve serials to Intune device IDs

Write-Log "Resolving serial numbers to Intune managed device IDs..."

$deviceIds = [System.Collections.Generic.List[string]]::new()

foreach ($serial in $serials) {
    $safeSerial = ConvertTo-SafeFilterString -InputString $serial
    try {
        $dev = Invoke-MgGraphRequest -Method GET `
            -Uri "https://graph.microsoft.com/beta/deviceManagement/managedDevices?`$filter=serialNumber eq '$safeSerial'" `
            -ErrorAction Stop

        if ($dev.value -and $dev.value.Count -gt 0) {
            $deviceIds.Add($dev.value[0].id)
            Write-Log "Resolved: $safeSerial -> $($dev.value[0].id)"
        } else {
            Write-Log "Device not found for serial: $safeSerial" "WARN"
        }
    } catch {
        Write-Log "Error resolving serial '$safeSerial': $($_.Exception.Message)" "ERROR"
    }
}

if ($deviceIds.Count -eq 0) {
    Write-Log "No devices resolved. Nothing to do." "WARN"
    exit 1
}

Write-Log "Resolved $($deviceIds.Count) of $($serials.Count) device(s)."

#endregion

#region Confirmation

Write-Host ""
Write-Host "=====================================================" -ForegroundColor Yellow
Write-Host "  Summary" -ForegroundColor Yellow
Write-Host "=====================================================" -ForegroundColor Yellow
Write-Host "  App    : $($selectedApp.displayName)"
Write-Host "  Devices: $($deviceIds.Count) resolved"
Write-Host ""
$confirm = Read-Host "Proceed with changeAssignments for all $($deviceIds.Count) device(s)? (Y/N)"

if ($confirm -notmatch '^[Yy]$') {
    Write-Log "Action cancelled by user." "WARN"
    exit 0
}

#endregion

#region Send changeAssignments

Write-Log "Sending changeAssignments remote action..."

$results = [System.Collections.Generic.List[PSCustomObject]]::new()
$counter = 0

foreach ($deviceId in $deviceIds) {
    $counter++
    Write-Log "#$counter/$($deviceIds.Count) - Processing device: $deviceId"

    try {
        $uri  = "https://graph.microsoft.com/beta/deviceManagement/managedDevices('$deviceId')/changeAssignments"
        $body = @{
            deviceAssignmentItems = @(
                @{
                    itemId   = $selectedApp.id
                    itemType = "application"
                }
            )
        } | ConvertTo-Json -Depth 4

        $null = Invoke-MgGraphRequest -Method POST -Uri $uri -Body $body `
            -ContentType "application/json" -ErrorAction Stop

        Write-Log "Device $deviceId - action sent successfully." "SUCCESS"
        $results.Add([PSCustomObject]@{
            DeviceId = $deviceId
            Status   = "Requested"
            Message  = "OK"
        })
    } catch {
        Write-Log "Device $deviceId - failed: $($_.Exception.Message)" "ERROR"
        $results.Add([PSCustomObject]@{
            DeviceId = $deviceId
            Status   = "Error"
            Message  = $_.Exception.Message
        })
    }
}

#endregion

#region Results summary

Write-Host ""
Write-Host "=====================================================" -ForegroundColor Cyan
Write-Host "  Results" -ForegroundColor Cyan
Write-Host "=====================================================" -ForegroundColor Cyan

$results | Format-Table -AutoSize

$succeeded = ($results | Where-Object { $_.Status -eq "Requested" }).Count
$failed    = ($results | Where-Object { $_.Status -eq "Error" }).Count

Write-Log "Completed. Succeeded: $succeeded | Failed: $failed"
Write-Log "Log saved to: $LogPath"

Breaking Down the Key Parts

Serial File and Logging

At the top of the script you set $SerialNumberFilePath to your text file of serial numbers – one per line. The $LogPath is auto-stamped with date and time so you get a new log file every run. Both the console and the log file receive the same output, so you have a full audit trail of what was sent and what failed.

Input Sanitization

Every serial number read from the file passes through ConvertTo-SafeFilterString before being inserted into an OData filter. This strips single quotes from the input, which prevents a raw user value from breaking or manipulating the filter string. Small detail, but important when you are processing a file that could contain unexpected characters.

Fetching Android Apps

The script calls deviceAppManagement/mobileApps on the beta endpoint and filters the response client-side for entries whose @odata.type contains "android". This covers Managed Google Play apps, Line of Business Android apps, and Android Enterprise system apps in one shot.

App Selection Menu

Rather than requiring you to know the app’s GUID ahead of time, the script prints a numbered list and lets you pick by number at runtime. The selected app name and ID are confirmed in the log before anything is sent.

Serial to Device ID Resolution

Intune’s changeAssignments endpoint needs the Intune Device ID – not the serial number. The resolution loop queries managedDevices filtered by serialNumber for each entry in your file. Devices that are not found are logged as warnings and skipped. You see a resolved count vs total count before the confirmation prompt so you know exactly what will be targeted.

Confirmation Gate

Before sending anything, the script prints a summary and asks for explicit Y/N confirmation. This is not optional – bulk actions against production devices should always have a human checkpoint.

The changeAssignments Call

For each resolved device ID the script POSTs to:

https://graph.microsoft.com/beta/deviceManagement/managedDevices('{deviceId}')/changeAssignments

with a body of:

{
  "deviceAssignmentItems": [
    {
      "itemId": "<app-guid>",
      "itemType": "application"
    }
  ]
}

HTTP 204 back means Intune accepted the request. The result per device (Requested or Error) is collected and printed in a summary table at the end.


How to Monitor the Result

After running the script, head to the Intune admin center to track the outcome per device.

Navigate to: Devices -> All devices -> [select Android or iOS device] -> Overview

At the top of the device overview pane, select Remove apps and configuration. This opens the monitoring page for that device where you can see the below useful info:

ColumnWhat it tells you
NameThe app targeted
ActionRemove or Restore
StatusIn Progress / Removed / Restored / Error
Status detailAdditional error context when populated
StartedTimestamp the action was initiated

Once the status shows Removed, the app is off the device. Intune will automatically restore it within 8-24 hours when it re-evaluates the device’s assignments. If you want to speed this up you have two options: use the Restore button on this monitoring page to push the restore immediately, or trigger a device restart to make the device check in sooner and pick up the re-applied assignment.


Wrapping Up

The “Remove apps and configuration” action in Intune is not just a troubleshooting click – it is a supported, reversible mechanism to trigger an app reinstall on Android Enterprise devices. The auto-restore behavior that makes the app “come back” is Intune enforcing its own assignment intent, which is the whole point.

The script in this post automates that trigger in bulk: reads serials from a file, shows you a live app list to pick from, resolves everything to device IDs, and fires the Graph API action with a confirmation gate and full logging.

As always – test first, scope your RBAC properly, and always review your results.

Happy automating!


References and Documentation

Other Interesting Posts

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *