# Grammar and vocabulary

The default convention that `assets/naming-rules.yaml` and `assets/naming-vocab.csv` encode. Adapt the values to the business; keep the mechanics.

## Default grammar

Tokens are joined with `_`. Words inside a token are joined with `-`. Everything is lowercase `a-z` and `0-9`.

Initiative level (Google Ads campaign, Meta campaign, LinkedIn campaign group):

```text
{market}_{objective}_{funnel}_{audience}_{offer}_{launch}
us_leads_bofu_nonbrand_demo_202610
uk_leads_tofu_smb_pipeline-webinar_202610
```

Targeting level:

```text
Google Ads ad group      {targeting}_{theme}               kw_crm-sync
Google Ads asset group   {targeting}_{theme}               sig_site-visitors-30d
Meta ad set              {targeting}_{theme}_{placement}   lal_customers-1pct_auto
LinkedIn campaign        {targeting}_{theme}_{format}      job_revops-leaders_doc
```

Ad level:

```text
Meta ad                  {format}_{concept}_{variant}      vid_founder-story_v2
Google Ads named ads     {format}_{concept}_{variant}      vid_founder-story_v2
LinkedIn creative        {concept}_{variant}               pipeline-checklist_v1
Google responsive search ad: no name field; registry row keyed by ad ID
```

LinkedIn creatives drop `format` because LinkedIn sets the format on the campaign, and the campaign name already carries it.

These names are for people scanning the platform and for the parser. They are not `utm_campaign`: that is the registry initiative name (`2026_10_pipeline_webinar`), set by utm-convention-and-registry. See `utm-mapping.md`.

## Why these tokens

The rule: a token earns its place when a recurring report groups spend by it and the platform does not store it as a flat field the spend connector exports.

- `targeting`, Meta `placement` and LinkedIn `format` are platform fields, kept anyway. Meta placement sits inside the ad set's nested targeting spec, LinkedIn's campaign format and Meta's targeting type are often missing from spend connectors, and buyers need all three visible in the platform's list view. Drop any of them when the team's connector exports it as a flat column and nobody reads the list view.
- Campaign type, bid strategy, budget, status, geo detail, devices, owner or agency, end dates and experiment arms stay out. Read those fields from the API.

## Why this order

- Broadest business grouping first, so the platform's name-sorted list groups by market, then objective.
- Launch month last, so `offer_launch` reads as the initiative and sorts by month within an offer.
- The order never changes after go-live. Parsers read by position. A reorder is a new rules version with a cutover date in `decisions.md`, and the parse model branches on `rules_version` from the registry.

## Token definitions

- `market`: the business market the budget belongs to, not the exact geo targeting (a platform field). Defaults: `us`, `ca`, `uk`, `dach`, `fr`, `nordics`, `emea`, `apac`, `latam`, `global`.
- `objective`: the business outcome the initiative is judged on, not the platform objective setting (a field, and immutable on a LinkedIn campaign group). Defaults: `leads`, `sales`, `signup` (trial starts and free sign-ups), `traffic`, `awareness`, `engagement`, `install`.
- `funnel`: `tofu` for people new to the brand, `mofu` for people who engaged but did not convert, `bofu` for high intent (brand search, pricing-page retargeting, demo offers), `cust` for existing customers.
- `audience`: who the whole initiative object is for. Company size codes (`smb`, `mm`, `ent`), `partner`, or `all` when its children target different groups. Existing customers are funnel `cust` with their size in `audience`, so there is no `customer` value.
- `audience` on keyword-targeted Search: `brand`, `nonbrand` or `competitor`, because the query set is the audience and brand spend must split cleanly from non-brand. Search campaigns therefore lose the company-size cut. Keyword targeting cannot select company size, so size for Search comes from the CRM after the click, not from the name.
- `offer`: what the click is for. One value per offer, reused across months and platforms: `demo`, `free-trial`, `pricing`, `pipeline-webinar`, `crm-guide`.
- `launch`: YYYYMM of first serving. It is not updated when a campaign runs on. A relaunch as a new object (a duplicate, or an experiment converted to a new campaign) gets a new launch month; budget and bid changes do not.
- `targeting`: `kw` keywords, `int` interests, `lal` lookalike, `rt` retargeting of site or engagement audiences, `list` uploaded customer or CRM list, `job` job title, function or seniority, `co` company list, `broad` platform-optimised or no explicit targeting, `sig` Performance Max audience signal, `ctx` contextual or placement targeting.
- `theme`: a hyphenated slug of at most 30 characters naming the keyword theme or audience (`crm-sync`, `customers-1pct`, `revops-leaders`).
- `placement`: `auto` for platform-chosen placements, otherwise `feed`, `story`, `reel`, `search`, `video`, `display`, `inbox`.
- `format`: `img`, `vid`, `car` (carousel), `doc` (document), `txt`, `msg` (message or conversation), `col` (collection).
- `concept`: slug of at most 30 characters naming the creative idea, stable across sizes and variants.
- `variant`: `v1` to `v99`, one per tested version of a concept.

