MSP monthly report contents
12 Aug 2026
What an MSP monthly report should actually contain
Here is a test worth running on your own deliverable. Open last month's report for your biggest client, and count how many of its pages would change anything if they were deleted. In most decks the honest answer is one or two — and they are usually not the first two. The rest is proof of work: charts that show the same shape they showed in October, generated because the export was already open.
This is the skeleton that survives that test. Five sections, in the order a client reads them, each with a done-condition you can check before you send it.
The rule
A monthly report is a diff, not a snapshot. If a number is the same as last month, it belongs in the appendix. If it moved and nobody can act on it, it belongs nowhere. Everything that survives both filters goes on page one.
Which numbers earn a page
Run every candidate metric through three questions before it gets a chart. Most will not survive, and that is the point — the report gets shorter as the service gets better, which is a message worth sending on its own.
The five sections, in this order
Money first. Not because it matters most, but because it is the only section the person who signs the invoice will definitely read, and the goodwill it buys is what gets the security section read too.
One table, one row per SKU, two columns. Graph
gives you both directly: subscribedSku
exposes prepaidUnits (what you are billed for) next to
consumedUnits, which is defined as the number of licences that
have been assigned — not used, assigned. The gap between the two is
money you are paying for nobody. Watch capabilityStatus too: a
SKU sitting in Warning is a subscription in its grace period,
which is a renewal conversation you would rather have this month than after
it lapses.
Done when: every non-zero gap has a name next to it
Named people, with a last-sign-in date and a
recommendation. This is the section that pays for the service, and it is
also the one with the sharpest trap: the
signInActivity
property requires an explicit $select, needs Entra ID P1 or
P2 plus AuditLog.Read.All, and — the part that ruins reports —
is simply not returned for a user who never signed in, or
who last signed in before April 2020. An empty field is not a zero. Sort
naively and your most dormant accounts fall off the bottom of the list.
Done when: blanks are counted as never, not skipped
MFA registration coverage, privileged account count, and whatever your baseline policy set is missing. Then the exceptions — by name, because "94% coverage" is a number nobody can act on and "these six admins" is a ticket. Keep the same three numbers every month even when they do not move; this is the one place where the flat line is the message, and where the appendix rule is worth breaking on purpose.
Done when: each gap names a person, not a percentage
A spend chart on its own is decoration. A spend chart against a budget is a conversation. Cost Management budgets are evaluated every 24 hours against cost data that is itself 8–24 hours behind, and they take up to five thresholds each. Say the honest part out loud in the report: a budget notifies, it does not stop consumption — "resources aren't affected, and your consumption isn't stopped". The client should know the guardrail is a smoke alarm, not a sprinkler.
Done when: every subscription has a budget and a named recipient
Incidents that touched this tenant, separated
into yours and Microsoft's. The
serviceAnnouncement
resource carries all three feeds you need: healthOverviews
for current service state, issues for the incidents themselves,
and messages for the Message Center posts announcing changes
heading your way. Two Microsoft incidents listed with dates and durations
will do more for a renewal than any chart in the deck.
Done when: the client stops asking "was that outage us?"
Every section has a ceiling — put it in the footnote
The credibility of a monthly report comes from admitting what it cannot see. Each of the five sections is drawing on a surface with a hard limit, and clients find out about those limits at the worst possible moment: when they ask for something last quarter.
| Section | Source | The ceiling to disclose |
|---|---|---|
| Seats | subscribedSku | Assigned, never used — the count says nothing about activity |
| Reclaim list | signInActivity | P1/P2 only; absent for never-signed-in and pre-April-2020 accounts |
| Adoption | M365 usage reports | Names concealed by default; only 7/30/90/180-day windows exist |
| Security | Entra sign-in and audit logs | 7 days on Free, 30 on P1/P2 — and upgrades are not retroactive |
| Spend | Cost Management | Costs land 8–24h late; budgets alert but never stop consumption |
Two of those deserve a sentence in the report itself. Usage reports conceal usernames, groups and site names by default, and that organisation setting follows you into Graph and Power BI — so the adoption section is either anonymous or the client has explicitly turned concealment off, and they should know which. And when a user account is deleted, their usage data goes within 30 days: the person whose licence you reclaimed in section 2 takes the evidence for it with them. Reclaim first, screenshot later, is the wrong order.
The section that makes it a report
Everything above is available to anyone with the right admin roles. What makes it a monthly report rather than a monthly export is the column that Microsoft will not give you: last month's value. Entra keeps sign-in and audit logs for seven days on Free and 30 days on P1 or P2, and explicitly warns that retention changes are not retroactive — buying P1 today does not resurrect last month. Usage data reaches 180 days and stops.
Which means a quarterly comparison is not something you query. It is something you kept, or it does not exist. Store each month's five sections as you produce them, and the diff column writes itself; skip a month because month-end was busy and that month is permanently blank, which is the real cost of assembling reports by hand — not the hours, the gaps they leave behind.
What to leave out
Storage-by-user charts with no quota pressure. Compliance percentages with no named exception. Anything with the word "engagement" in the title. Screenshots of admin center blades, which age badly and prove only that you logged in. And the biggest one: a licence chart that shows utilisation without showing the euro amount attached to the unused half — 15–25% waste is the normal finding, and a percentage on a slide is a fact while the same number in euros is a decision.
The finished thing is one page a client reads and four they can check. If it takes three hours to assemble every month it will quietly become two pages and then stop, so the sections above are worth wiring to a schedule rather than a person. That is what GraphPaaS does — the same five surfaces, per tenant, collected on a timer and retained past the window Microsoft keeps them in, so the diff column is always there when month-end arrives.
Sources
- subscribedSku resource type — that
consumedUnitscounts assigned licences, and whatcapabilityStatusvalues mean. - user resource type — signInActivity needs
$select, P1/P2 and AuditLog.Read.All, and is absent for never-signed-in users. - Microsoft 365 admin center usage reports overview — concealed names by default, the 7/30/90/180-day windows, and deletion of a user's usage data within 30 days.
- Microsoft Entra data retention — seven days on Free, 30 on P1/P2, and that retention changes are not retroactive.
- Tutorial: create and manage Azure budgets — 24-hour evaluation, 8–24h data lag, up to five thresholds, and that budgets never stop consumption.
- serviceAnnouncement resource type — the healthOverviews, issues and messages feeds behind the incident section.
GraphPaaS
Five sections, every tenant, already collected when month-end arrives.
See it on your tenant →