---
name: utm-convention-and-registry
description: Designs a UTM plan, a campaign tagging convention and registry so every tagged link lands in the GA4 default channel predicted before launch and its spend joins to sessions. Covers lowercase format rules, a source and medium vocabulary checked against GA4's live channel rules, utm_campaign and utm_id policy, a platform campaign map for cost joins, recipes for Google Ads, Microsoft Advertising, Meta, LinkedIn, TikTok, email, affiliates, AI assistant links and QR codes, a link builder and launch tests through to the CRM. Use when someone asks to create a UTM plan or UTM tracking plan, a UTM template, spreadsheet, builder or naming convention, UTM best practices, campaign tagging rules or tracking links for a campaign, what utm_id is, how to tag a newsletter or QR code, UTM parameters for Facebook, LinkedIn, Microsoft or Google Ads, sees paid traffic in Organic Social, Paid Other or Unassigned, or prepares GA4 cost data import or a warehouse cost join.
license: CC-BY-4.0
metadata:
  tier: free
  category: tracking-plans-and-naming
  version: "0.3.0"
  last_reviewed: "2026-10-17"
  status: reviewed
---

# UTM convention and registry

Produces format rules and a controlled vocabulary for campaign parameters, a registry with one row per distributed link plus a map of real platform campaign IDs, and links tested from the real click to the GA4 channel, the CRM record and the cost row. A consistent pair can consistently land in the wrong channel. The convention passes when every link lands in the channel predicted before launch and its spend joins to its sessions.

## When to use

Use this skill when:

- Someone asks for a UTM plan, template, spreadsheet, naming convention or link builder.
- Paid traffic shows as Organic Social, Paid Other, Referral or Unassigned and the collected source/medium pairs are the suspect.
- A paid platform's cost has to join to sessions, in GA4 campaign data import or in a warehouse.
- A new channel needs rules: an ad platform, a newsletter, an affiliate program, links inside an AI assistant, QR codes on print.

Use something else when:

- Unassigned or `(not set)` appears and the tags may be fine (redirects, consent, tag order, server events): ga4-unassigned-not-set-diagnosis.
- A launch test loses the campaign when consent changes: consent-debugging.
- In-platform campaign, ad group and ad names: campaign-naming-convention. The contract between the two skills is in step 2.
- Internal banners and navigation: events in a tracking plan (tracking-plan-builder), never UTMs.
- Planned, not yet published: utm-link-audit, utm-governance-and-adoption, first-party-click-id-capture, ai-traffic-tracking, marketing-warehouse-design. If one is not installed, do the minimum here (step 1 inventory, an owner line in `naming-rules.md`, the step 7 CRM test, AI referrals left untagged, the step 8 join keys) and tell the user the rest is out of scope.

## Inputs

Read these first if they exist:

- `martech-context/stack.md`: ad platforms, ESP, affiliate network, redirect service, CRM, warehouse, GA4 property.
- `martech-context/naming-rules.md`: current UTM and in-platform naming rules.
- `martech-context/decisions.md`: earlier value choices and cutover dates.
- `martech-context/data-dictionary.md`: cost tables and the CRM fields that store UTMs.
- `martech-context/tracking-plan/`: an existing UTM or campaign registry. Extend it; never start a second one.

Then gather, read-only:

