---
name: lifecycle-marketing-audit
description: Runs a read-only audit of a lifecycle marketing programme in Braze, Customer.io, Klaviyo, HubSpot or Iterable, across every system that emails, texts or pushes the same people. Rates journey coverage by lifecycle stage against source-of-truth volumes, triggers and event data, segments, frequency caps and suppression, preference and consent enforcement, deliverability and authentication, and holdout measurement as PASS, WARN or FAIL, with dependency-free scripts for inventories, send logs, message headers and holdout maths, a seed test plan and a prioritised gap list. Changes no settings. Use when asked for a lifecycle, CRM marketing, email or retention audit, a Braze, Customer.io, Klaviyo or Iterable audit or health check, a HubSpot marketing email audit, a flow or journey audit, when people get too many messages or messages after unsubscribing, before an ESP renewal or migration, after a handover, to check Gmail, Yahoo and Microsoft bulk sender rules, or when asked whether lifecycle revenue is incremental.
license: CC-BY-4.0
compatibility: Scripts need Python 3.9 or later and no packages. The audit also works from exports, dashboards and read-only MCP servers without them.
metadata:
  tier: free
  category: lifecycle-messaging
  version: "0.1.0"
  last_reviewed: "2026-11-06"
  status: reviewed
---

# Lifecycle marketing audit

Audits one lifecycle programme across every system that messages the same people: lifecycle platform, CRM marketing email, billing and product notifications, SMS and push. It rates seven areas PASS, WARN or FAIL per rule and ends in a coverage map, script evidence and a prioritised gap list with change requests. A configuration finding is a lead until data or a seed test confirms it.

The rule: a send count, an attributed revenue figure or a green DNS checker proves nothing about one customer. Every FAIL cites a configuration quote, a script line, a header excerpt or a dated dashboard reading.

## When to use

Use this skill when:

- Someone asks for an audit, health check or review of a lifecycle, CRM marketing, email or retention programme.
- An ESP renewal, migration, new lifecycle lead or agency handover needs a baseline.
- People get too many messages, or messages after unsubscribing.
- Gmail, Yahoo and Microsoft bulk sender rules need checking, or someone asks whether lifecycle revenue is incremental.

Use something else when (if a planned skill is not installed, use its fallback):

- One journey needs designing or rewriting: journey-spec-and-qa. Fallback: the contract fields in [journey-coverage.md](references/journey-coverage.md).
- Deep Braze QA, or one named person's missing Braze message: braze-implementation-qa.
- Customer.io or Klaviyo implementation QA: customer-io-implementation-qa, klaviyo-implementation-qa. Fallback: [platform-customerio.md](references/platform-customerio.md), [platform-klaviyo.md](references/platform-klaviyo.md).
- A HubSpot portal, CRM configuration, pipeline or lifecycle stage audit: crm-audit. Here HubSpot is only a sender: marketing email, sending workflows, subscription types.
- Designing a preference centre, cap service or suppression architecture: preferences-caps-and-suppression. Fallback: record the gap and a change request.
- Designing a holdout or lift test: incrementality-test-design. Fallback: [measurement-and-holdouts.md](references/measurement-and-holdouts.md).
- Outbound sequencers and their gates: gtm-workflow-design. Cold domains and warm-up: email-sender-infrastructure. A sequencer reaching these people is still a sender here (FC04, FC08).
- Website consent banners and consent mode: consent-debugging or consent-mode-v2-setup.
- Choosing or migrating a platform: lifecycle-platform-selection, braze-to-customer-io-migration-kit. Fallback: this audit of the current platform is the baseline.
- CRM duplicates and merges: crm-data-hygiene-audit, identity-merge-qa. Fallback: TD07 and PC09 cover only the consent side.
- Sync design: reverse-etl-and-audiences. Fallback: TD05 and PC01.
- Connecting MCP servers: marketing-mcp-setup. Fallback, and Iterable: each platform file's access section.
- Copy review is out of scope, except promotions in transactional messages (PC07) and copy that assumes an unchecked state (JC05).

