---
name: tracking-plan-builder
description: Builds a tracking plan for one customer journey as implementable event contracts. Each contract states the business question, trigger and must-not-fire conditions, source of truth, occurrence IDs and per-destination dedupe keys, typed properties with allowed values, PII exclusions, consent routing, destination mappings for GA4, Mixpanel, Amplitude, Segment, RudderStack and HubSpot checked against live limits, acceptance tests, versioning and change control. Use when starting instrumentation, asked what events to track, writing a tracking spec, event spec, analytics spec, instrumentation plan, event schema or data layer spec, defining an event taxonomy, asked for a tracking plan template or tracking conventions, mapping one plan to several analytics tools, adapting a plan for a mobile app, preparing Segment Protocols, RudderStack tracking plans, Avo, Mixpanel Lexicon or Amplitude Data, or retrofitting a plan onto events already being collected.
license: CC-BY-4.0
metadata:
  tier: free
  category: tracking-plans-and-naming
  version: "0.1.0"
  last_reviewed: "2026-10-17"
  status: reviewed
---

# Tracking plan builder

Produces one journey's tracking plan as event contracts that a developer can implement, an analyst can count and a reviewer can test. The output is a set of proposed files for `martech-context/tracking-plan/`, written only after the user approves them. The plan says what should happen; acceptance tests and monitoring prove what does happen.

## When to use

Use this skill when:

- A journey is about to be instrumented (signup to first value, checkout, lead to opportunity) and developers need to know exactly what fires, from where, with which fields.
- One plan has to feed several tools and the names, IDs and limits differ between them.
- A team asks for a tracking plan template, a tracking spec or tracking conventions.
- Events already exist and nobody can say what they mean (retrofit).
- A mobile app is joining a plan that was written for web.

Use something else when:

- The journey itself is not chosen yet. Agree the customer outcome, counting unit and window first; step 1 has the minimum, and stops if it cannot be written.
- Only naming rules are needed: use the event-naming-convention skill.
- The question is campaign values: use the utm-convention-and-registry skill.
- The work is writing SDK code for one vendor from an approved plan: hand off to that vendor's skill (see Boundaries).
- The question is what a metric means: that is a metric contract in `martech-context/metrics.md`, which this skill links to but does not define.

## Inputs

Read these first if they exist, before asking anything:

- `martech-context/stack.md`: emitters (web, apps, backend, billing), the CDP, destinations, environments, owners.
- `martech-context/naming-rules.md`, `metrics.md`, `data-dictionary.md`: naming rules, the metric this journey serves, shared property definitions.
- `martech-context/tracking-plan/`: existing journeys and contracts. Reuse a contract that already exists; never write a second definition of the same event.
- `martech-context/decisions.md`: identity model, consent model, the plan's `message_id` namespace, earlier naming decisions.

Then gather what is missing:

- The decision the plan supports, in one sentence, and who acts on it.
- The systems that can observe each step: browser, app, backend service, billing provider, CRM.
- The identity model: where `user_id` comes from, the account or workspace model, how anonymous IDs reach the backend.
- Consent categories, the regions they apply to, and the GA4 consent mode (basic or advanced).
- Owners: business, implementation, data, privacy.
- For retrofit: the observed event list and sample payloads from test accounts. For mobile: platforms, release cadence, minimum supported app version.

Never ask for, print or store credentials, tokens, API secrets or customer records. Sample payloads come from test accounts or are redacted first.

## Procedure

Draft everything in the conversation or in a scratch file outside `martech-context/`. Nothing under `martech-context/` or `.amplitude/` is created or changed until step 9.

### 1. Scope one journey

Draft the journey from [assets/journey-plan-template.md](assets/journey-plan-template.md): decision, primary actor, population and exclusions, counting unit, entry, outcome, window, repeat policy and complete-data cutoff.

- If the decision cannot be written as one sentence naming an action someone will take, stop and draft that sentence with the user. An event list without a decision becomes permanent noise.
- One counting unit per funnel. If steps count different units (people at signup, workspaces at activation), split the funnel or write the roll-up rule.
- If the metric exists in `metrics.md`, link its ID. The "first per workspace within 168 hours" rule lives in the metric, not in the event.
- Exclusions (employees, test accounts, invited users) are fixed by a flag at creation, so reclassification cannot change old cohorts.

