# Worked example: Acme Cloud (fictional)

Acme Cloud is a fictional 40-person B2B SaaS company on example.com. Its former agency uses example.net. A new marketing ops lead inherits the stack, four renewals fall inside 90 days, and leadership wants HubSpot and Snowflake connected to an AI assistant. The audit runs as of 2026-11-01. All names, IDs and amounts are invented.

## Files

- [../assets/example-acme-tools.csv](../assets/example-acme-tools.csv): the register, 20 rows (T01 to T20).
- [../assets/example-acme-spend.csv](../assets/example-acme-spend.csv): 185 AP, card and expense lines; 2 fall before the 12-month window.
- [../assets/example-acme-okta-app-usage.csv](../assets/example-acme-okta-app-usage.csv): Okta Application Usage, Application set to All.
- [../assets/example-acme-google-accessed-apps.csv](../assets/example-acme-google-accessed-apps.csv): the Google Workspace accessed apps download, with scopes.
- [../assets/example-acme-seats.csv](../assets/example-acme-seats.csv): user exports from seven tools, normalised, with MFA and identity provider status joined from the identity provider's user list.
- [../assets/example-acme-connections.csv](../assets/example-acme-connections.csv): 14 directional connections, one proposed.
- [../assets/example-acme-billing-accounts.csv](../assets/example-acme-billing-accounts.csv) and [../assets/example-acme-crm-companies.csv](../assets/example-acme-crm-companies.csv): the key lists for the match test.
- [../assets/example-acme-report.md](../assets/example-acme-report.md): the filled report and plan built from these outputs.

## Commands

The example files are the regression fixture, so these run from the skill folder. In a real audit, run from the audit folder with the script's path.

```bash
python3 scripts/stack_inventory.py --tools assets/example-acme-tools.csv \
  --spend assets/example-acme-spend.csv --sso assets/example-acme-okta-app-usage.csv \
  --oauth assets/example-acme-google-accessed-apps.csv --seats assets/example-acme-seats.csv \
  --company-domains example.com --vocabulary assets/capability-vocabulary.csv --as-of 2026-11-01

python3 scripts/connection_map.py --tools assets/example-acme-tools.csv \
  --connections assets/example-acme-connections.csv \
  --vocabulary assets/capability-vocabulary.csv --as-of 2026-11-01

python3 scripts/key_match.py --expected assets/example-acme-billing-accounts.csv --expected-key account_id \
  --delivered assets/example-acme-crm-companies.csv --delivered-key account_id
```

Final lines, each with exit code 1 because at least one FAIL exists:

```text
16 FAIL, 24 WARN, 5 PASS, 10 NOT CHECKED. Nothing was changed.
4 FAIL, 11 WARN, 5 PASS, 0 NOT CHECKED. Nothing was changed.
1 FAIL, 2 WARN, 3 PASS, 0 NOT CHECKED. Nothing was changed.
```

## Output excerpts

Act now, printed before everything else, so the agent tells the user the same day:

```text
T17 Unbounce: review-by 2026-10-26 passed, notice boundary 2026-11-11 (CON04)
T01 HubSpot: review-by 2026-11-01 (0 days), notice boundary 2027-01-15
T04 Segment: review-by 2026-11-06 (5 days), notice boundary 2026-11-20
OAuth app "Example Notes AI": restricted Google scopes and no register row; IT security today (INV02)
```

Spend and scope:

```text
T04 Segment: spend, contract; 14,400.00 USD; descriptors: TWILIO SEGMENT
USD tool spend 159,416.00 (register tools 158,124.00, unregistered 1,292.00); reported apart: media 60,000.00; services 48,000.00
USD: 7 tools cover 134,400.00 of 161,816.00 (83.1%): T01 HubSpot 38,400.00; T10 Snowflake 27,600.00; T06 Amplitude 18,000.00; T04 Segment 14,400.00; T08 Intercom 13,200.00; T07 Customer.io 12,000.00; T11 Fivetran 10,800.00
```

Renewal calendar (shortened):

