Compliance Drift Reporter
A worked example of an advanced workflow: a Claude-managed scheduled agent that, once a week, pulls two independent compliance signals — the last seven days of Liongard configuration-change detections, and the CIPP picture of which tenants have a compliance baseline and which tenants have healthy delegated access — and publishes a combined digest 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
The finished workflow runs unattended on a weekly cron. Each run it works through four phases: collect the week's Liongard detections, collect the CIPP tenant and baseline picture, compute a short posture scorecard, and deliver a two-section report to Slack. It turns a stream of scattered configuration changes and a partially rolled-out compliance program into a single weekly drift digest with a Slack trail.
Prerequisites
Before building, your Claude account needs all of the following:
| Requirement | Notes |
|---|---|
| WYRE MCP Gateway connector | Connected in claude.ai (https://mcp.wyre.ai/v1/mcp). Provides the liongard_* and cipp_* tools. See the Gateway overview. |
| Liongard enabled in the gateway | The gateway's Liongard API credentials must be able to read detections via liongard_detections_list. |
| CIPP enabled in the gateway | The gateway's CIPP connection must be able to read tenants via cipp_list_tenants and assigned standards via cipp_list_standards. |
| 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
These are the things that cost real time the first time through. Account for them up front and the build genuinely takes minutes.
- Liongard's
detections_listcarries a very largeChangeDetectionfield per detection — the routine reads only the small metadata and windows to 7 days to keep runs light. Each detection'sChangeDetectionfield is 5KB–44KB; even five detections is a ~145KB response. The routine reads onlyID,SystemName,Name,Date, andSystemTypeper detection and never touchesChangeDetection, and it scopes to a 7-day window so the total stays bounded. - A week yields hundreds of Liongard detections — paginate small and
cap. A typical 7-day window returns several hundred detections. The
routine pages at
pageSize5 and caps at the 25 most recent, reportingData.Pagination.TotalRowsas the true total and noting when it is showing a capped subset. - CIPP's Best Practice Analyzer is deprecated — never call
cipp_list_bpa. The endpoint returns HTTP 503: "The Best Practice Analyser has been deprecated and will be removed in a future release." The CIPP posture signal in this routine comes from assigned Standards and from per-tenant delegated-access health, not from BPA. -
cipp_list_standardsis empty until baselines are assigned in CIPP — that empty state is reported truthfully, never fabricated over. While a compliance-baseline rollout is in progress,cipp_list_standardslegitimately returns an empty array. The routine reports this as "N tenants unmanaged" and is explicitly instructed never to invent per-tenant compliance findings that are not in the data. As baselines get assigned in CIPP, the report fills in on its own. -
cipp_list_tenantsreturns one flat array of ~34 tenants — read only the small fields and never echoLastGraphError. Each tenant object carries aLastGraphErrorblob that can be long. The routine reads onlycustomerId,displayName,defaultDomainName,GraphErrorCount, andExcluded, and reports broken delegated access as just a tenant name plus the fact thatGraphErrorCountis non-zero. -
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. - 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
With the connectors above in place, paste this to Claude. It confirms the gateway, updates the existing routine, and verifies it end to end.
Build me the expanded Compliance Drift Reporter — a scheduled agent that combines Liongard configuration-change detections with CIPP baseline and posture data. Do all of this end to end:
1. Confirm the WYRE MCP Gateway works:
- Liongard: call liongard_detections_list with a recent 7-day startDate/endDate and pageSize 5; confirm a Data.Detections array plus a Data.Pagination envelope.
- CIPP tenants: call cipp_list_tenants; confirm a flat array of tenant objects, each with customerId, displayName, defaultDomainName, GraphErrorCount and Excluded.
- CIPP standards: call cipp_list_standards with tenantFilter "allTenants"; note whether it returns assigned baselines or an empty array. An empty array is expected until baselines are assigned in CIPP - it is not an error.
Do NOT call cipp_list_bpa: the Best Practice Analyser is deprecated and returns HTTP 503.
2. Note the shapes. Each Liongard detection carries a very large ChangeDetection field (5KB-44KB); the routine reads only ID, SystemName, Name, Date, SystemType and never ChangeDetection. cipp_list_tenants returns roughly 34 tenants; the routine reads only customerId, displayName, defaultDomainName, GraphErrorCount and Excluded, and never echoes LastGraphError.
3. Confirm a Slack connector is connected and note the destination channel name and ID (e.g. #sw-dev, C0931CKJ75X). 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. Update the existing scheduled routine named "Compliance Drift Reporter" (trigger trig_01KdNPXeYMep1SFEDEzCrPQV):
- Keep the schedule: weekly, cron "0 12 * * 1" (Monday 08:00 America/New_York = 12:00 UTC).
- On the WYRE MCP Gateway connector set permitted_tools to: liongard_detections_list, cipp_list_tenants, cipp_list_standards.
- On the Slack connector set permitted_tools to: slack_create_canvas, slack_send_message.
- An empty permitted_tools list = the routine runs with no tools.
- Install the exact routine prompt below.
5. Trigger a manual run and verify: a canvas titled "Compliance Drift - <date>" was created with a posture scorecard and two sections (Liongard, CIPP), a one-line summary landed in the destination channel, the Liongard count matches Data.Pagination.TotalRows, and the CIPP counts (tenants with a baseline + unmanaged) sum to the non-excluded tenant total. The resulting routine prompt
This is the lean prompt the build process installs into the scheduled routine itself. Substitute your own destination channel ID.
You are the Compliance Drift Reporter, a weekly routine for WYRE. You report two compliance signals in one digest: Liongard configuration-change detections, and CIPP baseline and posture state. Treat the two sources independently - a failure in one must not block the other.
PHASE 1 - Liongard detections.
Compute a 7-day window: endDate = now (ISO-8601), startDate = 7 days before now. Call liongard_detections_list with that startDate and endDate, pageSize 5, page 1.
- The envelope is Data.Detections (array) and Data.Pagination ({ TotalRows, HasMoreRows, CurrentPage, TotalPages, PageSize }).
- For EACH detection read ONLY: ID, SystemName, Name, Date, SystemType. NEVER read or echo ChangeDetection - it is 5KB-44KB each.
- Paginate by incrementing page until you have collected 25 detections OR HasMoreRows is false. CAP at 25 (5 pages).
- Record Data.Pagination.TotalRows as the true weekly total.
- If liongard_detections_list errors, mark the Liongard source UNAVAILABLE and continue to Phase 2.
PHASE 2 - CIPP baseline and access health.
Call cipp_list_tenants with no arguments. It returns a flat array of tenant objects.
- For each tenant read ONLY: customerId, displayName, defaultDomainName, GraphErrorCount, Excluded. Skip any tenant where Excluded is true.
- Access health: a tenant with GraphErrorCount > 0 has BROKEN delegated access. Record its displayName. Do NOT echo LastGraphError.
Call cipp_list_standards with tenantFilter "allTenants". It returns the assigned compliance baselines and their state, or an empty array.
- A non-excluded tenant absent from the standards result has NO baseline assigned - classify it UNMANAGED.
- A tenant present with a passing state is PASS; with a failing state is FAIL.
- If the standards result is an empty array, EVERY non-excluded tenant is UNMANAGED. This is expected while baselines are still being rolled out - it is NOT an error, and you must NEVER invent per-tenant compliance findings that are not in the data.
- If cipp_list_tenants errors, mark the CIPP source UNAVAILABLE.
Never call cipp_list_bpa (deprecated, HTTP 503) or cipp_run_standards_check (a write operation).
PHASE 3 - Total failure check.
If BOTH the Liongard source and the CIPP source are UNAVAILABLE, call slack_send_message to channel C0931CKJ75X with: 'Compliance Drift Reporter needs a human: neither Liongard nor CIPP could be read this week.' Then stop.
PHASE 4 - Zero findings check.
If every available source succeeded AND there are zero Liongard detections AND zero baseline FAIL tenants AND zero broken-access tenants, call slack_send_message to channel C0931CKJ75X with the single line: 'Compliance drift: no configuration changes, no baseline failures, no access issues this week.' Then stop. Tenants being UNMANAGED is a reportable finding, not zero findings - if any tenant is unmanaged, continue to Phase 5.
PHASE 5 - Build the report.
Compose a markdown report:
- SCORECARD at the top: 'Configuration changes this week: <Liongard TotalRows>. Tenants with a baseline assigned: <X> of <N>. Tenants failing baseline: <F>. Tenants with broken delegated access: <B>.' If a source was UNAVAILABLE, say so on its scorecard line instead of a number.
- SECTION 'Configuration changes (Liongard)': group the collected detections by SystemName, sort each group by Date descending, one line per detection (Name and human-readable Date). If TotalRows exceeds the number collected, note 'Showing the 25 most recent of <TotalRows>.' If the Liongard source was UNAVAILABLE, the section body is the single line 'Liongard data unavailable this run.'
- SECTION 'Baseline compliance (CIPP)':
* Baseline coverage: if any tenant has a standard assigned, list PASS and FAIL tenants by displayName, then list UNMANAGED tenants. If no standards are assigned at all, write the single line 'No compliance baselines assigned in CIPP yet - <N> tenants unmanaged.'
* Tenant access health: list the displayName of every tenant with broken delegated access, or 'All tenants have healthy delegated access.' if none.
If the CIPP source was UNAVAILABLE, the section body is the single line 'CIPP data unavailable this run.'
PHASE 6 - Deliver.
Call slack_create_canvas titled 'Compliance Drift - <today's date YYYY-MM-DD>' with the scorecard and both sections as markdown. Then call slack_send_message to channel C0931CKJ75X with a one-line summary linking the canvas, e.g. 'Compliance drift report ready: <TotalRows> config changes, <F> baseline failures, <B> tenants with access issues. See canvas: <link>.'
Use only these tools: liongard_detections_list, cipp_list_tenants, cipp_list_standards, slack_create_canvas, slack_send_message. Keep every step light - never read Liongard's ChangeDetection field or CIPP's LastGraphError blobs. How it works
Two sources, reported independently
The routine reads two unrelated systems — Liongard and CIPP — and they fail independently. So it treats them as separate sources: if one is unreachable the routine still delivers the other half and notes the gap in the report itself. Only when both sources fail does it escalate to a human and stop. A vendor outage never silently blanks the whole digest, and a partial report never hides that it is partial.
A weekly cadence, by design
Compliance drift is a slow-moving signal — configurations change and baselines roll out across days and weeks. The routine runs every Monday morning so the digest is waiting before the week's review work starts. A weekly cron is also kind to the APIs: one windowed sweep per week, not one per hour.
CIPP baseline drift and the empty-state rule
A CIPP Standard is a compliance baseline assigned to a tenant; a tenant that fails
its assigned Standard has drifted from baseline. The routine reads the assigned
Standards via cipp_list_standards and classifies every tenant as
pass, fail, or unmanaged (no baseline assigned). While a
baseline rollout is still in progress that endpoint returns an empty array — so the
routine is built to report the empty state honestly as a count of unmanaged
tenants, and is explicitly told never to fabricate compliance findings over empty
data. The report doubles as a live rollout tracker: "7 tenants unmanaged"
is a standing nudge to finish assigning baselines.
Tenant access health is a day-one signal
Each tenant in cipp_list_tenants carries a GraphErrorCount:
when it is non-zero, CIPP's delegated access into that customer's Microsoft tenant
is broken and CIPP can see nothing there. That is a real posture problem with data
available today, independent of the baseline rollout, so the routine surfaces the
list of tenants with broken delegated access every week alongside the baseline
picture.
Delivery is a Slack canvas plus a summary
The combined 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 linking it. When there are no detections, no baseline failures, and no access issues at all, it posts a single line and skips the canvas. See delivery adapters for other ways to surface a routine's output.
Extending it
The natural next step is escalation. For a system with repeated Liongard detections
week over week, or a tenant that fails its CIPP baseline run after run, the routine
could open a PSA ticket (via the gateway's autotask or
halopsa tools) so the recurring drift becomes tracked remediation work
rather than a line in a digest. A second extension targets the access-health
signal: a tenant whose delegated access stays broken for weeks is a candidate for an
automated GDAP-relationship repair workflow. The Best Practice Analyzer is
not an extension path — CIPP has deprecated it.
Questions or a workflow you'd like documented?
Open an issue
in the msp-claude-plugins repository.