## Inputs

Read first, if they exist in `martech-context/` ([example-acme-stack.md](assets/example-acme-stack.md) shows the fields):

- `stack.md`: senders, domains, DKIM selectors, syncs, owners.
- `tracking-plan/`: trigger and exit events.
- `data-dictionary.md`: preference fields, writers, update schedules.
- `metrics.md`: outcomes and windows.
- `decisions.md`: cap policy, quiet hours, consent model per region, sunset and holdout decisions.

Then gather:

- Senders in scope with owners, the business model (saas, ecommerce, app, leadgen), regions, channels and depth.
- The live journey and recurring campaign inventory with entries and sends for the window, and source-of-truth counts per trigger with their query.
- Dashboard reads of what APIs do not return: entry, exit and send settings, caps, quiet hours, holdouts.
- A 30-day send log for all senders and a 90-day preference log keyed by internal IDs or salted hashes, plus the holdout list with membership dates.
- Each sender's suppression list (hard bounces, complaints, unsubscribes, with timestamps) keyed like the send log (FC08).
- 30 days of bounce and deferral counts per stream by provider and SMTP reply, no recipient addresses (DL16).
- 30 days of DMARC aggregate data per primary domain (source IPs and DKIM d= per source) from the rua mailbox or reporting tool.
- The raw source of one recent message per stream, saved from a seed inbox the team controls.
- Postmaster compliance and spam rate per primary domain; Yahoo and Microsoft feedback counts if enrolled.
- Every impact claim with its method; who approves and runs seed tests, and with which controlled mailboxes.

Read-only access per platform:

- Braze: remote MCP as a user without edit rights, Operator Connect excluded ([platform-braze.md](references/platform-braze.md)).
- Customer.io: MCP with the default `read` scope only, or the `cio` CLI signed in with a read-only token ([platform-customerio.md](references/platform-customerio.md)).
- Klaviyo: custom connector with `read-only=true&disable-tools-with-user-generated-content=true` ([platform-klaviyo.md](references/platform-klaviyo.md)).
- HubSpot: remote MCP allowing only the listed read tools ([platform-hubspot.md](references/platform-hubspot.md)).
- Iterable: MCP (beta) with writes, sends and PII off ([platform-iterable.md](references/platform-iterable.md)).
- Other senders: read window type, retry, exit and default subscription states from live docs.

Keep exports and outputs in one folder of the user's project, never the skill folder; commands below shorten each script's path to `scripts/`.

Never ask for, print or store keys, passwords, tokens or contact details. message_pressure.py refuses keys that look like contact details; hash SMS keys before export. Do not open customer profiles; person-level checks use seeds. If the account read is not the one named, stop.

## Procedure

### 0. Scope and depth

- Scan: reads, exports and scripts, each finding labelled confirmed by data, configuration-only or unconfirmed.
- Full: Scan plus seed tests, only after the plan is approved. Focused: chosen areas. Default: Scan.
- Windows: 30 days for activity and sends; 90 days for imports and preference changes; holdouts since the last assignment change.
- Any area left out is NOT CHECKED with the reason; a silent skip fails acceptance.

### 1. Baseline and sender inventory

- Per sender: platform, workspace, region, channels, From and Return-Path domains, DKIM d=, owner, access mode and scopes, read time.
- A sender found in DMARC reports, Postmaster or seed headers but missing from `stack.md` is DL01 WARN and joins the inventory.

### 2. Journey coverage

Start from the header row of [journey-inventory-template.csv](assets/journey-inventory-template.csv), with expected counts from the source of truth minus entry-level exclusions, then:

```bash
python3 scripts/inventory_audit.py inventory.csv --model saas --global-holdout yes \
  --policy-caps-all-optional yes --as-of 2026-10-14
```