```text
2026-10-26 overdue T17 Unbounce: term ends 2026-11-25, notice 14 days (written_notice), boundary 2026-11-11 ... (lead 30, no disposition)
2026-11-01 0-30    T01 HubSpot: term ends 2027-01-15, notice 0 days (account_toggle), boundary 2027-01-15, decision target 2027-01-01 ... (lead 75, right-size, top spend and discount ending: a tested alternative takes 75 days)
2026-11-06 0-30    T04 Segment: term ends 2026-12-20, notice 30 days ... boundary 2026-11-20 ... (lead 30, keep)
2026-11-17 0-30    T13 Metabase: term ends 2026-12-31 ... boundary 2026-12-01 ... (lead 30, right-size)
2026-12-15 31-60   T06 Amplitude: term ends 2027-02-28, notice 45 days ... boundary 2027-01-14 ... (lead 75, consolidate)
2026-12-18 31-60   T18 Cookiebot: term ends 2027-01-31 ... boundary 2027-01-01 ... EUR
2027-02-15 later   T11 Fivetran
2027-04-17 later   T10 Snowflake (auto-renew no)
Locked: T12 Hightouch: boundary 2026-10-16 passed, term ends 2026-11-15; recertify by 2027-07-18
Unknown terms: T07 Customer.io (missing notice_days); T08 Intercom (missing term_end, notice_days)
Monthly: T05 Mixpanel; T15 Calendly; T16 Zapier (all reduction any_time)
```

Findings (shortened):

```text
FAIL INV02 unregistered-app: OAuth app "Example Notes AI" (7 users, Unverified) has no register row and holds restricted Google scopes (drive.readonly, gmail.readonly), which reach mail or Drive data; it also holds calendar.readonly. ...
FAIL CON04 decision-overdue: T17 Unbounce review-by date 2026-10-26 has passed and the notice boundary is 2026-11-11 ...
FAIL USE01 dormant: T10 Snowflake: an enabled admin (ACCOUNTADMIN) has no successful sign-in for 45 days or more (last login 2026-08-22). ...
FAIL ACC04 admin-mfa: T01 HubSpot: 4 enabled admins can sign in without enforced MFA or SSO on a system of record. ...
FAIL ACC05 offboarded-account: T01 HubSpot: 1 enabled account whose identity provider user is deactivated. ... Evidence: seats row 13.
WARN ACC06 restricted-oauth-scope: T16 Zapier: the Google grant "Zapier" holds restricted Google scopes (drive, gmail.modify) for 2 users. ...
WARN USE02 reclaimable-seats: T01 HubSpot: 4 reclaimable paid seats = 12 paid - 8 active - 0 dormant but required - 0 unknown; cash from the term end 2027-01-15. ...
NOT CHECKED ACC04 admin-mfa: MFA status is unknown for 2 enabled admins on T02 Google Analytics 4, T03 Google Tag Manager. ...
FAIL FLW01 two-writers: C07 T10 Snowflake -> T01 HubSpot via T12 Hightouch: contact.lifecycle_stage in T01 HubSpot has 2 writers ...
FAIL FLW02 stale: C09 T07 Customer.io -> T01 HubSpot: observed delay 1,440 minutes is over the required 15 minutes and the flow feeds customer_action. ...
FAIL KEY02 excess-duplicates: 6 excess rows on 6 keys, 2.33% of keyed delivered rows (FAIL above 0.5%).
WARN KEY01 coverage: 96.40% of expected keys reach the delivered file (241 of 250; PASS at 98%, FAIL below 95%); 9 missing.
```

## What the agent does with each finding

Act now, before step 4:

- The agent tells the user the four "Act now" items on 2026-11-01: Unbounce needs a decision before its 2026-11-11 boundary, HubSpot's renewal work starts today, Segment is decided by 2026-11-06, and Example Notes AI goes to IT security today.

Inventory and ownership:

- INV02 FAIL, Example Notes AI: an unverified meeting-notes app holds restricted Gmail and Drive scopes for 7 people with no owner or DPA. It goes to IT security the same day, outside the plan. The audit drafts a revocation list; the super administrator decides.
- INV01 FAIL, Hotjar and Typeform: recurring charges on an expense claim and Card B with no register row. Typeform is also the unregistered X01 that feeds HubSpot through Zapier (FLW09), so it is registered as forms with an owner. Hotjar duplicates Microsoft Clarity's session replay job; the Web lead confirms whether anyone uses it before a retire disposition.
- INV02 WARN, Notion: a general workspace tool with no marketing data flow. The agent proposes a register row with status out_of_scope and the reason in `evidence_ref`; on the next run the warning disappears.
- INV04 FAIL, Hightouch has no technical owner: the Data lead names one before the renewal recertification.
- INV05 FAIL with ACC01 and ACC02, GA4 under the agency and a GTM account whose only admin is on example.net: two company admins are added first, then the agency transfers ownership. Days 0 to 30.
- INV06 WARN, Unbounce: no charge reached the company's payment sources in 12 months although an annual term renewed in November 2025. Finance checks whether another entity or a personal card pays.

Contracts:

