# Worked example: Acme Analytics (fictional)

Acme Analytics is a fictional B2B SaaS company. The fixture runs below use the files in `assets/` and run from the skill folder; the files they write go to a folder outside the skill (`$WORK`). Every output block is pasted exactly as the scripts printed it on 2026-11-02. In a real audit, work from the user's working folder instead ([reading-the-crm.md](reading-the-crm.md), Working folder).

## The setup

- Stack: HubSpot Sales Hub Professional and Marketing Hub Professional; Stripe billing synced by a billing integration; a warehouse sync (reverse ETL) writing product data; an enrichment integration; a middleware that forwards website demo forms to HubSpot.
- Team, by role label only: SDRs rep_a and rep_b, AE rep_c, customer success cs_a, CRM admin revops_admin.
- Motions in scope: inbound demo requests (hand-raise) and trial or content leads scored to MQL.
- Coverage: SDRs on America/New_York, Monday to Friday 09:00 to 17:00, holiday 2026-09-07 (calendar sdr_us); customer success on America/Chicago, 08:00 to 16:00 (calendar cs_us, used by the closed-won class, which has no rows in this fixture).
- Audit window: 2026-07-01 to 2026-09-30, as of 2026-09-30T20:00:00Z (16:00 in New York). The CRM holds more than 90 days of records, so this is an audit. The fixture holds only the window, so the runs omit `--since`; with 90 days of earlier history in the file, pass `--since 2026-07-01`.
- Files: [../assets/example-acme-transitions.csv](../assets/example-acme-transitions.csv) (54 history rows), [../assets/example-acme-requests.csv](../assets/example-acme-requests.csv) (18 requests), [../assets/workflow-config.json](../assets/workflow-config.json), and the redesigned spec excerpt [../assets/example-acme-spec.md](../assets/example-acme-spec.md).
- Source labels such as FORM:demo-request and WORKFLOW:mql-scoring are Acme's own TYPE:detail labels, mapped from whatever HubSpot recorded. They are not HubSpot's literal source names.

## Step 0: normalize an export

[../assets/example-acme-lifecycle-export.csv](../assets/example-acme-lifecycle-export.csv) is a wide lifecycle history export for four contacts, with placeholder headers (real export headers are not documented, so print only your header row), display labels instead of internal values, and user emails as sources. First list the raw sources for labelling, without printing them:

```bash
WORK=$(mktemp -d)   # any folder outside the skill
python3 scripts/normalize_history.py wide --in assets/example-acme-lifecycle-export.csv \
  --list-sources "$WORK/sources-to-label.csv" --object contacts --field lifecyclestage --id-column "Record ID" \
  --group value,time,source
```

```text
wide: 4 records, 32 value groups read, 19 rows found for 4 records and 1 fields
  skipped: no value and no time: 13
  2 email-like sources were replaced with UNMAPPED_USER labels; add them to --source-map as role labels (for example CRM_UI:rep_a)
  10 distinct sources written to sources-to-label.csv, 10 without a label; fill the label column with TYPE:detail labels and pass the file as --source-map
```

The labelled result is [../assets/example-source-map.csv](../assets/example-source-map.csv). Then normalize:

```bash
python3 scripts/normalize_history.py wide --in assets/example-acme-lifecycle-export.csv \
  --out "$WORK/transitions-lifecycle.csv" --object contacts --field lifecyclestage --id-column "Record ID" \
  --group value,time,source --source-map assets/example-source-map.csv \
  --value-map assets/example-value-map.csv --cap 45
```

```text
wide: 4 records, 32 value groups read, 19 rows written for 4 records and 1 fields
  skipped: no value and no time: 13
  records at or above the cap of 45 rows: 0
```

The 19 rows are identical to the lifecyclestage rows for c-1001, c-1003, c-1005 and c-1006 in the transitions fixture. The source map turned `rep.a@example.com` into `CRM_UI:rep_a`, so no address reaches the shared file.

## Step 1: run the audit

```bash
python3 scripts/workflow_audit.py transitions --config assets/workflow-config.json \
  --transitions assets/example-acme-transitions.csv --as-of 2026-09-30T20:00:00Z
python3 scripts/workflow_audit.py sla --config assets/workflow-config.json \
  --requests assets/example-acme-requests.csv --as-of 2026-09-30T20:00:00Z
```