- Set `--model` from the business model, `--global-holdout` and `--policy-caps-all-optional` from decisions.md (both default to no), and `--as-of` to the last day of the window.
- JC02 ratio, rated when expected is 100 or more: 0.90 to 1.10 PASS; 0.75 to 0.90 or 1.10 to 1.25 WARN; below 0.75 FAIL (leakage); above 1.25 FAIL (duplicates).
- JC03: live with 0 entries in 30 days is FAIL when the source shows occurrences, else WARN.
- JC09: live with entries, but 0 sends or a message step that sends nothing or sits in a non-sending status, is FAIL.
- JC04: 2 or more messages or any delay with no exit on the goal is FAIL.
- JC05: a delay over 1 hour with no send-time recheck is WARN; FAIL when the copy states the unfinished state or offers a discount.
- JC01: a required type with no live journey or external owner is WARN, P1 when revenue- or deliverability-critical, else P2.
- JC07: two live journeys of one type is WARN; FAIL when both reach the same people within 24 hours.
- Models, default stages, columns and method: [journey-coverage.md](references/journey-coverage.md).

### 3. Triggers, event data and segments

Trace each journey with a FAIL or WARN: producer, path, occurrence ID, 90-day imports, freshness of filter and recheck fields ([triggers-data-and-segments.md](references/triggers-data-and-segments.md)).

- TD01: client-side triggers on commercial or account facts are WARN; FAIL when failed attempts enter or JC02 is above 1.10.
- TD02: same profile and occurrence within 60 seconds is FAIL.
- TD03: trigger events carry the occurrence ID the platform dedupes on; Braze does not dedupe, so its producer must.
- TD04: imports need a documented gate; an entry spike on an import date for profiles older than 30 days is FAIL.
- TD05: a field updating less often than the shortest delay reading it is FAIL.
- TD06: nulls above 5% WARN, 20% FAIL.
- TD07: duplicates above 1% WARN; mixed-consent clusters FAIL.
- SG02: engagement on opens alone is WARN.
- SG03: over 20% of marketing sends to people with no click, reply, session or purchase in 180 days is WARN.

### 4. Frequency caps and suppression

```bash
python3 scripts/message_pressure.py sends.csv --cap-24h 2 --cap-7d 5 --quiet 21:00-08:00 \
  --prefs prefs.csv --holdout holdout.csv
```

- Pass `--cap-24h`, `--cap-7d` and `--quiet` only from the written policy. With no policy, omit them: FC05 runs as a WARN screen and FC07 is NOT CHECKED. Add `--window calendar` when the policy or a Braze cap counts calendar days (FC03).
- FC02: every optional journey is covered by its sender's cap (Customer.io limits cover nothing until assigned).
- FC05: anyone over a documented cap is FAIL; with no policy, over 2 per rolling 24 hours or 5 per 7 days is WARN.
- FC04: over the policy while each sender is within its own cap is FAIL.
- FC06: Customer.io auto-retry on a state-sensitive message is FAIL (retries continue after exit).
- FC07: quiet-window sends WARN, SMS FAIL.
- FC08: a send after another sender suppressed the address is FAIL.
- Semantics, send log and timestamp formats: [caps-and-suppression.md](references/caps-and-suppression.md).

### 5. Preferences and consent

Map every control to its promised scope, each sender's field, the authoritative writer and a seed test ([consent-and-preferences.md](references/consent-and-preferences.md)).

- PC02, P0: an optional send in the scope more than 48 hours after a restrictive change, or at any time after a stale write reversed it, is FAIL. Sends between 1 and 48 hours are PC03 evidence; sends within 1 hour are in flight.
- PC01: a sync reversing an "off" with no customer action is FAIL.
- PC03, unsubscribe to suppression in every sender: 1 hour or less PASS; up to 48 hours WARN; longer FAIL.
- PC04: a promised scope a sender does not enforce is FAIL.
- PC05: default states are not consent; FAIL where decisions.md requires explicit opt-in.
- PC07: promotions classed transactional are FAIL.
- PC08: any text after STOP beyond one confirmation is FAIL, at any delay.

### 6. Deliverability and authentication

Dig SPF, DMARC and the DKIM selector from a received message, then:

