Build a Microsoft 365 inactive users report with PowerShell (2026)
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.
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.
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
createdDateTimeis 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.