Skip to main content

GTM Naming Conventions That Scale Past 100 Tags

A typed prefix system for tags, triggers, and variables that makes a 300-item container readable in one scroll.

Open a container that's been live for four years and you're doing archaeology. There's a tag called "GA4 Event," another called "GA4 Event 2," and a third called "GA4 Event - NEW (use this one)." A variable named "cjs" that nobody will delete because nobody can prove what reads it. A trigger called "Thank you page" that fires on a URL path the site stopped using in 2022.

None of that happened because the person was careless. It happened because there was no scheme, so every name was a fresh judgment call made under time pressure, and judgment calls made under time pressure don't agree with each other. A naming convention isn't tidiness. It's the difference between reading a container and excavating one.

The Archaeology Problem

The cost of bad names is invisible until the container passes about a hundred tags, then it becomes the dominant cost of working in it.

Say a client asks whether their Meta pixel fires on the checkout confirmation page. In a well-named container that's a search box and eight seconds. In an unnamed one you open Preview, run a test order, and read the fired tag list, which takes fifteen minutes and only tells you about the path you happened to test. Multiply that by every question anyone asks about the container and you've built a permanent tax on the account.

The second cost is deletion paralysis. Nobody deletes anything from a badly named container because nobody can establish what a thing does without tracing it, so containers grow monotonically. I've seen containers with 300 tags where fewer than half were firing. Every one of those adds page weight and adds a candidate whenever something breaks.

The third cost is the handover. A new person joining an account with a clean container is productive in a day. On a messy one they're guessing for a month, and the guesses become new badly named tags.

The Three Part Scheme

Every tag name has three parts, separated by a space hyphen space, in a fixed order: platform, type, purpose.

Platform is the destination system. GA4, Google Ads, Meta, LinkedIn, TikTok, Pinterest, Hotjar, Klaviyo. Use the product name people say out loud, not the internal one. Nobody searches for "gtag."

Type is what kind of tag it is inside that platform. For GA4: Config, Event. For Google Ads: Conversion, Remarketing, Conversion Linker. For Meta: Base, Event. For anything else, whatever the vendor's template calls it.

Purpose is the specific thing, written the way it appears in the destination. If GA4 receives an event named begin_checkout, the purpose is begin_checkout, not "Checkout Start." This one rule does more work than the other two combined, because it means searching for an event name in GA4 and searching for it in GTM return the same thing.

Sorting is the reason platform goes first. GTM's tag list sorts alphabetically by name, so platform-first groups every tag for a destination together without any filtering. When a client cancels TikTok, you type "TikTok" in the search box and you're looking at the complete list to remove.

Keep names free of dates, initials, and status words. "NEW", "v2", "FINAL", "JS test" are all signals that someone gave up. If you need to mark a tag as retired, pause it and move it to the deprecated folder, where its status is a property of the container rather than a word in a string.

Worked Examples Across Platforms

The scheme is easy to describe and easy to get wrong in the middle, so here's what a real container looks like.

Tag nameWhat it is
GA4 - Config - Main propertyThe Google tag loading the primary measurement ID
GA4 - Event - view_itemProduct detail view
GA4 - Event - add_to_cartCart add on API success
GA4 - Event - purchaseTransaction, client side
GA4 - Event - generate_lead (demo form)Same event name, one of several sources
Google Ads - Conversion - PurchaseConversion action with value
Google Ads - Remarketing - All pagesDynamic remarketing with ecomm params
Google Ads - Conversion Linker - All pagesLinker, fires on init
Meta - Base - PixelBase pixel code
Meta - Event - PurchaseStandard event, capitalized as Meta names it
LinkedIn - Insight - All pagesInsight tag
Klaviyo - Event - Started CheckoutThird party event, vendor's own casing
Utility - Custom HTML - Consent defaultsNo destination platform, so Utility

Two decisions in there are worth calling out. Notice that the Meta and Klaviyo purposes keep the vendor's capitalization while GA4's keep snake_case. That's deliberate: the purpose should match the destination exactly, and different platforms name events differently. Consistency with the platform beats internal consistency, because the platform is where someone will go looking.

Notice also the parenthetical on generate_lead (demo form). When several tags send the same event from different sources, the event name stays and the disambiguator goes in brackets at the end. That way alphabetical sorting still groups them.

Tags with no external destination get Utility as the platform. Consent defaults, a script that pushes a datalayer value, a polyfill someone insisted on. They're rare and they should be, but they need somewhere to live.

Variable Prefixes by Source

Variables use a prefix rather than the three part scheme, because what you need to know about a variable is where its value comes from. When you're debugging and a variable resolves to undefined, the prefix tells you which layer to go look at.

PrefixVariable typeExample
DLVData Layer VariableDLV - ecommerce.value
JSCustom JavaScriptJS - Normalized email hash
JSVJavaScript Variable (global)JSV - dataLayer.0.pageType
LTLookup TableLT - GA4 measurement ID by hostname
RTRegex TableRT - Page type from path
CONSTConstantCONST - Meta pixel ID
COOKIEFirst Party CookieCOOKIE - _fbp
URLURL VariableURL - Query gclid
DOMDOM ElementDOM - Price on PDP
AEVAuto-Event VariableAEV - Click classes