```bash
python3 scripts/check_message_headers.py newsletter.txt lifecycle.txt --class marketing
```

- DL02: Gmail bulk is about 5,000 a day to personal Gmail on the primary domain, permanently. Microsoft's rule starts at 5,000 a day to its consumer mailboxes from one From domain. Below them, pass `--volume low` or `--microsoft-volume low`.
- DL05: bulk streams need dmarc=pass on received mail.
- DL06: SPF and DKIM must both pass for Microsoft bulk.
- DL04: keys under 1,024 bits FAIL.
- DL07: one HTTPS List-Unsubscribe, `List-Unsubscribe-Post: List-Unsubscribe=One-Click`, both in DKIM h=, and a cookie-free POST that does not redirect; any header gap on a bulk stream is FAIL. A body with no unsubscribe or preferences link is FAIL; a preferences link only is WARN.
- DL16: sender-requirement rejections or deferrals from Gmail, Microsoft or Yahoo in 30 days of bounces on a bulk stream are FAIL; so is a Spamhaus listing of a brand domain or dedicated IP.
- DL09: Postmaster Needs work is FAIL.
- DL10: spam rate under 0.10% PASS, under 0.30% WARN, else FAIL; no Postmaster data is NOT CHECKED.
- DL11: hard bounces above 1% WARN, 2% FAIL.
- Providers, codes, DNS and denominators: [deliverability.md](references/deliverability.md).

### 7. Measurement and holdouts

List each impact claim with its method; run `holdout_math.py plan` and `result` ([measurement-and-holdouts.md](references/measurement-and-holdouts.md)).

- MH01: attributed revenue shown as impact without a control is WARN; so is credit summed across senders without deduplicating by order or conversion ID.
- MH03: any optional send to a holdout member while assigned is FAIL.
- MH04: withheld required messages are FAIL.
- MH05: SRM p below 0.0005 is FAIL.
- MH08: openers against non-openers is FAIL.
- MH09: an MDE above the decision threshold is WARN.

### 8. Confirm with seed tests (Full depth)

- Plan from [seed-test-plan-template.yaml](assets/seed-test-plan-template.yaml), get explicit approval, and use a test workspace or controlled identities only.
- A person the user names runs each test and the agent records the evidence; a test the agent runs goes through agent-write-safety with that approval.
- Every FAIL and P0 or P1 WARN is confirmed by a seed test or data, or carries the reason it could not be ([seed-tests.md](references/seed-tests.md)).

### 9. Gap list, change requests and report

- When a P0 condition is confirmed (PC02, PC08, PC09, DL05 to DL07 or DL16 on a bulk stream, spam rate 0.30% or more, wrong-recipient content), tell the user at once: the evidence and a drafted pause of that journey or sync field for the owner to approve. Then continue.
- P0 contain now: those conditions. P1 (about 30 days): other FAILs, spam WARN, missing revenue-critical journeys. P2 (about 90 days): WARNs needing decisions. P3: hygiene.
- Score within a priority: reach per month x severity (FAIL 3, WARN 1) x confidence (1.0, 0.6, 0.3) / effort (S 1, M 2, L 4). Owners may reorder with a written reason.
- Start from the header row of [gap-list-template.csv](assets/gap-list-template.csv). Write a change request per P0 and P1 item from [change-request-template.md](assets/change-request-template.md).
- Write the report from [audit-report-template.md](assets/audit-report-template.md). Rules and two worked requests: [gap-list-and-change-requests.md](references/gap-list-and-change-requests.md).
- Before hand-over, run `python3 scripts/gap_list_check.py gaps.csv` and fix every ERROR.
- Propose martech-context updates; write nothing until the user approves.

## Common failure modes