Both exit with code 1 because there is at least one FAIL.

Transitions output:

```text
Transitions: example-acme-transitions.csv, 54 rows used (none skipped), 16 records, 4 fields. As of 2026-09-30T20:00:00Z.
PASS T01 lifecyclestage: no record at the contacts revision cap (45); most revisions on one record: 8
PASS T01 hs_lead_status: no record at the contacts revision cap (45); most revisions on one record: 4
PASS T01 hubspot_owner_id: no record at the contacts revision cap (45); most revisions on one record: 3
PASS T01 dealstage: no record at the deals revision cap (20); most revisions on one record: 4
WARN T02 lifecyclestage c-1011: unknown value 'trial' set 1 time(s) by INTEGRATION:warehouse-sync on 1 of 9 records (11.1%)
PASS T02 hs_lead_status: every value is in the configured lists
INFO T02 hubspot_owner_id: no value list configured; values not checked
PASS T02 dealstage: every value is in the configured lists
FAIL T03 lifecyclestage c-1005: 2 backward move(s) by WORKFLOW:recycle (automated, outside the correction path) on 1 of 9 records: c-1005 marketingqualifiedlead to lead on 2026-07-24 (clear-then-set); c-1005 marketingqualifiedlead to lead on 2026-08-17 (clear-then-set)
WARN T03 lifecyclestage c-1006: 1 backward move(s) by CRM_UI:rep_b (manual, no correction source) on 1 of 9 records: c-1006 customer to lead on 2026-09-15
INFO T03 hs_lead_status: no ordered list; backward moves, gates and skips not checked
INFO T03 hubspot_owner_id: no ordered list; backward moves, gates and skips not checked
WARN T03 dealstage d-2002: 1 backward move(s) by CRM_UI:rep_c (manual, no correction source) on 1 of 5 records: d-2002 evaluation to discovery on 2026-08-03
FAIL T04 lifecyclestage c-1003: 1 of 4 records entering customer (25.0%) had no prior opportunity, set by FORM:guide-download-v2: c-1003 on 2026-08-11 as its first value
WARN T04 dealstage d-2005: 1 move(s) from discovery to closedwon on 1 of 5 records (20.0%) skip evaluation and proposal, which allowed_skips does not list: d-2005 by CRM_UI:rep_c on 2026-08-05
PASS T05 lifecyclestage: no A to B to A flips between different sources within 24 h
PASS T05 hs_lead_status: no A to B to A flips between different sources within 24 h
FAIL T05 hubspot_owner_id c-1009: 1 flip(s) between CRM_UI:rep_a and WORKFLOW:rotation on 1 of 2 records (50.0%): c-1009 rep_a to rep_b (WORKFLOW:rotation) and back (CRM_UI:rep_a) within 0.25 h
FAIL T05 dealstage d-2002: 2 flip(s) between CRM_UI:rep_c and INTEGRATION:warehouse-sync on 1 of 5 records (20.0%): d-2002 discovery to evaluation (INTEGRATION:warehouse-sync) and back (CRM_UI:rep_c) within 2 h; d-2002 evaluation to discovery (CRM_UI:rep_c) and back (INTEGRATION:warehouse-sync) within 10 h
INFO T06 lifecyclestage: writers seen: AUTOMATION:deal-created 4, AUTOMATION:deal-won 2, CRM_UI:rep_a 2, CRM_UI:rep_b 3, FORM:demo-request 2, FORM:guide-download 2, FORM:guide-download-v2 1, FORM:trial-signup 1, FORM:webinar 1, IMPORT:events-list-07 1, INTEGRATION:billing-sync 1, INTEGRATION:warehouse-sync 1, WORKFLOW:mql-scoring 6, WORKFLOW:recycle 2
FAIL T06 lifecyclestage c-1003: FORM:guide-download-v2 wrote values it may not write: customer x1; 1 of 29 changes (3.4%)
FAIL T06 lifecyclestage c-1008: IMPORT:events-list-07 wrote values it may not write: marketingqualifiedlead x1; 1 of 29 changes (3.4%)
FAIL T06 lifecyclestage c-1004: INTEGRATION:billing-sync wrote values it may not write: customer x1; 1 of 29 changes (3.4%)
FAIL T06 lifecyclestage c-1011: INTEGRATION:warehouse-sync wrote values it may not write: trial x1; 1 of 29 changes (3.4%)
FAIL T06 lifecyclestage c-1005: WORKFLOW:recycle wrote values it may not write: lead x2; 2 of 29 changes (6.9%) (2 clear-then-set collapsed)
INFO T06 hs_lead_status: writers seen: CRM_UI:rep_a 2, WORKFLOW:lead-status 2
PASS T06 hs_lead_status: every change came from an allowed writer (CRM_UI:, WORKFLOW:lead-status)
INFO T06 hubspot_owner_id: writers seen: CRM_UI:rep_a 1, INTEGRATION:enrichment 1, WORKFLOW:rotation 3
FAIL T06 hubspot_owner_id c-1010: INTEGRATION:enrichment wrote values it may not write: rep_b x1; 1 of 5 changes (20.0%)
INFO T06 dealstage: writers seen: CRM_UI:rep_c 12, INTEGRATION:warehouse-sync 2
FAIL T06 dealstage d-2002: INTEGRATION:warehouse-sync wrote values it may not write: evaluation x2; 2 of 14 changes (14.3%)
WARN T07 lifecyclestage c-1008, c-1005, c-1007: 3 of 3 records now in marketingqualifiedlead have been there longer than 5 days; oldest: c-1008 78.46 days, c-1005 36.25 days, c-1007 20.27 days
WARN T07 dealstage d-2002, d-2004: 2 of 2 records now in evaluation have been there longer than 30 days; oldest: d-2002 57.75 days, d-2004 51.21 days
INFO T08 lifecyclestage: marketingqualifiedlead to salesqualifiedlead within 30 days, cohort 2026-07: entered 4, reached 2, 50.0%
INFO T08 lifecyclestage: marketingqualifiedlead to salesqualifiedlead within 30 days, cohort 2026-09: entered 1, immature until 2026-10-31
INFO T09 lifecyclestage: marketingqualifiedlead to salesqualifiedlead: median 0.96 days, p90 0.97 days (n=2)
INFO T08 lifecyclestage: salesqualifiedlead to opportunity within 30 days, cohort 2026-07: entered 4, reached 4, 100.0%
INFO T09 lifecyclestage: salesqualifiedlead to opportunity: median 3.50 days, p90 4.99 days (n=4)
INFO T08 lifecyclestage: opportunity to customer within 90 days, cohort 2026-07: entered 4, immature until 2026-10-30
INFO T09 lifecyclestage: opportunity to customer: median 28.00 days, p90 42.04 days (n=3)
INFO T08 dealstage: discovery to closedwon within 90 days, cohort 2026-07: entered 4, immature until 2026-10-30
INFO T08 dealstage: discovery to closedwon within 90 days, cohort 2026-08: entered 1, immature until 2026-11-30
INFO T09 dealstage: discovery to closedwon: median 35.02 days, p90 42.04 days (n=2)
WARN T10 lifecyclestage c-1005: 1 of 5 records entering marketingqualifiedlead (20.0%) entered it more than 2 times within 90 days: c-1005 3 entries (2026-07-10, 2026-08-03, 2026-08-25)
11 FAIL, 7 WARN, 9 PASS. Nothing was changed.
```

