source of truth · lower-case everything

Naming conventions

Lower-case everything, separator is the hyphen -. Every name is parseable by grep on a single line. These four patterns are the only ones that should appear in dashboards, asset filenames, and UTM params.

Ad-set name pattern

Pattern
{audience}-{funnel_stage}-{geo}-{platform_suffix}
Audience tells you who, funnel_stage tells you where in the funnel they entered, geo tells you where to fly it, platform_suffix disambiguates Meta from Google when the same audience runs on both. Each piece is grep-able.
valid
prospecting-cold-lal1-us-meta
Matches all four slots. prospecting-cold + lal1 (lookalike) + us + meta platform. Grep for prospecting-cold finds every campaign that targets the cold-prospect audience.
invalid
Prospecting LA Lookalike US — Meta
Mixed case, embedded spaces, no separators, natural language phrases. Cannot be grep'd by audience without false positives. No funnel_stage slot.
valid
retargeting-tyvisit-uk-google
Targets UK visitors to the thank-you page on Google. Audience + stage + geo + platform all present.
invalid
retargeting_UK_v3
Underlines instead of hyphens. No funnel_stage slot. v3 is a version, not an audience — versioning belongs in the dashboard, not the name.

Creative filename pattern

Pattern
{concept_id}-{variant}-{format}.{ext}
First two segments become utm_content + utm_term. Filename stays parseable after the platform's auto-tagging mangling.
valid
hook-v1-wagleak.mp4
hook is the concept id, v1-wagleak is the variant. Drops cleanly into utm_term=hook-v1-wagleak.
invalid
Hook V1 - Wage Leak (Final)!.MP4
Spaces break every shell tool. Upper-case. Punctuation the platform will mangle. (Final) and ! end up as _- or stripped when the URL is built.
valid
explain-30s-payrollrun.png
Static image creative, named for the concept. Extension matched to the actual file format.
invalid
FINAL_Image_Copy_03_v2.jpeg
Naming reveals the iteration ladder, not the concept. FINAL_Image is meaningless to anyone but the author. New variant == new concept id; iterate the variant field, not the workflow.

Folder pattern per vertical

Pattern
/campaigns/{key}/assets/{concept_id}/{format}/{file}
Concept ids are stable across variants, formats (image, video, doc) are flat siblings inside the concept, files sit at the leaf. One per creative variant, never bundled.
valid
/campaigns/pl/assets/hook/image/hook-v1-wagleak.png
Concept hook, format image, file inside. Drift between campaigns is visible at the campaign level; drift between variants is visible at the concept level.
invalid
/campaigns/pl/assets/creatives/2026/aug/v1/png/hook.png
Time-stamped folders (2026/aug/v1) document when an asset was made, not what it is. New campaigns can't reuse old assets without renaming. Also: campaign pl is buried under generic labels.
invalid
/campaigns/pl/assets/hook-v1.png
All assets at the campaign root. The next variant (hook-v2) ends up a sibling, then the directory fills with hook-v1.png, hook-v1-wagleak.png, hook-v2-final.png and you can't tell which is current without opening them.

Product ID role suffixes

Product IDs in registry/products.json carry a role suffix so the same vertical can run multiple offers per role without ID collisions across environments. The suffix is appended to a stable vertical prefix:

Pattern
{VERTICAL_PREFIX}_{ROLE}[_{ENV}]
Vertical prefix + role is the same in every environment. The _ENV tail only changes for staging/test IDs — production IDs never carry a suffix.
RoleSuffixStable example
core offer_MAINPROD_PAYROLL_MAIN
order bump_BUMPPROD_PAYROLL_BUMP
OTO1 (assist)_OTO1PROD_PAYROLL_OTO1
OTO2 (case study)_OTO2PROD_PAYROLL_OTO2
test-pay (any role)_TESTPROD_PAYROLL_MAIN_TEST

Scenario link naming

The media planner at /plan/ writes its scenario into the URL hash. That URL is the share-link, and the slug in the hash should be human-readable so it survives copy-paste into chat / docs:

Pattern (URL hash)
#c={key}&p={preset}&{param}={value}&…
The hash is the canonical share format. c = campaign key, p = preset (conservative/base/aggressive), the rest are documented at the bottom of the planner page. Keys are stable across versions.
valid
/plan/#c=pl&p=base&s=8000&d=45&cv=6
Campaign + preset + spend bump + duration bump + a critical CVR change. Identifiable in chat: "the $8000/45d at 6% scenario."
invalid
/plan/#c=pl&s=8000&cv=6&d=45
No preset anchor. Receiver of the link can't tell whether cv=6 is a Conservative or Aggressive run — they have to click and inspect.
invalid
/plan/?scenario=pl-base-v2-s8000
Query-string form. Not used by the planner. The hash is the canonical contract.