Claude vs PowerShell vs admin center

28 Oct 2026

Three ways to answer "who hasn't used their licence?"

It's the most common licence question there is, and Microsoft 365 gives you two official ways to answer it: export a usage report from the admin centre, or pull the same report with Microsoft Graph PowerShell. We added a third: ask Claude through the GraphPaaS connector. All three read the same Microsoft report underneath. They differ in how much work they leave you, and in what they quietly get wrong.

The Claude answers below come from Fabrikam HQ, our demo tenant (synthetic people, real tool calls, data collected on 25 October at 05:45 UTC). "Unused" means no activity in Exchange, OneDrive, SharePoint or Teams for 90 days, so before 27 July.

42
people holding a paid licence
10
idle for 90 days or more
2
disabled accounts an activity filter misses
1
Microsoft report under all three

The admin centre: an export and a spreadsheet

The report lives at Reports › Usage › Microsoft 365 apps, on the Active users tab. Its table has a last active date per service (Exchange, OneDrive, SharePoint, Viva Engage, Teams) and a yes/no column per service licence. There's no single "last active" column, so the question can't be answered on screen. You select Export, open the CSV, and build the latest of the date columns yourself.

Two details decide whether that sheet is right. The licence columns say whether an Exchange or Teams licence is assigned, not which product or what it costs. And the table has no account state, so a disabled account that still holds a licence only shows up if it also went quiet. Anyone with Global Admin, Exchange Admin, SharePoint Admin, Teams Admin or Reports Reader can do this, among the roles Microsoft lists. Usage Summary Reports Reader can open the report but sees no user details.

PowerShell: a file, not objects

The Graph route is getOffice365ActiveUserDetail, which needs Reports.Read.All. Signed in as yourself, that's not enough on its own: for delegated calls, Microsoft also requires the user to hold one of a short list of admin or reader roles, and the permission needs admin consent. In the Graph PowerShell SDK it's Get-MgReportOffice365ActiveUserDetail, and -OutFile is mandatory: the API answers with a redirect to a CSV, so the cmdlet writes a file and you read it back.

Connect-MgGraph -Scopes "Reports.Read.All"
Get-MgReportOffice365ActiveUserDetail -Period D90 -OutFile .\active.csv

$cut  = (Get-Date).AddDays(-90)
$cols = 'Exchange Last Activity Date','OneDrive Last Activity Date',
        'SharePoint Last Activity Date','Teams Last Activity Date'
Import-Csv .\active.csv | Where-Object {
    $row  = $_
    $last = $cols | Where-Object { $row.$_ } | ForEach-Object { [datetime]$row.$_ } |
            Sort-Object | Select-Object -Last 1
    $row.'Assigned Products' -and $row.'Is Deleted' -ne 'True' -and
        (-not $last -or $last -lt $cut)
} | Select-Object 'User Principal Name', 'Assigned Products'

The -Period looks like the 90-day filter, but it isn't one. Microsoft says the last activity date is the most recent intentional activity whatever period you pick, so the cut-off has to be in your script. So does the next problem: Assigned Products is a list of product names. Users can sign up for free self-service subscriptions for themselves, and nothing in the CSV marks those as free. Someone holding only a free product who never opens it lands on your waste list. You need a price list to tell them apart, and the script grows.

ONE REPORT, THREE ROUTES TO "WHO HASN'T USED THEIR LICENCE?" getOffice365ActiveUserDetail · Microsoft's active users report last activity per service: Exchange, OneDrive, SharePoint, Teams · published once a day Conceal user names (on by default): hashes on all three routes until a Global Admin unticks it ADMIN CENTRE GRAPH POWERSHELL CLAUDE + GRAPHPAAS Reports › Usage Active users tab Export CSV of every user Spreadsheet latest of the date columns A list, no account state licences shown as yes/no Connect-MgGraph Reports.Read.All + a role -Period D90 -OutFile a file, not objects Import-Csv + filter a script you maintain A list, free products in no account state either One sentence the question, as you'd say it get_metrics licence_waste · paid only licence_advisor account state joined in 10 idle + 2 disabled Fabrikam HQ (demo) Same source and same concealment switch. The difference is what each route joins to the report.

Claude: one sentence, two receipts