- Two senders, no shared budget. Detect: the FC04 cross-sender line.
- A nightly sync resubscribes people. Detect: PC01 stale write, PC02; seed S11.
- Unsubscribe lands on a duplicate profile. Detect: TD07 mixed-state clusters, PC09; seed S06.
- A live journey enters people and sends nothing: a step left in draft or manual. Detect: JC09.
- Stale copy after a block (Customer.io retries after exit; Braze advances capped users). Detect: sends after the goal (FC06).
- Trigger only the web app emits, or a renamed event empties a journey. Detect: JC02 below 0.75 with TD01; JC03.
- Payment reminders continue after payment. Detect: JC04; JC07 against billing dunning.
- Newsletter signed with the provider's domain. Detect: DL05 with spf and dkim passing for another domain; Gmail 4.7.32 deferrals (DL16).
- Blended complaint rate hides Gmail. Detect: Postmaster 0.10% or more with a low ESP rate (DL10).
- Holdout contaminated by a second sender, or split broken after assignment. Detect: MH03, MH05.

## Worked example

[worked-example.md](references/worked-example.md): the audit run on fictional Acme Reports, with expected output, regression fixtures and the finished gap list.

## Acceptance checks

- Baseline: every sender from `stack.md`, headers, DMARC data and Postmaster is listed with access mode, scopes, window and read time. Pass: no sender found later is missing.
- Coverage: inventory rows match each platform's live list; the model is named and N/A types justified; JC01 to JC09 reported with the stage view. Pass: every journey with 100 or more expected entries has a ratio or a reason.
- Every rule: TD, SG, FC, PC, DL and MH rules each appear as PASS, WARN, FAIL or NOT CHECKED with a reason. Pass: none silently skipped.
- Evidence: every FAIL cites its evidence; in Full depth every FAIL and P0 or P1 WARN is confirmed by a seed test or data, or says why not. Pass: no confirmed finding without evidence.
- Scripts: each ran on the exports, or the report says "script not run" and how the rule was applied. Pass: appendix outputs match the findings and no input held contact details.
- Gap list: every FAIL and WARN appears once with priority, owner, effort, confidence, score and evidence; P0 and P1 items have change requests with test, rollback and what rollback cannot undo. Pass: gap_list_check.py reports 0 ERROR and a colleague could act without asking.
- Safety: the tool log shows reads only, with MCP modes recorded; no credential or contact detail in chat or files. Pass: zero agent write calls outside approved seed tests run through agent-write-safety; every seed test names its approval and who ran it.
- Context: proposed martech-context updates are listed. Pass: none written without approval.

## Boundaries

- Read-only. Never create, edit, launch, pause, trigger, send, subscribe, unsubscribe, suppress, merge, delete, import or sync anything, even when the connected identity could. Fixes are change requests; P0 containment is a drafted pause of the journey or sync field, never a deletion.
- Seed tests that create profiles, send messages or reply STOP need explicit approval and a test workspace or controlled identities. A person the user names runs them, or the agent through agent-write-safety.
- The consent model per region belongs to the privacy owner; the audit compares configuration with the written decision.
- DNS and DMARC changes are drafts for the domain owner. Never propose quarantine or reject before every legitimate sender is aligned.
- Hand-offs, read-only: the vendor MCP servers and Customer.io CLI in the modes above, Customer.io's workspace-health command, Gmail Postmaster Tools, Yahoo Sender Hub, Microsoft SNDS and JMRP, and a read-only warehouse connection.
- Writes go to agent-write-safety; fixes to the owners in the gap list.

## Live docs

Platform settings, limits and menu paths change. Before stating any parameter name, limit, quota or UI path:

- Fetch the current page from the vendor docs MCP server or llms.txt listed in `sources.yaml`, or open the canonical URL.
- If the live page disagrees with this skill, follow the live page and tell the user what changed.
- Cite the URL you used.

Most important here: each platform's caps, subscription, events and MCP pages; the Gmail, Yahoo and Microsoft sender pages and Gmail's SMTP error reference; RFC 8058 and RFC 9989. Sender rules, Postmaster and MCP pages: rechecked 2026-11-06. Others: 2026-11-01 or 2026-11-02 unless marked "verify live"; verify every Iterable setting live.
