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.
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.
Claude: one sentence, two receipts
Who in Fabrikam HQ hasn't used their licence in 90 days?
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
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.
Is anyone licensed whose account is switched off?
licence_advisor(tenant_id="ce48e256-019f-4f8c-b951-0c52d1d65b8e",
state="remove", window_days=90)
→ 2 rows · reason "account disabled"
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 |
- Microsoft 365 apps Active users report — where the report lives, the Export button, per-service dates and yes/no licence columns.
- Microsoft 365 admin center usage reports overview — roles that can view reports, names concealed by default (Graph included), last activity date ignores the period.
- reportRoot: getOffice365ActiveUserDetail — Reports.Read.All, the 302 to a CSV, the column list.
- Authorization for APIs to read Microsoft 365 usage reports — delegated calls also need an admin or reader role.
- Get-MgReportOffice365ActiveUserDetail — -Period or -Date, and -OutFile is mandatory.
- Manage self-service sign-up subscriptions — free subscriptions users sign up for themselves.