### 2. Inventory what already exists (retrofit and existing stacks)

- Pull the observed event and property list with read tools only: Mixpanel MCP read tools; Amplitude MCP on an account whose role grants only Use MCP (read); RudderStack MCP read tools (its transformation write tools change a live pipeline, so never call them while planning); Google's read-only GA4 MCP; or exports from the Segment app.
- Classify each event: keep, rename with a mapping, merge into a supported definition, or retire after a dependency check. Record from which date and app version each event's meaning is usable. Details: [references/change-control.md](references/change-control.md#retrofitting-a-plan-onto-existing-events).

### 3. Choose the events

- Every event needs a consumer: a metric, an investigation, an experiment or a workflow. No consumer means a backlog entry with the question that would justify it. An experiment consumer also needs an exposure event and the randomisation unit on each outcome event.
- When attempt, failure and success answer different questions, make separate events joined by an attempt or job ID (`*_started`, `*_failed`, success). The failure event carries a controlled `error_category`, never raw error text.
- Variants are properties, not events: `report_exported` with `export_format`, not one event per format.
- Name per `naming-rules.md`. Without rules, default to lowercase snake_case object then past-tense action (`data_source_connected`), at most 40 characters so GA4 can take it unchanged.
- Autocapture helps exploration but sees clicks, not committed facts, so it does not replace contracts.

### 4. Write one contract per event

Draft each contract from [assets/event-contract-template.yaml](assets/event-contract-template.yaml). Field rules: [references/contract-fields.md](references/contract-fields.md). The decisions that matter most:

- Source of truth is the system that commits the fact. Persisted state comes from the backend; payments and refunds from the verified billing webhook, keyed on the business fact, not the delivery ([references/ecommerce-and-billing.md](references/ecommerce-and-billing.md)); impressions from the client. When GA4 needs the browser session for a server fact, the browser sends a companion under its own name carrying the backend `event_id` as a property, and the contract states the expected ratio.
- Trigger is a state change. `must_not_fire` lists at least the intent step, the failure path, refreshes and re-renders, and delivery retries, in order; item n gets negative case T2.n.
- Five identifiers stay distinct: `event_id` (a plain UUID for the occurrence, created once at the source, reused on every retry; business keys are hashed into a UUIDv5), `anonymous_id`, `user_id` (stable internal ID, never an email), `account_id` and `workspace_id` read on the server at event time (never from client-supplied analytics IDs), and the business object ID.
- Every property has a type, requirement, allowed values or format, unit, `unknown_value_policy` and `null_means`. Money has a unit, a separate ISO 4217 currency and a business definition. A property used by two contracts is defined once in `data-dictionary.md`.
- `pii_exclusions` names what must never leave the source: emails, names, phone numbers, free text, raw error bodies, tokens, connection strings, URLs with query strings.
- Consent names every destination's behaviour when denied, the GA4 consent mode, where server routes enforce the stored consent state (they bypass the browser consent platform), and withdrawal propagation.
- Record `occurred_at` (source clock, UTC), `received_at` (ingestion), a late-arrival policy, owners and risk tier.

### 5. Map each destination

Fetch the live limits first, then fill the `destinations` block. Rules and limits checked on 2026-10-17: [references/destination-mapping.md](references/destination-mapping.md).

- Route an event to a destination only for a named use. HubSpot normally gets derived properties from a sync, upserted on a unique-value property holding `account_id`, never matched on domain, with a roll-up rule across workspaces.
- Record `via` (direct SDK, Segment, RudderStack, Measurement Protocol, GTM, warehouse sync) and the final name each destination receives, then run the collision check per destination and final name across every path ([scripts/check_plan.py](scripts/check_plan.py) over the sheet tabs).
- Map to a standard event only when the meaning matches: GA4 `sign_up` for a completed registration, `purchase` only with a real `transaction_id`, `items`, `value` and `currency`.
- Never put `event_id` in a transport dedupe field. Segment (24 hours, any source), RudderStack (7 days) and Amplitude (7 days per `device_id`) do not scope those keys by event name, so a companion or renamed event sharing one vanishes. Derive `message_id = UUIDv5(plan namespace, event_id + ":" + emitted name + ":" + emitter)`, reused on that emission's retries, and set `messageId`, `$insert_id` (36 bytes maximum), `insert_id` and HubSpot `uuid` from it. Record the configured field as `dedupe_mapping`.
- GA4 has no general event dedupe (purchases dedupe on `transaction_id`): guard companions at the source. Count parameters per event (25 maximum), keep IDs out of custom dimensions, and mark key events in Admin before release.