For data layer variables, name the variable after the exact key path it reads. DLV - ecommerce.items.0.item_id is long and completely unambiguous, and you never have to open it to know what it does. Resist the urge to name it something friendly. Friendly names are where "cjs" comes from.

Two prefixes earn special attention. CONST variables should hold every ID in the container: measurement IDs, pixel IDs, conversion IDs, API keys for tag templates. Reference the constant from the tags rather than pasting the ID into eight places, and when the client migrates properties you change one variable instead of hunting. JS variables should carry a comment block at the top of the code stating what they return and what happens on failure, because a custom JavaScript variable is the only thing in a container that can silently return undefined and take four tags down with it.

Triggers That State the Condition

A trigger name should tell you the event type and the condition without opening it. Two parts: type, then condition.

The type prefix mirrors GTM's own trigger type names, so someone scanning the trigger list is reading the same vocabulary the interface uses. The condition half should be specific enough that two triggers of the same type never share a name. If you find yourself writing Page View - Specific pages, you've stopped naming and started labelling.

Exception triggers get the word Blocking at the front: Blocking - Page View - Internal traffic by IP. It reads slightly awkwardly and that's the point, because a blocking trigger attached to the wrong tag is a nasty bug and you want it visually distinct in the tag's trigger list.

One habit that saves real time: when a trigger's condition changes, rename the trigger. A trigger called Page View - Path contains /thank-you that now checks a datalayer flag is worse than no name at all, because it's actively lying. Renaming a trigger is safe, since GTM references triggers by internal ID rather than by name.

Folder Structure That Matches

Folders are the second axis. Numbered so they sort in a sensible order rather than alphabetically, and shallow, because GTM folders don't nest.

Put a tag's triggers and variables in the same folder as the tag when they're used by one platform only. Shared items (the ecommerce DLVs, the consent variables) go in 00 Core. This is the one place where a little duplication of judgment is fine: if you can't decide, Core is the safe answer.

The 99 Deprecated folder deserves a policy. A tag goes in there paused, with a note in the version description saying when and why. Anything sitting there after two publish cycles gets deleted. Without the delete step it becomes a museum.

Retrofitting an Existing Container

You can rename an entire container in an afternoon using export, edit, and import. The workflow is straightforward, but one detail will bite you if you miss it.

Tags and triggers are referenced internally by numeric ID, so renaming them in the JSON is safe. Variables are referenced by name, as {{Variable Name}} strings inside tag parameters, trigger conditions, and other variables. Rename a variable and every reference to it breaks unless you replace those strings too. That's why this is a scripted find and replace rather than a manual pass.

  1. Go to Admin, Export Container, and export the live version. Keep an untouched copy somewhere safe.
  2. Build a mapping of old name to new name. Do it in a spreadsheet with a human reviewing every row, because this is where you catch two tags that should be one.
  3. Run the rename over the JSON, handling variable references explicitly.
  4. Import into a brand new workspace with Overwrite selected, so the result is exactly your file.
  5. Preview and walk the full site before publishing. You're checking that nothing resolves to undefined.
const fs = require("fs");
const c = JSON.parse(fs.readFileSync("container.json", "utf8"));
const map = JSON.parse(fs.readFileSync("rename-map.json", "utf8")); // { "old": "new" }
const cv = c.containerVersion;

["tag", "trigger", "variable", "folder"].forEach(k => {
  (cv[k] || []).forEach(item => {
    if (map[item.name]) item.name = map[item.name];
  });
});

// Variables are referenced by name. Rewrite every {{...}} token.
let blob = JSON.stringify(cv);
Object.keys(map)
  .sort((a, b) => b.length - a.length)   // longest first, avoids partial hits
  .forEach(old => {
    blob = blob.split("{{" + old + "}}").join("{{" + map[old] + "}}");
  });

c.containerVersion = JSON.parse(blob);
fs.writeFileSync("container-renamed.json", JSON.stringify(c, null, 2));

The longest-first sort matters. If you rename both Page Path and Page Path Clean, replacing the shorter one first corrupts the longer one. Sorting by descending length avoids it.

Do the rename as its own change, published on its own, with a version note saying exactly that. Bundling a rename with a functional change means that when something breaks you can't tell which half did it.

Keeping It Clean Afterward

A convention decays unless something checks it. Three mechanisms, in increasing order of effort and effectiveness.

Write it down in one page with worked examples, not rules in the abstract. People copy examples. Nobody reads a rule about capitalization and applies it correctly on the first try.

Put a naming check in your review step, so no workspace gets approved with a tag called "test." This is where most of the enforcement actually happens, because it's the one moment a second person is already reading the change.

Automate the audit. Export the live container monthly through the GTM API and match every tag, trigger, and variable name against a regular expression built from your scheme. Anything that fails goes on a list with the container name and the offending item. It takes an hour to write and it will still be catching things two years later, which is more than you can say for the wiki page.

Start with the tags. Tags are what people search for, tag names carry the most information, and renaming them requires no reference rewriting at all. Get those right and the container is already readable. Variables and triggers can follow in the next pass.