# Worked example: Acme Analytics (fictional)

Acme Analytics is a fictional B2B SaaS company on www.example.com. An agency hands over six flows and the RevOps lead asks for a review of all six and a redesign of the riskiest. Mode: Review, then Design for the billing flow. Every company, ID and value here is fictional.

## Files

- [../assets/example-acme-stack.md](../assets/example-acme-stack.md): the `stack.md` the agent reads first (tools, owners, integration identities, the seven-digit `acme_account_id` with leading zeros, open decisions).
- [../assets/example-acme-flow-inventory.csv](../assets/example-acme-flow-inventory.csv), [../assets/example-acme-field-map.csv](../assets/example-acme-field-map.csv) and [../assets/example-acme-test-plan.csv](../assets/example-acme-test-plan.csv): the six flows as handed over.
- [../assets/example-acme-n8n-workflow.json](../assets/example-acme-n8n-workflow.json): the agency's n8n demo-request workflow.
- [../assets/example-acme-expected.csv](../assets/example-acme-expected.csv): the billing summary Stripe says HubSpot should hold. [../assets/example-acme-hubspot-export.csv](../assets/example-acme-hubspot-export.csv): HubSpot after the agency's flow ran. [../assets/example-acme-hubspot-baseline.csv](../assets/example-acme-hubspot-baseline.csv): HubSpot before the run.
- [../assets/example-acme-integration-spec.md](../assets/example-acme-integration-spec.md), with [../assets/example-acme-redesign-inventory.csv](../assets/example-acme-redesign-inventory.csv) and [../assets/example-acme-redesign-field-map.csv](../assets/example-acme-redesign-field-map.csv): the redesigned billing flow.

## Commands

From the skill folder:

```bash
python3 scripts/lint_integration.py assets/example-acme-flow-inventory.csv \
  assets/example-acme-field-map.csv assets/example-acme-test-plan.csv
python3 scripts/check_ipaas_export.py assets/example-acme-n8n-workflow.json --tier 1
python3 scripts/size_sync.py --records 48000 --batch 1 --preset hubspot-crm-search --retry-overhead 0.1
python3 scripts/size_sync.py --records 48000 --batch 100 --readback-calls-per-batch 1 \
  --preset hubspot-private-pro --remaining 600000 --reserve 0.2 --other-calls 50000 --compare-single
python3 scripts/reconcile_readback.py --source assets/example-acme-expected.csv \
  --dest assets/example-acme-hubspot-export.csv --key acme_account_id \
  --map assets/example-acme-field-map.csv --flow billing_to_crm \
  --baseline assets/example-acme-hubspot-baseline.csv --dest-id hs_object_id
python3 scripts/lint_integration.py assets/example-acme-redesign-inventory.csv \
  assets/example-acme-redesign-field-map.csv
```

## Lint of the handover

Ends with `26 FAIL, 17 WARN, 25 PASS. Nothing was changed.` and exit code 1. Lines, shortened:

```text
FAIL FO01 field HubSpot company.next_renewal_date: written by billing_to_crm and lifecycle_sync (rows 3, 8) without version or effective-time conflict rules on every row
FAIL ID01 flow billing_to_crm: match key 'company_domain' is a domain; names collide and domains are shared, so match on a stable source ID ...
FAIL IP02 flow billing_to_crm: idempotency key 'stripe_event_id + received_at' uses attempt or arrival data; ...
FAIL IP03 flow billing_to_crm: ledger kept 1 day, shorter than the 30-day replay window
FAIL RT01 flow billing_to_crm: nested attempts multiply to 15 (sdk:3;worker:5); ...
FAIL WH01 flow demo_to_salesforce: acknowledgement p99 7.5 s is above half the sender's 10 s timeout; ...
WARN FO03 field-map row 7 (lifecycle_sync Salesforce contact.Lifecycle_Stage__c): two-way decision-driving field resolved by a global winner; ...
FAIL RM01 flow pql_to_tools: current-membership contract with removal_policy none; ...
FAIL OP05 flow ads_offline: tier 1 flow reconciled monthly; reconcile tier 1 flows daily, weekly at the least
```

