Add on SKUs nobody opens
16 Sept 2026
Visio, Project and the add-on SKUs nobody opens
Somebody asked for Visio in 2022. Procurement bought a block of seats because a block was cheaper per seat than four, and the block has renewed every year since without anyone looking at it. The same story exists for Project, for the Power BI Pro seats that came with a pilot, and for whatever your tenant's equivalent is.
The question is trivially easy to ask — of these forty Visio seats, how many people opened Visio this quarter? — and surprisingly hard to answer. Not because the data is buried. Because for these two products, the report that measures "opened it" does not have a column for them.
The gap you are about to fall into
Microsoft 365 has two per-user reports that sound like they cover this, and they cover different halves of it.
The Microsoft 365 Apps Activations report is explicit that it breaks down "Microsoft 365 Apps for enterprise, Project, and Visio Pro for Microsoft 365 subscription activations". Your add-ons are in there, as their own product rows. What it records is that a user "activated their Office subscription on at least one device" — an install event, once, possibly years ago.
The Microsoft
365 Apps Usage report is the one that measures real behaviour: active users
are "any users who perform any intentional actions within these
apps". The apps are named on the same page — "Outlook, Word, Excel, PowerPoint,
OneNote, and Teams". Read the per-user column list in the Graph version, getM365AppUserDetail,
and you get thirty-odd columns of app-by-platform detail for exactly those six
products. There is no Visio column. There is no Project column. There is no
$select that conjures one.
Microsoft gives you activation for the add-ons and usage for the core apps. It never gives you usage for the add-ons. Every honest add-on audit is therefore built out of a weaker signal — and the trick is knowing exactly how weak, so you don't reclaim a seat you can't defend reclaiming.
The audit, in five steps
GET /subscribedSkus returns one subscribedSku per purchased product, with consumedUnits against prepaidUnits and the full servicePlans list. Filter to the add-on part numbers — this is the invoice, restated as an API response. Done when you have a row per add-on with two numbers on it: bought, and assigned.
getOffice365ActivationsUserDetail returns a CSV with a Product Type column next to User Principal Name, Last Activated Date, and per-platform counts for Windows, Mac, iOS, Android and Activated On Shared Computer. One row per user per product — which is what makes Visio separable at all. Done when you can subtract: assigned seats minus users with an activation row for that product.
A licensed user with no activation row for that product never installed it. That is not a judgement call about how much they use it — they don't have it. This bucket needs no second signal, no conversation and no 30-day watch period, and in most tenants it is the largest of the three. Done when those seats are unassigned and the difference is visible in consumedUnits on the next run.
The signIn resource carries appDisplayName and resourceDisplayName, both $filter-able, plus createdDateTime. Filtering on the Visio or Project web application gives you the closest thing to "opened it recently" that this tenant can produce. Two hard limits, both worth stating in the output: you need Entra ID P1 or P2 to read sign-in logs through Graph at all, and retention is 30 days on P1/P2 and seven on Free. Done when every still-assigned seat has a last-seen date or an explicit "no signal".
Deciding to drop a seat and being able to drop it are different dates. On an annual commitment the seat is yours until the term ends whether it was ever opened or not — see annual versus monthly commitment. Done when the list has a date attached to it and lands in someone's calendar before the renewal window, not during it.
What each number actually proves
The reason add-on audits get argued down in meetings is that someone presents one column and calls it usage. Label them honestly and the argument goes away.
| Signal | What it proves | What it costs |
|---|---|---|
consumedUnits |
A seat is assigned. Nothing about a human. | One call, no report pipeline. |
| No activation row | The strongest evidence available: never installed on any device. | Reports.Read.All; a 302 to a short-lived CSV URL, no period parameter. |
Last Activated Date |
When they installed it. Not when they last opened it. | Same call as above. |
| Sign-in in last 30 days | Somebody opened the web app. Silence proves nothing about the desktop app. | Entra ID P1/P2, and the window closes after 30 days. |
Where this goes wrong
Reading silence as absence. A desktop-only Visio user who signs in once and stays signed in generates no fresh sign-in events and no new activation. They are the most likely person to appear on a reclaim list and the most likely to escalate when the licence disappears. Step 3 exists precisely so the easy wins don't have to share a spreadsheet with this case.
Auditing the SKU instead of the service plan. Some Visio and
Project entitlements ride inside a bundle rather than arriving as a standalone
purchase, which means the seat count and the entitlement count are different
questions — the same distinction covered in the service
plans nobody uses. Check servicePlans on the SKU before
concluding a product isn't licensed anywhere.
Trusting the display name. The string your tenant reports is
a part number, not a marketing name, and several of these products differ by a
tier digit. The product
names and service plan identifiers table is the authority for what maps to
what; its String ID column is documented as the value behind
skuPartNumber. A filter written from memory reports beautifully on
the wrong SKU.
Running it once. An add-on audit expires. Seats get reassigned, projects end, the thirty-day sign-in window rolls forward and takes your evidence with it. A finding from March is not evidence in September.
Make it a standing column, not a project
The useful artefact is small: one row per add-on seat, with the assigned user, whether an activation row exists for that product, the activation date if it does, and a last-seen date from sign-in logs if there is one. Four columns, recomputed monthly, sorted so the never-activated seats float to the top.
Nobody builds it because it is three different Graph surfaces — a
/subscribedSkus call, a CSV behind a redirect with no period
parameter, and a paged log query with a retention clock on it — joined on
userPrincipalName and worth redoing every month. That is a
scheduled job, not an afternoon, and the afternoon version is what everyone
tries first.
GraphPaaS collects the licence counts and the per-product activation counts per tenant on a schedule, so "how many of these seats has anyone ever installed" is a number on a page rather than a CSV you have to go and fetch before the download URL expires.
- Microsoft 365 Apps Activations report — the explicit breakdown of "Microsoft 365 Apps for enterprise, Project, and Visio Pro for Microsoft 365" activations, and the definition of activation as installing on at least one device.
- Microsoft 365 Apps Usage report — "intentional actions" as the definition of active, the six apps it covers, and the note that shared computer activations are excluded.
- getM365AppUserDetail — the full per-user column list: Outlook, Word, Excel, PowerPoint, OneNote and Teams across four platforms, and nothing else.
- getOffice365ActivationsUserDetail — the
Product TypeandLast Activated Datecolumns,Reports.Read.All, and the 302 redirect to a short-lived download URL. - signIn resource type — the filterable
appDisplayNameandresourceDisplayName, and the statement that reading sign-in logs via Graph requires Entra ID P1 or P2. - Microsoft Entra data retention — sign-ins retained seven days on Free and 30 days on P1 and P2, and the fact that upgrading does not recover expired data.
Add-on seats bought, assigned and ever activated — side by side, per tenant, every month.
See your add-on seats →