### 6. Write the acceptance tests

For each contract: normal success, one negative case per `must_not_fire` item, normal failure, delivery retry, repeated genuine action, prohibited field, and, with a companion or rename overlap, both sent through the same CDP and both expected to arrive. Add identity, consent, boundary, CRM matching and billing cases where they apply. Each case states the result at emission, collection, destination, warehouse and use: [references/acceptance-tests.md](references/acceptance-tests.md).

### 7. Set versioning and change control

- Contract `version` is an integer, bumped on breaking changes only (trigger, grain, required property, type, identifier, removed value). A compatible change that alters validation updates the enforcement plan before release, with no bump.
- Risk tier decides approvers. Tier 1 (revenue, paid or ad conversions, CRM workflows, identity, consent) needs business, data and implementation owners plus staging evidence; tier 2 data and implementation owners; tier 3 the data owner.
- A rename ships with a consumer inventory, a release boundary, the new name added to enforcement tools first, a separate `message_id` per name, and no metric counting both names. A semantic change gets a new name or metric version. Procedure: [references/change-control.md](references/change-control.md).

### 8. Adapt for a mobile app

When an app emits any event in the plan, add `introduced_in` per platform and send `app_version` on every event; never reuse a name for a new meaning. Generate `event_id`, `message_id` and `occurred_at` when the action happens and persist them in the offline queue. If a queue can outlive a destination's late-event cut-off, the canonical record comes from the backend or warehouse. Give every workflow a maximum lateness. Details: [references/mobile-and-offline.md](references/mobile-and-offline.md).

### 9. Propose the files and hand off

List the proposed files with their content and wait for the user's approval of that list before writing any of them:

- the journey file and one `<event_name>.yaml` per contract in `martech-context/tracking-plan/`
- a `martech-context/tracking-plan/CHANGELOG.md` entry, and additions to `data-dictionary.md` and `decisions.md` (including the `message_id` namespace)
- optionally `.amplitude/instrumentation-agent-context.md` from [assets/amplitude-instrumentation-agent-context.md](assets/amplitude-instrumentation-agent-context.md) when Amplitude is in the stack

Teams that work in a sheet get the same content as two tabs: [assets/plan-sheet-events.csv](assets/plan-sheet-events.csv) and [assets/plan-sheet-properties.csv](assets/plan-sheet-properties.csv). Pick one source of truth for the plan, then prepare enforcement drafts per [references/enforcement-handoffs.md](references/enforcement-handoffs.md).

## Common failure modes

- Trigger on the click, not the commit. Detect: count exceeds the matching database rows for the day; failed attempts appear as successes.
- Two producers, one name (browser and server, or a CDP route plus a direct tag). Detect: count near double the source of truth; split the count by `source` or library.
- Companion dropped as a duplicate. Detect: the browser companion or the renamed event is sent but missing in a destination; two messages share one `messageId` or `insert_id`.
- New ID per retry. Detect: pairs with the same object ID and `occurred_at` within seconds but different `event_id`.
- Account reconstructed from current membership. Detect: historical events move between accounts after a user changes team.
- Silent GA4 drops over limits. Detect: parameters past the 25th, or values past 100 characters, missing in DebugView or the export; a key event over 40 characters not reported as a key event.
- Connector name collisions. Detect: one GA4 name with two producers after Segment's space and lowercase transformations.
- Short user IDs in Amplitude. Detect: IDs under 5 characters show as anonymous or are rejected.
- CRM properties on the wrong company. Detect: two accounts on one HubSpot company, or a new company per sync run.
- Enum drift. Detect: values outside `allowed_values`, often a new plan or provider mapped silently to an old one.
- Unit mix-ups. Detect: revenue totals off by a factor of 100 against the ledger.
- PII in free text. Detect: emails or names in error messages, search terms or `page_location` query strings.
- Late mobile events dropped. Detect: event volume by `app_version` against backend facts after offline use.

## Worked example