## Vocabulary rules

- Every value lives in `naming-vocab.csv`: `token`, `value`, `status` (`approved` or `retired`), `alias_of`, `retired_on`, `approved_by`, `approved_on`, `notes`. The linter reads it, and the warehouse loads it as the `naming_vocab` seed. No other copy.
- Short lowercase ASCII codes. A value may contain `-` but never `_`; the linter refuses to load a value with the delimiter.
- "Spans several" values exist where a token needs one: market `global`, audience `all`, placement `auto`, targeting `broad`. No token has `other`, `misc`, `new` or `test`. Test campaigns run in test accounts or carry `status=test` in the registry.
- One value per meaning. A value that appears in two tokens must mean two different things, or one of them goes. That is why objective has `signup` rather than a second `trial`.
- A value never changes meaning. To replace one: add the new value as `approved`, set the old row to `retired` with `alias_of` pointing at the new value and `retired_on` set to the cutover date. Old names keep parsing through the alias. The linter fails the retired value on any name without a `start_date` or starting on or after `retired_on`, and warns on older names.
- A term with two meanings gets two values. Example: "GTM" can mean a tag manager or go-to-market, so the vocabulary has `gtm-tag-manager` and `go-to-market`, and on Search the two get separate ad groups and landing pages so search terms, ads and reports never mix them.
- Avoid GA4 channel-rule terms in market and offer values: nothing starting with `shop`, and nothing containing `shopping` or `cross-network`. GA4's manual-traffic rules send a paid campaign name matching `^(.*(([^a-df-z]|^)shop|shopping).*)$` to Paid Shopping and one containing `cross-network` to Cross-network. Offers often feed the registry initiative name, so an offer such as `shopify-app` could pull Meta or LinkedIn traffic out of Paid Social. Use the term only when that channel is intended.
- Never add a value to make a name pass the lint. List it as a candidate with the names and spend that use it, for the approvers in `governance.md`.
- Do not encode people's initials. Ownership changes; the registry holds the owner.

## When the default does not fit

- Several products: add `product` after `market` only when spend reports split by product and the offer does not already imply it. When each offer belongs to one product, map offer to product in the vocabulary notes or a lookup and keep the name short.
- Agency-run accounts: same grammar. The agency is the registry `owner`, not a token.
- More platforms (Microsoft Advertising, TikTok, Reddit): map their objects to the three levels and reuse the initiative grammar unchanged, so cross-channel reports keep one shape.
- Platform-created objects, such as the default campaign group LinkedIn creates with every ad account: list them in `exempt_ids` as `platform:object_id` with a reason, or rename them where the platform allows it.

## Before and after

```text
Q4 Push - US - Leads (NEW) copy        ->  us_leads_bofu_nonbrand_demo_202610
  free text, punctuation, a "copy" suffix from duplicating, no offer or launch

PMax | All Products | tROAS 400        ->  us_sales_bofu_all_spring-sale_202603
  campaign type and bid target are fields; the name kept neither offer nor launch.
  spring-sale is not in the default vocabulary: it must be approved and added
  to naming-vocab.csv before this name passes the lint.

FB_Prospecting_Lookalike_UK_Oct        ->  uk_leads_tofu_smb_pipeline-webinar_202610
  platform is implied by the account; month without year; no offer
```