You ask

Who in Fabrikam HQ hasn't used their licence in 90 days?

Claude calls
describe_domain(domain="licence_waste", tenant_id="ce48e256-019f-4f8c-b951-0c52d1d65b8e")
→ available · 42 rows · daily
get_metrics(domain="licence_waste", tenant_id="ce48e256-019f-4f8c-b951-0c52d1d65b8e",
            where={"last_activity": null},
            columns=["display_name", "assigned_products", "first_assigned", "last_activity"])
→ 5 rows
get_metrics(domain="licence_waste", tenant_id="ce48e256-019f-4f8c-b951-0c52d1d65b8e",
            where={"last_activity": {"<": "2026-07-27"}},
            columns=["display_name", "assigned_products", "last_activity"])
→ 5 rows
Claude answers

Ten of the 42. Five have no activity date at all: Ana Dias, João Yilmaz, Sofia Dias, Bernardo Yilmaz and Kelly Dias. Five were last active before 27 July: Clara Fonseca (1 March), Hugo Keller (14 April), Olivia Ramos (19 May), Jonas Moreira (22 June) and Vera Yilmaz (29 June). Eight of the ten hold Microsoft 365 E3 plus EMS E3; Ana and Jonas hold E3 alone.

The licence_waste domain is the same Microsoft report, with the two problems above already handled: it keeps only people holding a paid licence (free and trial products alone don't count) and works out one last activity date per person. At the demo's sample catalogue prices (€432 a year for E3, €120 for EMS E3), those ten hold €5,280 a year. That figure is ours, multiplied from the catalogue the tool returned; your own catalogue gives your own number.

You ask

Is anyone licensed whose account is switched off?

Claude calls
licence_advisor(tenant_id="ce48e256-019f-4f8c-b951-0c52d1d65b8e",
                state="remove", window_days=90)
→ 2 rows · reason "account disabled"
Claude answers

Two: Diego Yilmaz (E3 and EMS E3, €552 a year) and Uma Ramos (E3, €432). Both accounts are disabled, and neither is in the list of ten.

That second answer is the one the other two routes can't give. Uma's last activity in the report is 14 October, so any activity filter, in a spreadsheet or in PowerShell, lets her through. The usage report has no column for account state, so finding her means a second export and a join by hand. The connector joins it for you.

What all three get wrong the same way

Microsoft hides user names in usage reports by default, and the setting applies to the Graph reports API too, not only the admin centre. In a tenant nobody has touched, the admin centre export and the PowerShell CSV both come back with hashes where the names should be. The connector reads the same report, so it can't see through the setting either. The difference is that it tells you (summary.concealed) instead of handing you a list of hashes. A Global Admin clears Conceal user, group, and site names in all reports under Org settings › Reports, and all three routes get names.

The connector has its own blind spot, and it's in the domain's note: activity only counts in Exchange, OneDrive, SharePoint and Teams. Someone who lives in Power BI or only uses an Intune-managed device looks idle. And the data is a daily snapshot, because Microsoft publishes the report once a day. It's never live.

Side by side

Admin centre Graph PowerShell Claude + GraphPaaS
To the list Navigate, export, spreadsheet formulas Sign in, one cmdlet, a filter script One sentence
Who can ask Global, Exchange, SharePoint or Teams Admin, Reports Reader Reports.Read.All with admin consent, plus a reader or admin role Any GraphPaaS member with access to that tenant
Costs Included Free module, your time A GraphPaaS plan and Claude
Free products Can't tell: yes/no per service Can't tell: product names only Left out
Disabled accounts Second export, manual join Second query, manual join Joined
Concealed names Hashes Hashes No names, and it says why
Which one to use
Once, for one tenant: the admin centre export is fine, as long as you check the names aren't hashed and you look up disabled accounts separately. Every month: a script beats clicking, and we covered what that script turns into after a year. Across clients, or when the person asking isn't the one who writes PowerShell: ask the question. The licence review in one prompt takes the same ten names to the cheapest plan that still fits.
Sources
GraphPaaS

Ask your own tenant who hasn't used their licence in 90 days.

Add the connector →

Newsletter

Get the next article by email

Practical M365 & Azure cost pieces like this one. No spam, unsubscribe anytime.