- CON04 FAIL, Unbounce: the review-by date passed on 2026-10-26 and the boundary is 2026-11-11. With no SSO users, no seats and no charges, the Growth marketer checks which landing pages are live before deciding. If none, the contract owner sends written notice before 2026-11-11.
- HubSpot renewal, review-by 2026-11-01: a right-size on the largest contract, with the discount ending on the term end (CON05), gets the 75-day lead. The Head of Growth starts now: the renewal is decomposed into discount, unit price and quantity, and an alternative quote is priced while there is still leverage. The decision is due by 2027-01-01, 14 days before the account-toggle boundary.
- CON01 FAIL, Customer.io and Intercom: order forms requested in the first week. Until they arrive, both stay under "unknown terms" and Intercom's disposition stays investigate.
- CON03 WARN, Hightouch: the renewal locked on 2026-10-16, before the audit began. It gets a recertification date of 2027-07-18, not a cancellation.
- CON02 WARN, Snowflake: actual credits ran 15% over the contract value. The Data lead checks for overage charges before the 2027-04-17 review.

Usage and overlap:

- USE04 WARN, HubSpot contacts: 31,000 peak fits the 35,000 tier at 0% growth. The owner's written forecast is 15%; the re-run with `--growth 0.15` gives 35,650, which no longer fits, so USE04 clears and the tier stays. Seat reclaim (USE02, 4 seats) stays on the renewal decision.
- USE01 FAIL, the dormant Snowflake ACCOUNTADMIN: removing the role alone would leave one company admin and turn ACC01 into a WARN. The Data lead either grants the role to a second active owner before removing it, or documents the account as an emergency admin with a named holder who signs in on a schedule.
- USE03 WARN, Customer.io: last logins came from Entra P1 sign-ins (30 days), so two accounts are dormancy-unknown, not dormant. The Lifecycle lead exports the tool's own team list with last activity.
- OVL02 FAIL, email:trial_onboarding in both HubSpot and Customer.io: the agent compares one week of both send logs for the same trial accounts. Class 1 if both sent to the same people.
- OVL01 WARN, Mixpanel and Amplitude: Amplitude's consolidate disposition carries all five fields (destination T05, owner, test, commercial deadline 2027-01-14 (notice boundary); replacement test finished by 2026-12-15 (review-by), dependencies: C03 events rerouted as proposed C14).

Access and AI connections:

- ACC04 FAIL, HubSpot: the account does not require two-factor authentication, so its four Super Admins, including the shared marketing login (ACC03) and the agency admin, can sign in with a password alone. Dependency: the shared login is replaced with named users first, then the Head of Growth turns on the requirement. GA4 and GTM stay NOT CHECKED for the agency admin's MFA until the agency confirms it or the admin is removed.
- ACC05 FAIL, HubSpot and Metabase: a person whose identity provider user was deactivated in July still holds a paid HubSpot marketing seat and a Metabase viewer seat. The tool admins list what each account owns (lists, workflows, saved questions, dashboard subscriptions), move it to named owners, then disable the accounts. The HubSpot seat is one of the 4 reclaimable seats.
- ACC06 WARN, HubSpot and Zapier: HubSpot's Google grant holds gmail.readonly for 12 users, and Zapier's holds gmail.modify and full Drive for 2. The RevOps lead confirms who needs inbox logging, and which zaps touch mail and Drive; if they only touch files they create, the zap is rebuilt on drive.file.
- AI connection FAIL (from the AI connection inventory, not a script): the HubSpot remote MCP server is connected by a Super Admin in one assistant, with all tools allowed and no off switch recorded. That is a personal admin login with write access and no approval step. Class 2, days 0 to 30: the connection owner disconnects it, or reconnects it under a least-privilege user with only read tools allowed in the client.

Flows and identity:

- FLW02 FAIL, C09: unsubscribes reach HubSpot a day late against a 15-minute requirement. Class 1, days 0 to 30: interim suppression in both tools, then a fix. Acceptance: an unsubscribe reaches HubSpot within 15 minutes in three of three tests.
- Point of action, W2 trial onboarding: the Customer.io day-3 send step re-checks the suppression list when it runs, and history shows no sends to the four people who unsubscribed mid-delay: PASS. HubSpot's onboarding workflow has no goal or exit rule, so trials that activate during a delay still get the remaining activation emails: FAIL, folded into the OVL02 decision, because whichever tool keeps onboarding must exit on activation.
- FLW01 FAIL, C07: the warehouse writes lifecycle stage while HubSpot is recorded as its system of record. The decision on one authority goes to days 31 to 60: revops-workflow-design says who may set lifecycle stage, then martech-integration-engineering's field ownership register carries it into the sync.
- FLW05 FAIL, C08: the form-to-CRM zap runs on a departed person's credential. It is rebuilt under a company service identity before anyone removes that user.
- KEY02 FAIL and KEY01 WARN: 2.33% excess duplicate account IDs in the CRM and 96.4% coverage. Eleven unexpected keys are trials and churned accounts, so they are explained, not defects. The duplicates go to a review queue (crm-data-hygiene-audit); no new account segments until they pass.