SLA output:

```text
Requests: example-acme-requests.csv, 18 rows, classes hand_raise, mql. As of 2026-09-30T20:00:00Z.
Calendars: hand_raise on sdr_us (America/New_York); mql on sdr_us (America/New_York).
PASS S01 hand_raise: all 11 eligible requests have received_at and crm_seen_at; no stop before start
FAIL S02 hand_raise: compliance 40.0% (4 on time of 10 due) against a 90% target; 12 requests: 1 excluded, 11 eligible; on time 4, late 4, overdue 2, not yet due 1
INFO S02 hand_raise: late: r-02 40, r-03 25, r-04 50, r-08 42 business minutes (target 15)
INFO S02 hand_raise: not yet due: r-11 10 (business minutes used of 15)
INFO S02 hand_raise: answered-only rate 50.0% (4 of 8 answered) is not compliance
INFO S02 hand_raise: excluded: spam 1
FAIL S03 hand_raise r-06, r-09: no first_response_at and past the target: 2 of 10 due (20.0%); business age r-06 14520, r-09 2100 minutes
WARN S04 hand_raise r-02, r-08: received-to-CRM p90 29 minutes is above 5 (median 1, n=11); slowest: r-02 31, r-08 29
INFO S04 hand_raise: CRM to assigned: median 1, p90 1 wall-clock minutes (n=10); assigned to first_response_at: median 10, p90 49 business minutes (n=8)
INFO S04 hand_raise: buyer clock among answered (n=8): median 19.5, p90 50 business minutes; wall-clock median 41, p90 3865 minutes
INFO S04 hand_raise: baseline for a first target, every due request with overdue ones at their age (n=10, 2 overdue): median 32.5, p90 2100 business minutes; fewer than 30 due, so use the buyer promise or a labelled proposal
INFO S04 hand_raise r-03, r-07: outside coverage hours the buyer waited longer than the SLA shows: r-03 3865 wall-clock for 25 business minutes, r-07 1325 wall-clock for 5 business minutes
WARN S05 hand_raise r-04: 1 of 11 measured requests (9.1%) with 2 or more owner changes before the stop event: r-04 2
WARN S06 hand_raise r-02, r-04, r-08: on time by the owner clock (from last_assigned_at) but late for the buyer; a report that starts at assignment shows these as on time
INFO S06 hand_raise: owner clock on time for 7 of 8 answered (87.5%); buyer clock on time for 4 of 8 (50.0%)
FAIL S07 hand_raise r-06: 1 of 11 measured requests (9.1%) with no owner past 15 business minutes; business age r-06 14520
WARN S07 hand_raise r-02, r-08: 2 of 11 measured requests (18.2%) assigned after 15 business minutes: r-02 32, r-08 30
PASS S01 mql: all 6 eligible requests have received_at and crm_seen_at; no stop before start
FAIL S02 mql: compliance 60.0% (3 on time of 5 due) against a 90% target; 6 requests: 0 excluded, 6 eligible; on time 3, late 1, overdue 1, not yet due 1
INFO S02 mql: late: m-03 1080 business minutes (target 480)
INFO S02 mql: not yet due: m-06 120 (business minutes used of 480)
INFO S02 mql: answered-only rate 75.0% (3 of 4 answered) is not compliance
FAIL S03 mql m-05: no disposition_at and past the target: 1 of 5 due (20.0%); business age m-05 7110 minutes
PASS S04 mql: received-to-CRM p90 0 minutes is within 5 (n=6)
INFO S04 mql: CRM to assigned: median 1, p90 1 wall-clock minutes (n=6); assigned to disposition_at: median 299, p90 1079 business minutes (n=4)
INFO S04 mql: buyer clock among answered (n=4): median 300, p90 1080 business minutes; wall-clock median 1260, p90 3000 minutes
INFO S04 mql: baseline for a first target, every due request with overdue ones at their age (n=5, 1 overdue): median 360, p90 7110 business minutes; fewer than 30 due, so use the buyer promise or a labelled proposal
INFO S04 mql m-03, m-04, m-02: outside coverage hours the buyer waited longer than the SLA shows: m-03 3000 wall-clock for 1080 business minutes, m-04 1320 wall-clock for 360 business minutes, m-02 1200 wall-clock for 240 business minutes
PASS S05 mql: no request with 2 or more owner changes
PASS S06 mql: no request on time for the owner but late for the buyer
INFO S06 mql: owner clock on time for 3 of 4 answered (75.0%); buyer clock on time for 3 of 4 (75.0%)
PASS S07 mql: no request waited more than 60 business minutes for an owner
INFO S01 closed_won_to_cs: configured, but the requests file has no rows of this class, so it was not audited
5 FAIL, 4 WARN, 6 PASS. Nothing was changed.
```

