Build a Microsoft 365 inactive users report with PowerShell (2026)

Microsoft 365 cost optimization · ~6 min read

Inactive licensed users are one of the most common sources of Microsoft 365 waste. Here's how to build a clean inactive-users report with the Microsoft Graph PowerShell SDK — including the one property that trips everyone up.

Setup: Install-Module Microsoft.Graph -Scope CurrentUser, then connect with read-only scopes. The key property, signInActivity, requires Microsoft Entra ID P1 plus AuditLog.Read.All.

The property that matters: signInActivity

Each user object can carry a signInActivity with two fields:

  • lastSignInDateTime — last interactive sign-in (a human logging in).
  • lastNonInteractiveSignInDateTime — last token/background sign-in (apps, mail clients).

For "is this person actually using their license," interactive sign-in is the honest signal. Non-interactive can stay warm long after someone's gone (a phone still syncing mail), so don't rely on it alone.

The gotcha: on a tenant without Entra ID P1, asking for signInActivity returns a 403 for the entire Get-MgUser query — not just a null field. Wrap it: try with the property, and on failure retry without it (you can still report disabled-but-licensed and never-assigned seats).

The report

Connect-MgGraph -Scopes "User.Read.All","AuditLog.Read.All","Organization.Read.All"

$days   = 30
$cutoff = (Get-Date).AddDays(-$days)

$users = Get-MgUser -All -Property `
  displayName,userPrincipalName,accountEnabled,createdDateTime,assignedLicenses,signInActivity

$users |
  Where-Object { $_.AssignedLicenses.Count -gt 0 } |   # licensed only
  Select-Object displayName, userPrincipalName, accountEnabled,
    @{n='LastSignIn'; e={ $_.SignInActivity.LastSignInDateTime }},
    @{n='Status'; e={
        $last = $_.SignInActivity.LastSignInDateTime
        if (-not $last) { 'never signed in' }
        elseif ($last -lt $cutoff) { 'inactive' }
        else { 'active' }
    }} |
  Export-Csv .\m365-inactive-users.csv -NoTypeInformation

Two refinements

  • Don't flag brand-new accounts. A user created last week with no sign-in isn't "inactive" — exclude anyone whose createdDateTime is newer than your cutoff.
  • No Entra P1? You can't read sign-in activity, but Microsoft 365 usage reports (via the Reports API) give activity by workload as a fallback — coarser, but enough to spot dormant accounts.

Turn the report into dollars

A list of inactive users is useful; a dollar figure gets action. Join each user's licenses to your price table and sum the reclaimable spend — that's the number that justifies the cleanup. (how the dollar math works →)

Skip the scripting

SeatScout builds the inactive-user report (with the P1 fallback handled), prices every seat, and adds disabled/unassigned/downgrade findings — read-only, in your tenant. Free tier available.

Get SeatScout →

Related: How to remove unused licenses · License audit checklist

SeatScout is independent and not affiliated with Microsoft.