## AI readiness as rated

- Tracking accuracy: partly. A plan exists; the last end-to-end test was in February 2026.
- Identity resolution: fix. No shared user ID across product analytics and the CRM; 2.33% duplicate account IDs.
- Warehouse models: partly. Snowflake and Fivetran run on schedules; the models have no duplicate tests.
- Metric definitions: fix. No written definition of revenue or of a customer; MQL differs between the CRM report and the BI dashboard.
- Context docs: partly.
- Access and permissions: fix. The HubSpot MCP connection on a Super Admin login, the unverified AI notes app, the shared CRM admin login, CRM admins without enforced MFA, the agency-only GTM admin.
- MCP readiness: partly. The HubSpot remote MCP server was connected on purpose, by a named owner; it is not read-only and has no central off switch.
- Blocks: no AI answers on CAC or pipeline by channel until identity and metric definitions pass; no write-capable connection until access passes. The first step is a read-only CRM connection under a least-privilege user, tested on five questions with known answers.

## Plan and savings excerpts

The full versions are in [../assets/example-acme-report.md](../assets/example-acme-report.md).

- Days 0 to 30: contain the AI notes app (IT security); interim suppression for C09, then the fix; compare onboarding send logs and keep one sender; decide Unbounce before 2026-11-11; start the HubSpot renewal work now; decide Segment by 2026-11-06 and Metabase seats by 2026-11-17; company admins for GTM and GA4, then the agency transfer; the access clean-up (Snowflake admin, shared HubSpot login, then required two-factor authentication, the leaver's two accounts); disconnect or narrow the HubSpot MCP connection; rebuild C08 under a service identity; request the Customer.io and Intercom order forms; quick wins after owner checks.
- Days 31 to 60: one authority for lifecycle stage (C07); the CRM duplicate review queue; the Amplitude replacement test, sends and writes off, finished by 2026-12-15; owners confirm the restricted Google scopes; consent rules for C10 and C11.
- Days 61 to 90: Amplitude notice before 2027-01-14 if the test passed; the HubSpot decision by 2027-01-01; Calendly into HubSpot meetings; the intake rule; re-tests of closed FAILs; a read-only AI connection once access passes.
- Savings: cash through 2027-01-31 of USD 538 to 588 after owner checks; run-rate of USD 22,152 to 22,752 a year once commitments end, most of it Amplitude from 2027-03-01 less any added Mixpanel cost; no released capacity assumed; Amplitude saves no cash before its prepaid term ends on 2027-02-28.

## Regression

The example files double as the regression fixture. After changing a script, re-run the three commands and compare the final lines above and these IDs. A case that names a change runs on a scratch copy of the example files, never on the files themselves.

- stack_inventory.py FAIL: INV01 Hotjar; INV01 Typeform; INV02 Example Notes AI; INV04 T12; INV05 T02; CON01 T07; CON01 T08; CON04 T17; USE01 T10; OVL02 email:trial_onboarding; ACC01 T03; ACC02 T03; ACC03 T01; ACC04 T01; ACC05 T01; ACC05 T13.
- stack_inventory.py WARN: INV02 Notion; INV02 Typeform; INV03 T15; INV06 T17; INV07 T08; CON02 T10; CON03 T12; CON05 T01; USE01 T01, T06, T13; USE02 T01, T06, T13; USE03 T07; USE04 T01; USE05 T01; OVL01 product_analytics; OVL01 scheduling; ACC01 T02; ACC02 T01; ACC02 T02; ACC06 T01; ACC06 T16.
- stack_inventory.py PASS: CON06; OVL03; ACC01 T01, T07, T10. NOT CHECKED: USE01 T02, T03; ACC01 T04, T09, T11, T12, T16, T18, T19; ACC04 (T02 and T03 in one line).
- stack_inventory.py "Act now": CON04 T17; review-by T01 2026-11-01 and T04 2026-11-06; INV02 Example Notes AI.
- stack_inventory.py with `--growth 0.15`: 16 FAIL, 23 WARN, 6 PASS, 10 NOT CHECKED (USE04 moves to PASS).
- connection_map.py FAIL: FLW01 C07; FLW02 C09; FLW05 C08; FLW09 X01. WARN: FLW03 C08, C10; FLW04 C05, C10; FLW05 C11; FLW06 C03; FLW07 C07; FLW08 C10, C11; FLW10 C13; FLW11 T18. PASS: C01, C02, C04, C06, C12.
- connection_map.py, copies are one writer: add a row that copies contact.lifecycle_stage from T04 to T07, or from T01 to T10. Neither may raise FLW01; C07 still fails it.
- connection_map.py, authority: `field_authority` source_wins on C07 still FAILs FLW01, because HubSpot is recorded as that field's system of record. destination_wins or a named rule passes it: 3 FAIL, 11 WARN, 6 PASS, 0 NOT CHECKED.
- connection_map.py, unknown stays unknown: `field_authority` unknown on C07 still FAILs FLW01. An added T08 to T01 row with objects contacts, match_key email, status observed and every judgement column unknown prints the same findings as that row left blank: FLW03 to FLW08 WARN, and no connection PASS. An added T10 to T13 row with every column unknown and status observed raises FLW04 to FLW07 WARN and no connection PASS; FLW08 does not apply to a reporting destination. The same row into T01 adds FLW08 NOT CHECKED, because its objects are unknown.
- connection_map.py, FLW08 wording, added rows into T07 with no rule: "user profiles", "customer profiles", "contact records", "lead", "hashed emails", "subscriber", "members" and "user_profile" raise FLW08 WARN. "form submissions" prints FLW08 NOT CHECKED and no connection PASS. "reporting tables" raises no FLW08.
- connection_map.py, freshness and the map: `last_success` 2026-10-01 on C05 raises FLW02 FAIL, because the flow feeds billing. C03 is observed with no evidence_ref, so it is drawn dotted with "no dated evidence".
- connection_map.py, scope and dates: a critical tool with `status` unknown stays critical and in its layer, and a Note names it. `last_success` 10/01/2026 exits 2 naming the connections row and column.
- stack_inventory.py, scope: `status` unknown on T17 keeps 16 FAIL, 24 WARN, 5 PASS, 10 NOT CHECKED, CON04 T17 and its "Act now" line, and a Note names T17. `billing_period` unknown on T08 keeps those totals and CON01 T08.
- stack_inventory.py, dates: a seats row with last_login 10/21/2026 and no `--date-format` exits 2 naming the seats row and last_login, and a spend date or generic SSO last_login the script cannot read does the same. Seats row 2 with last_login 10/30/2026 exits 2; with `--date-format %m/%d/%Y` HubSpot stays at 4 reclaimable seats.
- stack_inventory.py, unknown activity: `last_login` unknown on seats row 13 gives HubSpot 3 reclaimable seats and USE03 WARN. `source_retention_days` unknown on seats rows 46 and 50 gives Metabase 4 reclaimable seats and USE03 WARN. Neither row counts as dormant.
- stack_inventory.py, unknown lists: the word unknown in `capabilities`, `customer_actions` and `system_of_record_for` on three rows raises no OVL finding for it.
- stack_inventory.py, retired rows: `status` retired on T05 raises INV08 WARN (1 charge, the last on 2026-10-05; 3 SSO users, the last on 2026-10-31), lists `retired T05 Mixpanel` in channel coverage, and reports 9,000.00 apart from USD tool spend of 150,416.00. Totals stay 16, 24, 5, 10: INV08 comes in and OVL01 product_analytics goes out. With out_of_scope the spend split stays and INV08 does not fire. With no retired row, no INV08 line prints.
- key_match.py: FAIL KEY02; WARN KEY01, KEY03; PASS KEY04, KEY05, KEY06.
- Sparse inputs never PASS: a register with only `tool_id` and `tool_name` (two rows, no other file) gives `0 FAIL, 2 WARN, 0 PASS, 26 NOT CHECKED`, the two WARNs being INV04. A register with a header and no rows gives `0 FAIL, 0 WARN, 0 PASS, 27 NOT CHECKED`. Header-only spend, SSO, OAuth or seats files beside the Acme register print NOT CHECKED, never PASS, for INV02, INV06, CON02, USE01, USE03 and ACC06. key_match.py on two header-only files gives `0 FAIL, 0 WARN, 0 PASS, 5 NOT CHECKED`. A workbook passed as any CSV exits 2. A connections file with only `connection_id`, `source_id` and `destination_id` gives `0 FAIL, 0 WARN, 1 PASS, 11 NOT CHECKED`: FLW09 passes, and the other ten checks and the configured connection print NOT CHECKED.