- 90 days of GA4 data by Session source / medium, Session manual source, medium, campaign name, campaign ID and source platform, and Session default channel group, with sessions and key events (Google's GA4 MCP server, `run_report`). On auto-tagged Google Ads clicks only manual ad content and manual term come from UTMs. If an `(other)` row appears or the response flags `dataLossFromOtherRow`, split the request by month or read `collected_traffic_source` in the BigQuery export.
- URL settings at every level on each ad platform, and LinkedIn parameters at account and campaign level. How to read each: [references/channel-rules.md](references/channel-rules.md).
- ESP tagging settings, the affiliate network's link format, how shorteners and QR redirects treat query strings, and how the CRM stores UTMs (first touch, latest touch).
- The cost route per paid platform: GA4 product link, automated connector, file import, warehouse only, or none.

Never ask for credentials, tokens or customer records. Strip personal data from any URL sample before storing it.

## Procedure

### 1. Inventory what is collected

- Group by lowercased manual source and medium. Values that differ only by case are one value split across rows and a broken cost join: GA4 values and import matching are case-sensitive, channel rules are not.
- Review every pair from a paid platform and every pair above 0.5% of sessions or with any key event. List the rest in bulk.
- One line per reviewed pair: observed pair, sessions, key events, current channel, intended channel, action (`keep`, `map`, `retire`), cutover date.
- Flag literal or encoded macros (`{`, `}`, `%7B`, `CAMPAIGN_ID`). Values used by a connector, CRM workflow or dashboard filter change only with a mapping and a cutover date in `decisions.md`.

### 2. Set the format rules

Each parameter does one job. Columns and ID policy: [references/registry-and-cost-joins.md](references/registry-and-cost-joins.md).

- `utm_source`: who sent the visit (platform, publication, partner, placement). One slug per relationship; never a generic `partner`.
- `utm_medium`: how it was distributed, from the list in step 3.
- `utm_campaign`, non-paid: `yyyy_mm_initiative`, plus a qualifier only when it changes a decision, 60 characters at most. Always-on programs keep the month they started (`2024_03_onboarding_flow`).
- `utm_campaign`, paid platforms: the platform's campaign name by its name macro, because Google's import and campaign URL pages ask for names that match the platform. Name platform campaigns with the registry name plus a qualifier (`2026_11_forecasting_launch_prospecting`), or by campaign-naming-convention.
- `utm_id`: paid, the ID of the object cost is reported on, by macro. Everything else, a registry ID (`cmp_2026_311`), one per initiative per publication, partner or placement. Never reused, never changed on rename.
- `utm_content`: the ad ID by macro, or a slot from the pair's list (`hero_button`). `utm_term`: the paid search keyword. `utm_source_platform`: the ESP, network or DSP when it is not the source.
- `utm_creative_format`, `utm_marketing_tactic`: only when a named model or report reads them and a test visit shows the property returns them.
- Lowercase `a-z`, `0-9` and `_`; mediums keep GA4's spelling (`ai-assistant`); macros stay as the platform expects. No emails, names, phone numbers or CRM IDs.

Contract with campaign-naming-convention: it owns in-platform names and, when the team uses it, the paid templates for `utm_campaign`, `utm_id` and `utm_content`. This skill owns source, medium, the vocabulary, non-paid values, the registry and the campaign map, and checks every paid template against steps 4 and 8. Reuse the initiative IDs that skill minted, whatever their prefix, and never mint a `utm_id` equal to one.

### 3. Build the controlled vocabulary

Copy [assets/utm-vocabulary.yaml](assets/utm-vocabulary.yaml) and [assets/allowed-pairs.csv](assets/allowed-pairs.csv) (one row per approved pair with expected channel, content slots and test result). Defaults checked on 2026-10-17:

- `cpc` with a search source (`google`, `bing`): Paid Search. `paid_social` with a social source: Paid Social. `paid_video` with a video source: Paid Video.
- `display`: Display. `email`: Email. `sms`: SMS. `push`: Mobile Push Notifications. `audio`: Audio. `social`: Organic Social. `referral`: Referral. `affiliate`: Affiliates. `ai-assistant`: AI Assistant.
- `offline` (QR, print, events): no default rule, so Unassigned in the default group and Offline in a custom group.

Adding values:

- A new medium needs an intended default channel, or a custom channel rule shipped with it.
- A source with a paid medium must be on Google's social, search, video or shopping list, or it lands in Paid Other. The 2023 source file has `facebook`, `fb`, `instagram`, `ig`, `linkedin`, `lnkd.in`, not `li`, `meta`, `msg`, `an` or `x`; those are predicted Paid Other and confirmed with a test visit.
- A listed source with a non-paid medium that has its own rule (`linkedin / email`, `youtube / referral`, `facebook / affiliate`) satisfies rules for two channels with no documented winner. Use the publication or partner as source.

### 4. Predict each pair's channel before launch

Walk every pair through [references/ga4-channel-rules.md](references/ga4-channel-rules.md#how-to-predict-a-pair):

- A listed source with a paid medium: that list's paid channel. A paid medium with no listed source and no other medium rule: Paid Other.
- Rules pointing to two channels: `test required`, no prediction. Change a value so one channel's rules match. If nothing can change, run one controlled test visit before launch and store the observed channel in `test_result`; the builder refuses the pair until then.

These look sensible and misclassify:

- A social source with `social_paid`, `sponsored`, `boost` or `social` (LinkedIn's own API sample): Organic Social, so paid spend reads as organic.
- Mediums containing `video`, `mobile` or `notification`, or ending in `push`, match Organic Video or Mobile Push; when also paid (`cpm`, `paid_video` with an unlisted source), `test required`.
- `newsletter`, `partner`, `qr`, `print`, `ai`, `ai_assistant` with an unlisted source: Unassigned. `affiliates` is untested and may be too.
- Campaign names with `shop` or `cross-network` can trigger the Shopping or Cross-network rules.

### 5. Apply the channel rules

Recipes and read-only checks: [references/channel-rules.md](references/channel-rules.md). The decisions that go wrong most:

- Google Ads: keep auto-tagging on. Add manual UTMs only when a CRM or form tool needs them and cannot store click IDs and `gad_campaignid`. Then use the full set in the final URL suffix, a `campaign` custom parameter equal to each campaign's name, and record the fallback: clicks whose click ID cannot be used are classified from the UTMs alone, so `google / cpc` is Paid Search for every campaign type. Redirects keep `gclid`, `gbraid`, `wbraid` and `gad_*`.
- Microsoft Advertising: UTM auto-tagging off and the full set in the account final URL suffix with `{Campaign}` and `{CampaignId}`. Never combine UTM auto-tagging with registry UTMs in the suffix.
- Meta: `{{campaign.name}}` and `{{campaign.id}}` on every ad. Name macros keep the first-published name, so name campaigns before publishing and do not rename them on a connector route.
- LinkedIn: parameters at one level only, dynamic name and ID, no UTMs on the creative URL.
- AI assistant links you control: your own app slug as source, never an assistant's name. QR: print a 302 or 307 redirect on your own domain, never the tagged URL.
- Never tag internal links or URLs that get indexed or reshared: later direct sessions inherit the latest campaign values.

### 6. Fill the registry and the campaign map

Copy [assets/utm-registry-template.csv](assets/utm-registry-template.csv): one row per distributed link ([columns](references/registry-and-cost-joins.md#registry-columns)).

- Macro rows store the parameter string as entered and where (UI or API); static rows store the built URL. `expected_channel` comes from step 4 before approval.
- At launch, add one row per platform campaign to [assets/platform-campaign-map-template.csv](assets/platform-campaign-map-template.csv) from a read-only export: ID, name, the `utm_campaign` the links send, `initiative_id`, `link_id`, dates seen ([columns](references/registry-and-cost-joins.md#platform-campaign-map-columns)). Cost joins use this map, never the macro text.
- A rename keeps `utm_id`, adds a dated map row and a `decisions.md` entry. Ended rows stay.

### 7. Build and test the links

Run [scripts/build_links.py](scripts/build_links.py) as described in [references/link-builder.md](references/link-builder.md). It checks static rows against `allowed-pairs.csv`, the format patterns and personal data, keeps existing parameters and the fragment, and fails URLs over 1,000 encoded characters.

Test the resolved link. The agent writes a test sheet per row and reads GA4 the next day; a person clicks, preferring ad previews over live ads. Fresh private window, a network outside the internal traffic filter, no debug mode, a click from the real context.

- Consent accepted on the landing page: the final URL and first GA4 request carry each `utm_` key once, no literal macro. Next day the channel equals the prediction.
- Consent rejected: check the request only (none in basic mode, cookieless in advanced). Record it; no next-day check.
- Consent accepted on a second page: record whether the campaign survives; if not, consent-debugging and a `decisions.md` entry.
- Conversion: the CRM record holds source, medium, campaign and `utm_id` from the URL, first touch kept, latest touch updated.

### 8. Line up the cost joins

Details: [references/registry-and-cost-joins.md](references/registry-and-cost-joins.md#cost-joins).

- GA4 campaign data import joins on `utm_source`, `utm_medium` and date; `utm_id` and `utm_campaign` are recommended and must match logged values exactly. `utm_content` and `utm_term` cannot be imported. Cost needs a currency. Template: [assets/cost-import-template.csv](assets/cost-import-template.csv).
- File imports: join the platform export to the map on platform and campaign ID, taking the `utm_campaign` live on each date. An ID missing from the map is a tagging gap: list it, never guess.
- Automated connectors (Meta, Pinterest, Reddit, Snap, TikTok): one fixed source and medium per platform, unchangeable on Meta; `utm_campaign` must be the platform name, or use a file import from the map.
- Warehouse: cost joins through the map to `collected_traffic_source.manual_campaign_id` and the CRM's `utm_id`; Google Ads on `session_traffic_source_last_click.google_ads_campaign.campaign_id`; affiliate commission to orders on the network's order ID.

### 9. Propose the files

Show the user and wait for approval before writing:

- the UTM section of `martech-context/naming-rules.md`, with its owner
- `martech-context/tracking-plan/utm-registry.csv`, `utm-allowed-pairs.csv` and `platform-campaign-map.csv`
- `decisions.md` entries: cutovers, mappings, custom channel order, the Meta source and Google Ads manual UTM choices
- `stack.md` additions: cost routes, ESP and redirect behaviour, consent mode type

## Common failure modes

Detection queries for each: [references/failure-modes.md](references/failure-modes.md).

- Paid in Organic Social: Organic Social sessions whose manual campaign ID is in the map.
- Literal or encoded macros: campaign values containing `{`, `}`, `%7B` or a macro name.
- Case splits: values equal after lowercasing; low import match rate.
- Meta name drift: one manual campaign ID with two names after a rename.
- Duplicate keys: LinkedIn creative UTMs or both LinkedIn levels, auto-tagging next to registry values, Google syntax imported into Microsoft.
- Stripped parameters: GA4's missing URL parameter diagnostic; shorteners, bio tools, redirects or single-page apps dropping the query.

## Worked example

Acme Analytics, a fictional B2B SaaS, launches `2026_11_forecasting_launch`. Full walk with inventory lines, an ambiguous pair and the map-to-import join: [references/worked-example.md](references/worked-example.md). Files: [assets/example-acme-registry.csv](assets/example-acme-registry.csv), [assets/example-acme-campaign-map.csv](assets/example-acme-campaign-map.csv).

- LinkedIn: the agency's `li / paid` (predicted Paid Other) became `linkedin / paid_social` with `CAMPAIGN_NAME` and `CAMPAIGN_ID`.
- Newsletter: draft `example_esp / newsletter` (Unassigned) became `acme_weekly / email`; one issue carries two initiatives and two registry IDs.
- `linkedin / email` for a LinkedIn newsletter was ambiguous, so it became `linkedin / social`.
- Google Ads keeps auto-tagging plus a manual set for the CRM; Microsoft uses the suffix with auto-tagging off.

## Acceptance checks

- Vocabulary: every allowed pair has an expected channel or `test required`, with URL and date. Pass: no Paid Other or Unassigned without a reason or custom rule; every ambiguous pair in use has a `pass` result.
- Format: static values `^[a-z0-9_]+$`, mediums `^[a-z0-9_-]+$`, campaigns `^20[0-9]{2}_(0[1-9]|1[0-2])_[a-z0-9_]+$`; map `utm_campaign` values pass the house naming rules. Pass: zero violations.
- IDs: every paid campaign with spend has a map row. Pass: on any date each platform campaign ID and each `cmp_` ID maps to one `utm_campaign`; in GA4 each Session manual campaign ID has one name except across a logged rename.
- Tests: every live row records the final URL, the consent-accepted first request and the observed channel. Pass: each `utm_` key once, observed equals `expected_channel`; the consent-rejected run is recorded, not scored.
- CRM: one test conversion per channel. Pass: fields equal the URL values, first touch unchanged by a later test visit, latest touch updated.
- Platform settings: Microsoft's URL test tool shows each key once with `utm_source=bing`, rerun after Google imports; LinkedIn uses one level; every Google Ads campaign with manual UTMs has a `campaign` parameter equal to its name. Pass: no duplicate, missing or literal value.
- Cost: every paid platform has a route. Pass: "% imported" is 100%; test-date cost equals the platform export within 1% in the same currency; names and IDs in the Non Google campaign report match the map; unmatched rows are fixed or explained.
- Privacy: no static value contains `@`, 7 or more digits in a row, or a CRM ID; no internal URL is in the registry. Pass: the builder and a search return none.
- Approval: nothing in `martech-context/` changed without the user's approval.

## Boundaries

- This skill writes a convention, a registry, a campaign map, a test sheet and draft URLs. It does not change ad platform, GA4, ESP, CRM or redirect settings or published links; each change is a draft applied by a human.
- A custom channel group needs a GA4 Editor. New values do not rewrite GA4 history; record the cutover and give analysts the mapping.
- Hand-offs:
  - GA4: Google's GA4 MCP server, read-only.
  - Google Ads: Google's Google Ads MCP server, strictly read-only (GAQL). Edits in the UI or Editor after approval.
  - Microsoft Advertising: Microsoft's Advertising MCP server, read-only through open flight (August 2026 product news), for account and campaign reads. Confirm live whether it returns tracking templates and suffixes; otherwise an account user reads URL options and runs the URL test tool.
  - Meta: Meta's Ads MCP server, read tools only; URL parameter changes go through a human in Ads Manager.
  - LinkedIn: no official MCP server; an account admin edits tracking parameters in Campaign Manager or the Marketing API.
  - consent-debugging for consent-state failures; ga4-unassigned-not-set-diagnosis for missing rows after a correct link.

## 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.

The entries that matter most: Google's default channel group page and source spreadsheet, combining tagging methods, aggregate identifiers, campaign data import, the ValueTrack page, Microsoft's UTM auto-tagging, final URL suffix and URL parameters pages, Meta's URL tags guide and the GA4 Meta connector page, and LinkedIn's Dynamic UTM Tracking page. Platform facts were checked on 2026-10-17.