## Step 2: what the agent does with each finding

Each FAIL is confirmed on one record in HubSpot (the property's history on the record: value, time, source) before it goes in the audit.

- T03 FAIL and T10 WARN, c-1005: the recycle workflow clears lifecycle and sets Lead when a rep picks Bad timing. Scoring re-qualifies the contact, so marketing counted three MQLs for one person (1 of 5 records that entered MQL). Owner: marketing ops. Fix: CR-03.
- T04 FAIL and T06 FAIL, c-1003: a contact became Customer as its first value from FORM:guide-download-v2, 1 of 4 records that entered Customer. Owner: marketing ops. Fix: CR-02.
- T06 FAIL, c-1004: INTEGRATION:billing-sync set Customer. Owner: finance for billing state, RevOps for lifecycle. Fix: CR-01.
- T06 FAIL, c-1008, and the oldest T07 MQL: an event list import set MQL directly, and nobody worked it for 78 days. Owner: marketing ops. Decision needed: imports may set Lead only (the config already allows IMPORT: for lead and nothing else). Correct c-1008 through the correction workflow after review.
- T02 WARN and T06 FAIL, c-1011; T05 and T06 FAIL, d-2002: INTEGRATION:warehouse-sync writes a "trial" lifecycle value and fights rep_c over deal stage twice in one day. Owner: data owner. Fix: CR-06.
- T05 FAIL, c-1009, with S05 WARN on r-04: the rotation workflow moved an accepted request from rep_a to rep_b, and rep_a took it back by hand. Owner: CRM admin. Fix: CR-04.
- T06 FAIL, c-1010: INTEGRATION:enrichment wrote the contact owner overnight. Owner: CRM admin. Fix: CR-05.
- T03 WARN, c-1006: rep_b moved a customer back to Lead with no correction source. Decision needed from revops_admin: if the account churned, churn belongs in customer state, not lifecycle.
- T03 WARN, d-2002: the rep's backward move is the other half of the warehouse fight; it resolves with CR-06.
- T04 WARN, d-2005: discovery straight to closed won, 1 of 5 deals. Decision needed from the sales lead: add the skip to allowed_skips for single-call deals, or require the stages.
- T07 WARN: c-1007 has sat in MQL for 20 days; it is the same record as the overdue MQL request m-05. d-2004 has been in evaluation for 51 days against a 30-day limit: a pipeline review item, not a defect.
- T08: the July MQL to SQL rate of 50.0% includes the looping c-1005 and the imported c-1008. Restate it after the corrections. The September and opportunity to customer cohorts are immature and are not reported as rates.
- S02 and S06: hand-raise compliance is 40.0% of 10 due, while the owner clock, which starts at assignment like HubSpot's Lead response time report, shows 7 of 8 on time. r-02, r-04 and r-08 look fine on the native report and were late for the buyer.
- S04 baseline: 32.5 business minutes over the 10 due hand-raises, with r-06 and r-09 at their age, against an answered-only median of 19.5. Only 10 are due, so the target stays the demo page promise of 15; the register records the baseline with its n. The MQL baseline, 360 over 5 due, keeps the 480-minute proposal.
- S04 and S07 WARN: r-02 and r-08 waited about 30 minutes before HubSpot saw them, so they got an owner only after 32 and 30 business minutes against the 15-minute limit (2 of 11 measured). The middleware, not the reps. Fix: CR-07.
- S03 and S07 FAIL: r-06 never got an owner and was never answered (14,520 business minutes); r-09 was assigned and never answered (2,100). That is 2 of 10 due unanswered, so no target can reach 90% until they are handled. No escalation ladder exists; the SLA register adds one, and CR-04 adds the no-owner branch.
- r-03 and r-07: 25 and 5 business minutes, but the buyers waited 3,865 and 1,325 wall-clock minutes over a weekend and a holiday. Show both numbers when the team debates coverage.
- S02 mql: 60.0% of 5 due; m-03 took 1,080 business minutes and m-05 is overdue at 7,110.
- S01 INFO, closed_won_to_cs: the class is configured but this fixture has no closed-won rows, so the run says it was not audited. In a real audit, add its rows or list the handoff under Not checked.

## Step 3: checks the files cannot show

- M1: the "Guide download v2" form was copied from a customer referral form and kept its lifecycle setting at Customer (cause of c-1003).
- M2: the billing integration maps subscription status into lifecycle. c-1004's subscription uses send_invoice and its first invoice is still open, so Customer was set before payment.
- M3: the rotation workflow re-enrolls when company size changes and has overwrite on. An enrichment update re-ran it for c-1009 (r-04).
- M4: the enrichment integration maps its own owner field to the contact owner (c-1010).
- M5: the warehouse sync writes deal stage from product usage and created the lifecycle option "trial" (d-2002, c-1011).
- M6: the recycle workflow clears lifecycle and sets Lead on Bad timing (c-1005).
- M7: the middleware forwards website demo forms in 30-minute batches (r-02, r-08).
- M8: on 2026-08-18 every SDR was set to Away. "Assign to available users only" left r-06 with no owner, and no fallback branch existed.
- M9: stage calculated properties were off for the "evaluation" stage added on 2026-07-15, so native velocity reports for it were blank.

## Step 4: the first change requests

CR-01 and CR-04 are written out in full in [../assets/change-list-template.md](../assets/change-list-template.md).

- CR-01: stop the billing integration writing lifecycle stage; map status to subscription_status and the latest invoice status to latest_invoice_status, which is the paid evidence. Cohort: c-1004, corrected through the correction workflow.
- CR-02: set the "Guide download v2" form's lifecycle stage to Lead; correct submissions since 2026-08-11 through the correction workflow with a reason.
- CR-03: replace the recycle workflow. The SDR rejects the lead (sdr_decision rejected, reason bad_timing), the workflow sets recycle_date, lifecycle is untouched, and each demo request, score-path MQL and re-qualification gets exactly one lead record (the redesigned spec).
- CR-04: move rotation to lead records: turn off "Sync lead owner with contact or company owner"; a lead-based rotation for the leads demo intake and MQL scoring create, with available users only, overwrite off, a no-owner branch to the SDR team lead and an unowned view checked hourly; then turn off the contact-based rotation, so rotation stops writing hubspot_owner_id.
- CR-05: remove the owner mapping from the enrichment integration.
- CR-06: stop the warehouse sync writing deal stage and lifecycle; write product summary properties instead; retire the "trial" option after moving its records.
- CR-07: the middleware pushes each submission on receipt and carries received_at from the form tool.
- CR-08: turn on stage calculated properties for "evaluation" (values fill in retroactively).

Each change request's Test field holds its rehearsal plan. revops_admin runs the cases after the list is approved; the agent compares the recorded results with reads only.

## Step 5: proposed martech-context updates

Shown to the user, written only after approval:

- metrics.md: MQLs in a period = first MQL entries in the period, effective 2026-10-01; SLA compliance = on time / (eligible - not yet due); the SLA baseline = median over due requests with unanswered ones at their age; the owner clock is a coaching metric.
- data-dictionary.md: received_at, first_response_at, sdr_decision, disposition_at, disqualification_reason, closed_lost_reason, recycle_date, subscription_status and latest_invoice_status with their writers; lifecyclestage value-level writers.
- decisions.md: Customer comes only from deal won or the correction workflow; billing writes subscription_status and latest_invoice_status, never lifecycle; the two coverage calendars; the correction path and its approver.

## Second fixture: Salesforce Lead history

[../assets/example-sf-lead-history.csv](../assets/example-sf-lead-history.csv) is a LeadHistory extract for three Leads of a second fictional company, Example Co, with placeholder user IDs. Each owner change appears twice, once with IDs and once with names, as Salesforce can record it; `--keep-values` keeps the ID form (real IDs start 005 for users and 00G for queues; these placeholders start user- and queue-). History objects start at the first tracked change, so `old=OldValue` writes each Lead's starting value, dated at its created row:

```bash
python3 scripts/normalize_history.py long --in assets/example-sf-lead-history.csv --out "$WORK/transitions-sf.csv" \
  --object leads --map record=LeadId,field=Field,value=NewValue,time=CreatedDate,source=CreatedById,old=OldValue \
  --only-fields Status,Owner --field-map Status=lead_status,Owner=owner --keep-values "Owner=^(user-|queue-)" \
  --source-map assets/example-source-map.csv
python3 scripts/workflow_audit.py transitions --config assets/example-salesforce-config.json \
  --transitions "$WORK/transitions-sf.csv" --as-of 2026-08-31T00:00:00Z
```

```text
long: 14 rows read, 13 rows written for 3 records and 2 fields
  skipped: created rows (used for starting values): 3
  skipped: field not selected: 1
  skipped: value filtered by --keep-values: 2
  starting values written: 5
```

```text
Transitions: transitions-sf.csv, 13 rows used (none skipped), 3 records, 2 fields. As of 2026-08-31T00:00:00Z.
INFO T01 lead_status: no revision cap set for leads (unknown or not applicable); check one busy record against the CRM before trusting transition counts
INFO T01 owner: no revision cap set for leads (unknown or not applicable); check one busy record against the CRM before trusting transition counts
PASS T02 lead_status: every value is in the configured lists
INFO T02 owner: no value list configured; values not checked
FAIL T03 lead_status L-3003: 1 backward move(s) by INTEGRATION:marketing-automation (automated, outside the correction path) on 1 of 3 records: L-3003 Unqualified to Open on 2026-08-24
INFO T03 owner: no ordered list; backward moves, gates and skips not checked
PASS T04 lead_status: no gate violations and no unexpected skips
FAIL T05 lead_status L-3003: 2 flip(s) between CRM_UI:rep_b and INTEGRATION:marketing-automation on 1 of 3 records (33.3%): L-3003 Open to Unqualified (CRM_UI:rep_b) and back (INTEGRATION:marketing-automation) within 0.33 h; L-3003 Unqualified to Open (INTEGRATION:marketing-automation) and back (CRM_UI:rep_b) within 0.58 h
PASS T05 owner: no A to B to A flips between different sources within 24 h
INFO T06 lead_status: writers seen: CRM_UI:rep_a 3, CRM_UI:rep_b 2, FORM:web-to-lead 2, INTEGRATION:marketing-automation 2
PASS T06 lead_status: every change came from an allowed writer (CRM_UI:, FORM:web-to-lead, INTEGRATION:marketing-automation)
INFO T06 owner: writers seen: CRM_UI:rep_a 1, FORM:web-to-lead 1, INTEGRATION:enrichment 1, INTEGRATION:marketing-automation 1
FAIL T06 owner L-3001: INTEGRATION:enrichment wrote values it may not write: user-rep-b x1; 1 of 4 changes (25.0%)
WARN T07 lead_status L-3001: 1 of 1 records now in Working have been there longer than 10 days; oldest: L-3001 18.40 days
INFO T08 lead_status: Open to Qualified within 30 days, cohort 2026-08: entered 3, immature until 2026-10-01
INFO T09 lead_status: Open to Qualified: median 1.04 days, p90 1.04 days (n=1)
3 FAIL, 1 WARN, 4 PASS. Nothing was changed.
```

What the agent does with it:

- T06 FAIL, L-3001: the enrichment integration user changed the owner one second after its own Industry update. The REST update did not send `Sforce-Auto-Assign: FALSE`, so the active assignment rule ran again (FM05 in [failure-modes.md](failure-modes.md)). Fix: the integration sends the header; restore the owner from history.
- T03 and T05 FAIL, L-3003: the marketing automation sync wrote Open over rep_b's Unqualified, and rep_b set it back 35 minutes later. T06 passes because the config allows that sync to write Lead Status, which is the defect: a sync and a rep disagreeing with no written precedence (FM03). T05 catches what a field-level writer list allows. Fix: the sync stops writing Status after creation.
- Without `--keep-values`, the two name-form rows would add two more owner changes ("Rep B", "Rep A") and double every owner count.
- L-3002 sat in the inbound queue for 12 minutes after creation. For a requests file, its assigned_at is 15:12, the first change to a user, not the queue entry at 15:00 (FM24).
- T01 INFO: Salesforce history has no revision cap. Its limits are field history tracking and retention, so confirm tracking is on for Status and Owner before trusting the counts.
- T08: the August cohort is immature until 2026-10-01, so no rate is reported.

## Self-tests

```bash
python3 scripts/workflow_audit.py self-test
python3 scripts/normalize_history.py self-test
```

Expected: `self-test passed (35 checks)` and `self-test passed (15 checks)`.
