M365 License Auditor
A worked example of an advanced workflow: a Claude-managed scheduled agent that, once a month, reads the Microsoft 365 license data for a fixed set of tenants through CIPP, flags every SKU with more seats purchased than assigned, estimates the wasted spend, and publishes a tenant-by-tenant report to Slack as a canvas with a one-line summary. There are no servers and no code to deploy: the agent is a saved prompt plus a schedule plus two MCP connectors, running in Claude’s cloud.
What it builds
Section titled “What it builds”The finished workflow runs unattended on a monthly cron. Each run it reads the license SKUs for each configured tenant, reasons about which ones have unassigned seats, estimates the monthly waste at a stated blended per-seat cost, groups the findings by tenant with a portfolio total, and notifies your team, turning scattered license sprawl into a single waste report with a Slack trail.
Prerequisites
Section titled “Prerequisites”Before building, your Claude account needs all of the following:
| Requirement | Notes |
|---|---|
| Conduit connector | Connected in claude.ai (https://conduit.wyre.ai/v1/mcp). Provides the cipp__* tools. See Connecting an AI Client. |
| CIPP connected in Conduit | The CIPP vendor connection must be configured and able to read tenants and license data across your managed Microsoft 365 estate. |
| Slack connector | The first-party Slack connector connected in claude.ai (https://mcp.slack.com/mcp). Provides slack_create_canvas and slack_send_message. |
| Scheduled routines access | Created via the /schedule capability; managed at claude.ai/code/routines. |
| A destination Slack channel | e.g. #sw-dev, plus its channel ID. The Slack connector must have access to it. |
Known gotchas
Section titled “Known gotchas”These are the things that cost real time the first time through. Account for them up front and the build genuinely takes minutes.
- CIPP is the license source — the Microsoft 365 connector cannot
enumerate licenses. The Microsoft 365 connector available to claude.ai
is search/read only — it surfaces mail, files, and calendar, but it cannot
list tenant license SKUs or seat counts. License auditing has to go
through CIPP’s
cipp__cipp_list_licensesin Conduit, which is why the routine attaches the Conduit connector rather than the Microsoft 365 one. - Scope the routine to a fixed set of tenants. An MSP’s CIPP commonly
manages dozens of Microsoft 365 tenants, and each
cipp__cipp_list_licensesrecord carries largeAssignedUsersandServicePlansarrays. Sweeping every tenant in one routine run is heavy and risks the run drowning in payload before it can finish — the same failure mode the Patch Drift Reporter hit. So this routine is scoped to a fixed set of about six tenants whose combined license data a run can comfortably hold, with their tenant filters baked into the prompt. With a modest tenant count (under ~15) a singlecipp__cipp_list_tenantscall at the top of the routine is acceptable instead. - Read seat counts from the list payload — ignore the heavy arrays.
Each
cipp__cipp_list_licensesrecord already exposesCountUsed(assigned),TotalLicenses(purchased), andCountAvailable(unassigned). TheAssignedUsersandServicePlansarrays on each record are not needed for the audit — the routine is told to ignore them. - The waste figure is an estimate with a stated assumption. The routine does not look up exact per-SKU pricing — it applies one blended per-seat cost (USD 20/month) and states that assumption in the report. Treat the dollar figure as a directional signal, not an invoice.
permitted_toolsmust be populated per connector. A routine with a connector attached but an emptypermitted_toolslist runs with no tools and silently does nothing — no error, no output. List the exact tool names the routine needs.- Faster-than-hourly cadences are rejected. The routine scheduler will not accept a cron more frequent than hourly. A monthly cron is well within bounds — and the right cadence for license churn anyway.
- A routine reaches only its attached connectors. The routine sandbox blocks arbitrary network egress, so notifications must go through the Slack connector’s tools, not an outbound webhook. Attach every connector the workflow touches.
The one-shot build prompt
Section titled “The one-shot build prompt”With the connectors above in place, paste this to Claude. It confirms the Conduit connection, creates the routine, and verifies it end to end.
Build me a scheduled M365 License Auditor agent. Do all of this end to end:
1. Confirm the Conduit connector works and CIPP is reachable: call cipp__cipp_list_tenants and check it returns your managed Microsoft 365 tenants.2. Pick the tenants this audit should cover. cipp__cipp_list_licenses returns one record per license SKU, and each record carries large AssignedUsers and ServicePlans arrays - sweeping every tenant in one routine run is heavy. If you manage more than ~15 tenants, choose a fixed set of about six and record each tenant's tenantFilter (domain or ID). These get baked into the routine prompt. With a modest tenant count you may keep it portfolio-wide.3. Confirm a Slack connector is connected. Note the destination channel name and ID (e.g. #sw-dev). If Slack does not show in the /schedule connector list, read its connector_uuid and url from an existing routine that already uses Slack (RemoteTrigger list -> get -> mcp_connections).4. Create a Claude-managed scheduled routine named "M365 License Auditor": - Schedule: monthly, cron "0 13 1 * *" (1st of the month, 09:00 America/New_York = 13:00 UTC). Faster-than-hourly cadences are rejected. - Attach TWO connectors, each with permitted_tools populated: * Conduit: cipp__cipp_list_licenses * Slack: slack_create_canvas, slack_send_message An empty permitted_tools list = the routine runs with no tools. - Routine prompt: every run, read license data for each baked-in tenant, flag SKUs where purchased seats exceed assigned seats, estimate the monthly waste with a stated assumed per-seat cost, build a report grouped by tenant with a portfolio total, publish it as a Slack canvas, and post a one-line summary linking the canvas. Use the exact routine prompt below, with your own tenant filters.5. Trigger a manual run and verify: a canvas titled "M365 License Audit - <month year>" was created, a one-line summary landed in the destination channel, and the report content matches the live tenants (spot-check one).The resulting routine prompt
Section titled “The resulting routine prompt”This is the lean prompt the build process installs into the scheduled routine itself. Substitute your own tenant filters and destination channel.
You are the M365 License Auditor. You run monthly. Keep it lean.
Use ONLY: cipp__cipp_list_licenses, slack_create_canvas, slack_send_message.Do NOT call cipp__cipp_list_tenants - the tenants are listed below.
Tenants to audit (tenantFilter - name):- <tenant-id-1> - <tenant name 1>- <tenant-id-2> - <tenant name 2>- ... about six tenants total ...
1. For each tenant above, call cipp__cipp_list_licenses with that tenantFilter. Each record is one license SKU and carries: License/skuPartNumber (SKU name), CountUsed (assigned seats), TotalLicenses (purchased seats), and CountAvailable/availableUnits (unassigned seats). Ignore the large AssignedUsers and ServicePlans arrays on each record - they are not needed.2. For each tenant, flag every SKU where TotalLicenses exceeds CountUsed. The unassigned count for that SKU is TotalLicenses minus CountUsed (= wasted seats). Skip SKUs that are fully assigned.3. Estimate monthly waste. Assume a typical per-seat cost of USD 20/month as a blended rate across SKUs - STATE this assumption explicitly in the report; do not look up or invent exact per-SKU pricing. Per-tenant waste = total unassigned seats x USD 20. Portfolio waste = sum across tenants.4. Build a report grouped by tenant: tenant name, and for each flagged SKU the SKU name, assigned seats, purchased seats, and unassigned count; then the tenant's total unassigned seats and estimated monthly waste. End with a portfolio total: total unassigned seats and total estimated monthly waste. Include the line 'Estimate assumes a blended USD 20/seat/month; actual per-SKU pricing varies.'5. slack_create_canvas titled "M365 License Audit - <current month and year>" with the full grouped report. Then slack_send_message to #sw-dev (channel ID C0931CKJ75X): a one-line summary ('M365 license audit: ~$N/mo estimated waste across 6 tenants - see canvas') linking the canvas.6. If a tenant cannot be read, list it in a "could not check" section instead of skipping it silently.How it works
Section titled “How it works”A monthly cadence, by design
Section titled “A monthly cadence, by design”License churn is slow — seats are added and removed over weeks as people join and leave, not minute to minute. A monthly run on the 1st catches the previous month’s drift in time for the billing cycle, and a once-a-month sweep is kind to CIPP: one pass per month, not one per hour.
Waste is unassigned seats times a stated per-seat cost
Section titled “Waste is unassigned seats times a stated per-seat cost”For each SKU the routine compares TotalLicenses (purchased) against
CountUsed (assigned); the difference is unassigned seats, licenses paid
for but not in use. It multiplies the unassigned total by one blended
per-seat cost (USD 20/month) to estimate monthly waste, and the report
states that assumption plainly: “Estimate assumes a blended USD
20/seat/month; actual per-SKU pricing varies.” The routine deliberately does
not invent exact per-SKU prices; the goal is a directional waste signal that
is consistent month to month, not a precise invoice.
It covers a fixed set of tenants, not the whole portfolio
Section titled “It covers a fixed set of tenants, not the whole portfolio”Each cipp__cipp_list_licenses record carries large AssignedUsers and
ServicePlans arrays. Across 33 managed tenants that payload swamps a
single routine run before it can finish. So the routine is scoped to a fixed
set of about six tenants whose combined license data a run can comfortably
hold, with their tenant filters baked into the prompt. A smaller portfolio
could instead let the routine call cipp__cipp_list_tenants once and loop
everything.
Nothing is dropped silently
Section titled “Nothing is dropped silently”If a tenant’s license data cannot be read — a CIPP refresh error, a lapsed delegated admin relationship — the routine lists it in a “could not check” section rather than omitting it. A tenant missing from the report always means a real gap.
Delivery is a Slack canvas plus a summary
Section titled “Delivery is a Slack canvas plus a summary”The full tenant-by-tenant report can be long, so the routine publishes it as a Slack canvas, a durable document teammates can scroll and reference, and posts only a one-line summary to the channel with the portfolio total estimated waste, linking the canvas. The channel stays readable; the detail lives in the canvas. See Delivery Adapters for other ways to surface a routine’s output.
Extending it
Section titled “Extending it”The natural next step is to cross-reference assigned seats against account
state: a license assigned to a disabled user is waste that the
purchased-vs-assigned comparison alone misses. Adding
cipp__cipp_list_users to the routine lets it flag licenses held by
disabled accounts as recoverable on top of the unassigned-seat count. The
waste estimate can also be tightened: swap the single blended rate for a
small per-SKU price map baked into the prompt. The report-building and
Slack-delivery body of the routine is identical.
Questions or a workflow you’d like documented? See Getting support.