Findings per flow:

- billing_to_crm (11 FAIL, 5 WARN): ID01 and ID02 (domain match, creates without a unique field), IP02 (key built from `received_at`), IP03 (1-day ledger against Stripe's 30-day CLI resend window), RT01 (3 x 5 attempts), TP01 (eight mandatory cases missing: out_of_order, unknown_outcome_retry, unknown_event_type, malformed_payload, backfill_no_outreach, replay_no_duplicate, credential_revoked, loop_check), TP02 (production test B09 with no approver), FO01, FO02 and FO04 on `next_renewal_date`, FO06 (`mrr_amount` with no currency). WARN: WH01 (sender timeout unknown), TP03 (B05 and B08 failed), TP04 (B04 and B10 without evidence).
- demo_to_salesforce (7 FAIL, 4 WARN): CT03, ID02, IP01, WH01, OP01, OP03, TP01; WARN ID01 (email on a lead), RT02, OP04, FO07.
- lifecycle_sync (1 FAIL, 3 WARN): TP01; WARN ID03 and FO03 on `Lifecycle_Stage__c` and `next_renewal_date` ("Salesforce wins" as a global setting).
- pql_to_tools (4 FAIL, 4 WARN): RM01, TP01, FO04 (`pql_score` cleared on blanks), FO05 (`pql_tier` with no allowed values); WARN MS02, OP02, FO07, FO09 (the model writes HubSpot's `lifecycle_stage`).
- product_events: PASS lines only (keys, retention, retries, test coverage).
- ads_offline (3 FAIL, 1 WARN): IP03 (7-day ledger against the 60-day replay window the inventory records), OP05, TP01; WARN OP04.

## What the agent does with the lint

- billing_to_crm: domain matching plus create-on-miss explains the duplicate company in the read-back and the failed test B08; the receipt-time key explains the two renewal tasks in B05; the stale value on 0001006 is the out-of-order case nobody tested. Redesign rather than patch (below).
- Renewal date: two writers. The agent does not pick the owner. It raises decision D-1: billing owns `next_renewal_date` and the connector maps it one-way into Salesforce, or Salesforce owns a separate contracted renewal field. It is recorded in `decisions.md` after the RevOps lead answers.
- demo_to_salesforce: blind retries on two creates plus Continue after a failed lookup can create duplicate leads and tasks, and a 7.5-second reply against the website's 10-second timeout breaks WH01. CR-2 below.
- pql_to_tools: one row covers two destinations; split it into two flows. Add a removal policy (clear the score and set `pql_state` to not_qualified when an account leaves the model), change the blank policy to skip_and_flag, give `pql_tier` allowed values, and either relax freshness to 30 minutes or move the trigger path off reverse ETL.
- ads_offline: raise ledger retention to at least 60 days, pin the API version, reconcile at least weekly, and confirm the upload path uses the Data Manager API given the 15 June 2026 restriction.
- lifecycle_sync: replace the global winner with per-field direction. Matching on the Salesforce ID while the connector creates records is acceptable only if the connector owns creation; record that in the spec.

## n8n export check

`check_ipaas_export.py` on the demo workflow at tier 1 ends with `5 FAIL, 5 WARN, 1 PASS, 1 INFO. Nothing was changed.` and exit code 1:

- IX01 FAIL "Create lead" and "Create owner task": POST with retryOnFail and no idempotency evidence.
- IX02 FAIL "Find lead by email": Continue passes the last valid data into both creates.
- IX04 FAIL: no error workflow on a tier 1 flow.
- IX05 FAIL "Find lead by email": a literal Authorization header (the value is never printed).
- IX06 WARN: the webhook has no authentication.
- IX07 WARN: the webhook answers after the last node while calling three external nodes, and failed executions are not saved.
- IX08 WARN on both create nodes: fixed retry waits that ignore Retry-After.
- IX09 INFO: the workflow is active. IX10 WARN: the "Normalise fields" Code node needs a manual review.
- PASS IX03: no legacy continueOnFail.

## Budget

- Agency design, one CRM search per company to find it by domain: `52,800` calls at 3.50/s, about 15,086 seconds (4.2 hours), and every ambiguous domain still mismatches. Output ends `0 FAIL, 0 WARN, 1 PASS, 1 NOT CHECKED` because no remaining figure was given.
- Redesign, batch upsert on the unique `acme_account_id` plus one batch read-back per batch: 480 write plus 480 read-back calls, 1,056 with retry overhead, about 79 seconds at 13.30/s, 0.17% of 625,000; available 425,000; `PASS RL01` and `PASS RL02`.
- The same writes one record per call (`--compare-single`): 52,800 calls, 8.45% of the daily allowance, about 3,970 seconds.

## Read-back of the billing flow

Ends with `7 FAIL, 3 WARN, 2 PASS, 1 NOT CHECKED. Nothing was changed.` and exit code 1:

```text
Matched: 10 exact, 1 after key repair; 8 with every owned field equal
FAIL R1 MISSING 0001007: expected in the destination, not found
FAIL R2 DUPLICATE 0001003: 2 destination records (hs_object_id 9001003, 9001013)
FAIL R3 MISMATCH 0001004 billing_status: expected "past_due", found "active" (hs_object_id 9001004)
FAIL R3 MISMATCH 0001006 next_renewal_date: expected "2026-11-30", found "2026-10-31" (hs_object_id 9001006)
FAIL R3 MISMATCH 0001006 billing_state_revision: expected "18", found "17" (hs_object_id 9001006)
FAIL R4 NULL_OVERWRITE 0001005 next_renewal_date: expected "2026-12-15", destination blank (hs_object_id 9001005)
FAIL R5 KEY_MANGLED 0001009: destination stores the key as "1009" (leading zeros stripped) (hs_object_id 9001009); ...
WARN R6 ORPHAN 0001099: in the destination with no source row (hs_object_id 9001099); ...
WARN R8 NON_OWNED_CHANGED hs_object_id 9001002 lifecycle_stage: "customer" before, "opportunity" now; ...
WARN R8 NON_OWNED_CHANGED hs_object_id 9001010 lifecycle_stage: "opportunity" before, "lead" now; ...
```

How each finding is explained:

- 0001007 MISSING: the cancelled account's company was never matched; its domain is shared with another customer.
- 0001003 DUPLICATE: a failed domain lookup created a second company (create on lookup failure).
- 0001004 MISMATCH: a delayed `active` event overwrote `past_due` (no version check).
- 0001006 MISMATCH on two fields: an older event overwrote the newer renewal date and revision (out of order).
- 0001005 NULL_OVERWRITE: an event without `current_period_end` cleared the date (blank policy clear).
- 0001009 KEY_MANGLED: a spreadsheet import stripped the leading zeros.
- 0001099 ORPHAN: a company with billing fields but no Stripe customer; ask who wrote them.
- 9001002 and 9001010 NON_OWNED_CHANGED: `lifecycle_stage` changed during the window; this flow does not own it, so field history should show the HubSpot Salesforce connector, not the billing worker.
- No finding for 0001001 (1200 equals 1200.00) or 0001012 (blank renewal date on both sides).

## Redesign

The redesigned spec is [../assets/example-acme-integration-spec.md](../assets/example-acme-integration-spec.md). Its lint ends with `0 FAIL, 2 WARN, 6 PASS, 1 NOT CHECKED` and exit code 0:

- WH01 WARN: Stripe's webhook page gives no timeout figure, so the sandbox measurement of acknowledgement time is still owed.
- FO02 WARN on `next_renewal_date`: the HubSpot Salesforce connector stays listed under `other_writers` until CR-3 makes its mapping one-way. The version rule binds only the billing worker; the connector never compares `billing_state_revision`.
- The test plan is not linted until it is written (NOT CHECKED).

FO01 only sees the flows it is given. Linted together with the handed-over lifecycle_sync rows, the renewal date still fails FO01 (`1 FAIL, 5 WARN, 11 PASS, 1 NOT CHECKED`), and it keeps failing until decision D-1 and CR-3 land. When you report the redesign's own lint, say that FO01 still fails across the two flows.

## Decisions needed

- D-1: owner of the renewal date (RevOps lead).
- D-2: one summary per currency for accounts billed in two currencies (finance systems lead).
- Owner rule for demo requests: task in Salesforce or HubSpot (head of sales development).

## First two change requests

CR-1 Replace the billing flow's domain matching and receipt-time key (ID01, ID02, IP02, IP03, RT01 FAIL):

- Entity: billing_to_crm worker and the HubSpot company property `acme_account_id`.
- Current setting: match on `company_domain`, create on miss, key `stripe_event_id + received_at`, ledger 1 day, SDK 3 attempts inside a worker with 5.
- Change: create `acme_account_id` as a unique value property; backfill it from the crosswalk; match on it; stop creating companies; store the Stripe event ID as the delivery key and key effects on billing_to_crm + acme_account_id + billing_state_revision; ledger retention 35 days; SDK retries off.
- Why: the read-back shows a duplicate company, a stale write and a mangled key; test B05 created two renewal tasks.
- Could break: companies that only matched by domain become exceptions (about 3% from the read-back).
- Test: B01 to B10 plus the eight missing cases, in the sandbox.
- Rollback: redeploy the previous worker version. It does not remove the property or undo company updates already written.

CR-2 Make the n8n demo flow idempotent and observable (IX01, IX02, IX04, IX05, WH01):

- Entity: n8n workflow "Acme demo request to Salesforce".
- Change: answer the webhook immediately after storing the payload in an n8n data table keyed by `submission_id`; remove retryOnFail from the creates and replace them with update-or-create on a unique field plus a task claim per `submission_id`; set the lookup's On Error to Continue (using error output) routed to an exception path; set an error workflow; move the Authorization header into the credential store and rotate it; add header authentication on the webhook.
- Test: duplicate submission, timeout after create, lookup failure, request without the auth header.
- Rollback: re-import the exported version. Leads or tasks created during the test stay and are listed for cleanup.

## Regression fixtures

Run these after changing a script and compare the summary lines.

Lint: `python3 scripts/lint_integration.py assets/regression-flow-inventory.csv assets/regression-field-map.csv` ends with `10 FAIL, 9 WARN, 144 PASS, 1 NOT CHECKED`:

- reg_bom_quoted: the file starts with a byte order mark and the rationale holds quoted commas; it parses with no CT01.
- reg_duplicate (two rows): CT02 FAIL once.
- reg_trigger_webhook: CT06 WARN.
- reg_retry_product_10 (`sdk:2;worker:5`): RT01 WARN only. reg_retry_product_12 (`sdk:3;worker:4`): RT01 FAIL only. reg_retry_zapier_6: no RT01.
- reg_ack_half (2.5 of 5 s): WH PASS. reg_ack_over_half (2.6): WH01 FAIL.
- reg_ledger_equal (30 and 30): IP PASS. reg_ledger_short (29 and 30): IP03 FAIL.
- reg_alert_twice (120 against 60): no OP02. reg_alert_over (121): OP02 WARN.
- reg_email_company: ID01 FAIL. reg_email_contact: ID01 WARN. reg_composite: no ID finding.
- reg_key_attempt (`order_id + delivery_attempt`): IP02 FAIL. reg_key_line (`order_id + line_number`): no IP finding.
- reg_update_nonunique (update-only, `destination_unique` no): ID04 WARN only. reg_layers_blank (blank `retry_layers`): RT01 WARN only. reg_inbound_blank (blank `inbound_webhook`): CT01 FAIL only.
- reg_removal_na (`membership_contract` current, `removal_policy` not_applicable): RM01 FAIL only. reg_key_event_only (`reg_key_event_only + hubspot_event_id` on a flow that creates tasks): IP02 WARN only, because naming the flow adds nothing to a delivery key.
- Field map: `plan_tier` written by two flows with version rules, FO01 WARN (confirm both compare the same version field); `seat_count` with one owner_wins row, FO01 FAIL; `billing_region` enum without allowed values, FO05 FAIL; `renewal_date` with another writer listed under a version rule, FO02 WARN.
- Note: the second reg_duplicate row prints its own PASS lines; CT02 is reported once, against the first row.

iPaaS: `python3 scripts/check_ipaas_export.py assets/regression-n8n-workflow.json --tier 2` ends with `2 FAIL, 5 WARN, 3 PASS`: IX01 FAIL on "PUT without key", IX01 WARN on "App node default operation" (retryOnFail on a Salesforce node with no operation in the export), IX05 on "Literal token call", IX03 on "Legacy continue", IX08 on the three HTTP retry nodes (the louder under-1,000 ms note on "PUT without key"). No IX01 for the POST with an Idempotency-Key header or the upsert, and no IX02, IX04, IX06 or IX07. "Sanitized node" has no `parameters` key: it gets no IX01, the Not checked line names it, and IX01, IX05, IX06 and IX07 print no PASS.

`python3 scripts/check_ipaas_export.py assets/regression-n8n-mcp-details.json --tier 1` ends with `5 FAIL, 5 WARN, 1 PASS, 1 INFO`, the same as the Acme Download. The file is the Acme workflow as `get_workflow_details` returns it, in the `{"workflow": ..., "triggerInfo": ...}` wrapper, with a different unpublished draft at the top level, so the header says `published version`.

`python3 scripts/check_ipaas_export.py assets/regression-make-blueprint.json --tier 1 --ordered yes` ends with `6 FAIL, 4 WARN, 1 PASS, 1 INFO`: IX05 twice on module 3 (a literal Authorization header and `api_key` query value; the mapped `X-Session-Token` header is not flagged), IX21 (dlq false), IX22 (dataloss true), IX24 (module 3, POST with a Retry handler), IX25 (module 5, Skip on a create); WARN IX23, IX28, IX29 (module 2 responds before module 3), IX30 (module 7); INFO IX27. Module names are synthetic.

Read-back: `python3 scripts/reconcile_readback.py --source assets/regression-readback-expected.csv --dest assets/regression-readback-destination.csv --key sf_account_id:sf_id --fields is_customer,arr,tier --expected-absent assets/regression-readback-absent.csv --tolerance 0.005` ends with `3 FAIL, 3 WARN, 3 PASS, 1 NOT CHECKED`:

- MISMATCH on `001Ef00000StU56` is_customer (false against 1); KEY_MANGLED for `001Pq00000Rst34` stored as `001PQ00000RST34`; NOT_REMOVED for `001Gh00000VwX78`.
- WARN ORPHAN `001Jk00000YzA90`, CASE_COLLISION for `001Ab00000XyZ12` and `001ab00000xyz12`, MIXED_SF_ID (15- and 18-character keys; `001Ab00000XyZ12` becomes `001Ab00000XyZ12IAF`).
- No finding for true against yes, yes against 1, 1000.00 against 1000.004 within the tolerance, or 0 against 0.00.
- Adding `--membership current --dest-case-insensitive` turns ORPHAN and CASE_COLLISION into FAIL: `5 FAIL, 1 WARN, 3 PASS, 1 NOT CHECKED`.
- Passing `--fields is_customer,arr,tier,plan` instead exits 2 with `owned field(s) plan in neither export`: a missing owned field stops the run rather than shrinking the comparison.

Sizing: the five commands in "Budget" above and in [rate-limits-and-backfills.md](rate-limits-and-backfills.md) give 52,800 calls (search per record), 1,056 calls PASS (redesign), 52,800 calls at 8.45% (single-record comparison), FAIL with 6,000 records fitting and 2,000 deferred (Salesforce), and `python3 scripts/size_sync.py --search-results 23000 --preset hubspot-crm-search` FAIL with 3 windows.
