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'smessage_idnamespace, 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_idcomes 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: 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.
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 controllederror_category, never raw error text. - Variants are properties, not events:
report_exportedwithexport_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. Field rules: 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); 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_idas a property, and the contract states the expected ratio. - Trigger is a state change.
must_not_firelists 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_idandworkspace_idread 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_policyandnull_means. Money has a unit, a separate ISO 4217 currency and a business definition. A property used by two contracts is defined once indata-dictionary.md. pii_exclusionsnames 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.
- 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 over the sheet tabs). - Map to a standard event only when the meaning matches: GA4
sign_upfor a completed registration,purchaseonly with a realtransaction_id,items,valueandcurrency. - Never put
event_idin a transport dedupe field. Segment (24 hours, any source), RudderStack (7 days) and Amplitude (7 days perdevice_id) do not scope those keys by event name, so a companion or renamed event sharing one vanishes. Derivemessage_id = UUIDv5(plan namespace, event_id + ":" + emitted name + ":" + emitter), reused on that emission's retries, and setmessageId,$insert_id(36 bytes maximum),insert_idand HubSpotuuidfrom it. Record the configured field asdedupe_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.
7. Set versioning and change control
- Contract
versionis 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_idper name, and no metric counting both names. A semantic change gets a new name or metric version. Procedure: 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.
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>.yamlper contract inmartech-context/tracking-plan/ - a
martech-context/tracking-plan/CHANGELOG.mdentry, and additions todata-dictionary.mdanddecisions.md(including themessage_idnamespace) - optionally
.amplitude/instrumentation-agent-context.mdfrom 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 and assets/plan-sheet-properties.csv. Pick one source of truth for the plan, then prepare enforcement drafts per 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
sourceor 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
messageIdorinsert_id. - New ID per retry. Detect: pairs with the same object ID and
occurred_atwithin seconds but differentevent_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_locationquery strings. - Late mobile events dropped. Detect: event volume by
app_versionagainst 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. Three full contracts (user_registered, data_source_connected, report_generated): assets/example-acme-contracts.yaml. Points worth copying:
user_registeredis canonical on the backend; GA4 getssign_upfrom a dataLayer push once perevent_id, with its own transport identity, as the only key event.data_source_connection_started(browser intent) anddata_source_connected(backend fact) share anattempt_id, so drop-off is measurable.report_generatedcarriesinput_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_fireitems including delivery retry, a source of truth with a reason, all five identifier kinds (or not applicable with a reason) plus themessage_idderivation, properties withunknown_value_policyandnull_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 adedupe_mappingset frommessage_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_firelist one to one; retry, repeated-action and prohibited-field cases exist; a companion case exists whereverother_emittersordual_emissionis 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
approvedwithout approvers. - Mobile, when relevant:
introduced_in,app_versionand 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):
- 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-lexiconapplies descriptions after approval;tracking-implementationimplements 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.