Acme Reports, a fictional B2B product, plans "trial signup to first report" for workspaces. Journey: [assets/example-acme-journey.md](assets/example-acme-journey.md). Three full contracts (`user_registered`, `data_source_connected`, `report_generated`): [assets/example-acme-contracts.yaml](assets/example-acme-contracts.yaml). Points worth copying:

- `user_registered` is canonical on the backend; GA4 gets `sign_up` from a dataLayer push once per `event_id`, with its own transport identity, as the only key event.
- `data_source_connection_started` (browser intent) and `data_source_connected` (backend fact) share an `attempt_id`, so drop-off is measurable.
- `report_generated` carries `input_data_kind`, so demo-data reports do not count; boundary cases sit at 167:59 and exactly 168 hours.
- HubSpot gets derived company properties upserted on `acme_account_id`, rolled up across workspaces.

## Acceptance checks

- Journey file: decision, unit, outcome, window and exclusions are filled and the metric ID resolves in `metrics.md`. Pass: no placeholder text remains.
- Coverage: every event in the journey file has a contract and every contract lists the journey. Pass: no orphans either way.
- Contracts: each has a consumer, a state-change trigger, `must_not_fire` items including delivery retry, a source of truth with a reason, all five identifier kinds (or not applicable with a reason) plus the `message_id` derivation, properties with `unknown_value_policy` and `null_means`, PII exclusions, a consent block naming every destination, owners and a risk tier. Pass: no field left as template text.
- Destinations: each records `via`, the final name, and a `dedupe_mapping` set from `message_id`; limits were checked live with URL and date; the collision check ran per destination and final name across every path. Pass: zero collisions, zero names over the strictest limit, no two emissions sharing a transport key.
- PII: search property keys, destination parameter maps and identifier fields (not descriptions, acceptance cases or exclusion lists) for email, name, phone, token, URL and free-text fields. Pass: every match holds a controlled value (an enum or internal ID); none carries a personal value.
- Tests: negative cases T2.1 to T2.n match the `must_not_fire` list one to one; retry, repeated-action and prohibited-field cases exist; a companion case exists wherever `other_emitters` or `dual_emission` is set; each case states evidence per layer.
- Change control: approvers for the tier are named and the changelog has a dated entry. Pass: status is not `approved` without approvers.
- Mobile, when relevant: `introduced_in`, `app_version` and the late-arrival policy are set on every mobile contract.
- Approval: the user approved the listed files before they were written. Pass: nothing under `martech-context/` or `.amplitude/` changed without a recorded approval.

## Boundaries

- This skill writes a plan. It does not write SDK code, publish tags, change tracking plans in vendor tools or alter CRM properties. Nothing under `martech-context/` or `.amplitude/` is created or changed until the user approves the listed files. Any change to a live system is a draft (file, CSV, diff or change list) that stops for human approval.
- Consent and legal basis decisions belong to the privacy owner. The plan records the decision; it does not make it. Metric definitions belong to the metric owner in `metrics.md`.
- Hand-offs, with read-only modes where they exist ([references/enforcement-handoffs.md](references/enforcement-handoffs.md)):
  - Segment: Twilio's MCP server only searches docs. Protocols plans go through the Segment app or the Public API, applied by a workspace owner.
  - RudderStack: the MCP server reads plans and violations; its only write tools create or update transformations and connect them to destinations, never called without approval. Rudder CLI YAML plans are applied after a reviewed diff.
  - Mixpanel: MCP read tools only while planning. `manage-lexicon` applies descriptions after approval; `tracking-implementation` implements from the spec, with the project token given by the user directly to that skill.
  - Amplitude: connect with a role granting only Use MCP (read), such as Viewer. The agent drafts the taxonomy change list; a branch is created only after approval, by a user with Use MCP (write) and Update Tracking Plan, or through Amplitude's taxonomy skill.
  - GA4: Google's GA4 MCP server is read-only. Admin and GTM container changes are made by a human.
  - HubSpot: event definitions, the unique-value match property and CRM properties are created by a developer or admin after approval; internal names are permanent.

## 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 used here: GA4 limits, key events and consent mode; Mixpanel deduplication and Import Events; Amplitude HTTP V2 and MCP; Segment duplicate data, Protocols and Amplitude (Actions); RudderStack deduplication and MCP; HubSpot send event data and record deduplication; Stripe webhooks. Every limit here was checked on 2026-10-17.
