Conventional approach
DLV - user_idDLV - ecommerce.valuecjs - find closest elementcookie - isPaidCustomer1pc - sessionIdconst - GA4 measurement ID
Every prefix repeats what GTM’s “Type” column already shows in the next field over.
By working on thousands of GTM containers I spend a lot of time defining the best naming convention for tags, variables and containers.
For years, I made the same mistakes as all major existing guides. Specifically, six recurring mistakes:
One convention can’t fit every component. Tags, triggers and variables play different roles and need different kinds of rules. What most analysts call a “GTM naming convention” is really a concatenation template — not a naming convention in the software-engineering sense (think JavaScript’s camelCase or verb-first functions, which are shape rules, not field assembly).
Prefixes duplicate what GTM already shows you.
Every DLV - and cjs - prefix duplicates information GTM’s interface already displays in a dedicated column. Wasted characters, wasted screen space, wasted attention.
No visible distinction between raw and processed data.
A variable that returns the dataLayer’s order_id directly and a variable that returns a deduplicated, normalized version of that same order_id are fundamentally different things — one is owned by your developers, the other by your container — but most conventions name them identically.
Names don’t help you find duplicates. In a 200-variable container, you will end up with three variants of the same value. The convention either makes those duplicates self-evident through search and sort, or it doesn’t.
The convention is written as if notes don’t exist. GTM provides a note field on every asset. The competing guides treat the name as the only information channel — forcing names to encode rationale, source, and history that belong elsewhere.
No update of naming convention since new search functionality. Search bar can search into most assets, no need to write all parameters into the name to bypass the search limits.
GTM’s interface already gives you several places to put information: the asset list, the type column, the folder structure, the description field, the search box. Each one is a channel that can carry meaning. The name is one channel among them — not the only one, and not the place where every piece of information belongs.
Competing conventions ignore this. They make the name carry type, ownership, source documentation, and rationale all at once — because they treat it as the only information surface. It isn’t. Put each kind of information in the channel built to carry it, and the name itself gets small: just identity and category, nothing else.
Strict naming conventions get ignored because they don’t earn their cost. I have never seen a container where the convention is perfectly followed — there’s always a typo, always assets that drifted from the template. The reason isn’t sloppiness. It’s that the convention doesn’t bring enough value to be worth following.
A typical naming convention for tags looks like: Tool Name - Tag Type - Trigger. In practice you end up with:
GA4 - Custom Event - Document Download
GA4 - Event - Click Element - Button - Link
CAPI - GA4 - Add to Cart
Looks familiar?
How do you parse the last one? What is Add to Cart? The trigger? The event tag label? The template imposed structure but didn’t tell you what each field means — so people fill the slots inconsistently and the structure becomes noise.
Triggers get the same treatment, but worse. Some conventions try to put all the filter rules into the name: Booking Funnel Step 3 Activities - Event checkout AND event EQ consent-ready
This is already long for a single filter. What happens when the trigger covers two page categories? Three? The name keeps growing until it stops being a name and starts being a configuration dump.
Start by deciding what the naming convention needs to do. Where will you use it, and what decisions will it help you make? A few examples from real engagements:
Consent reporting. In most European containers, consent is critical and I need to export the list of tags by category fast. Every tag in my convention starts with a single letter: A for analytics, M for media, P for personalization. One alphabetical sort and the categories cluster themselves.
Side-effect visibility. Some variables and custom templates aren’t pure — they push to the dataLayer, write cookies, set global JS variables. These need to be visible at a glance because they’re the ones that cause cross-tag bugs. I prefix them with ! so they stand out in any list.
Each rule earns its place because it answers a real question the container makes you ask repeatedly. Rules that don’t answer a real question get ignored — which is exactly what happens to the “Tool Name - Tag Type - Trigger” template.
One last point, which sets up everything that follows: trigger naming can’t be a strict concatenation template at all. The realistic variation across events, filters and exceptions is too high for any fixed shape. What most analysts call a “GTM naming convention” isn’t really one in the software-engineering sense — it’s field assembly. Real naming conventions (JavaScript’s camelCase, verb-first functions, semantic prefixes) are shape rules, not slot filling.
Open any GTM container and look at the variable list. There’s a column labeled “Type” that shows whether each variable is a Data Layer Variable, a Custom JavaScript variable, a 1st-Party Cookie, or any of the other built-in types. GTM is already telling you the type. In a clearly visible column. On every row.
Now look at the names other conventions recommend:
Conventional approach
DLV - user_idDLV - ecommerce.valuecjs - find closest elementcookie - isPaidCustomer1pc - sessionIdconst - GA4 measurement IDEvery prefix repeats what GTM’s “Type” column already shows in the next field over.
After removing duplication
$user_id$ecommerce.valuefindClosestElementisPaidCustomersessionIdG-XXXXXXXXXXThe name says what the value is. The type column says what kind of variable. Each channel does its own job.
This is where the convention starts doing real work. In any non-trivial container, two fundamentally different kinds of values share the variable list:
These are completely different things architecturally. Raw data is owned by whoever populates the source — your developers’ dataLayer, the URL the user landed on, the cookie the CMP wrote. Processed data is owned by GTM — your container is responsible for the logic that produced it.
When something breaks downstream, those two ownerships matter. A wrong value in raw data is a developer or source-system problem. A wrong value in processed data is a container problem. Diagnostics start with knowing which it is. The convention should make that visible at a glance.
Most conventions name these identically — both end up as user_id, order_id, or some other indistinguishable label. The reader has to open the variable to find out which kind it is. Multiply by hundreds of variables, and the cognitive cost is real.
The fix isn’t a prefix. It’s a typing system encoded in the visual shape of the name itself. Four kinds of variables, four casings — each one immediately recognizable in a list without reading carefully:
Raw data
$ + lowercase, mirror source
Values pulled directly from the dataLayer, cookies, or URL parameters. The name mirrors the source key as closely as possible. The leading $ is the visible marker that this is unprocessed source data.
$user_id
$ecommerce.items.0.item_name
$funnel_type
$_session_token
Function
camelCase, verb-first
Boolean tests and computed values. The verb describes the work the variable does — testing, calculating, finding, formatting. Never starts with get: every GTM variable already gets when referenced; get would be redundant.
hasProductList
isFirstPurchase
calculateOrderTotal
findClosestElement
Processed data
TitleCase, descriptive
The output of computation — normalized values, derived metrics, hashed PII, multi-input combinations. TitleCase signals “this is a computed result, owned by the container.” Concise and meaningful, no leading verb.
NormalizedOrderID
SelectedCourseName
HashedUserEmail
ParentClassNames § upto10Levels
Constant
UPPERCASE, exact value when possible
Literal values that don’t compute or read from anywhere — measurement IDs, fixed parameter codes, conversion IDs. The exact value goes in the name when it’s short enough; otherwise an UPPERCASE label.
FB4534455
G-XXXXXXXXXX
GA4_DEFAULT_CURRENCY
CONSTANT=1
The reader’s eye does the work. Lowercase with $ means “this came from outside, mirror of source.” camelCase starting with a verb means “this does work.” TitleCase means “computed result.” UPPERCASE means “literal.” Without reading any name carefully, the reader can scan a list of 50 variables and immediately know the architectural shape of the container.
The $ isn’t decorative. It solves a real GTM constraint. GTM doesn’t allow variable names to start with an underscore — but it does allow names to start with $. So when the dataLayer pushes _user_id or _session_token (a JavaScript convention for internal/private values that bleeds into structured data), the variable can be named $_user_id and still mirror the source exactly.
Without the $ prefix, every underscore-prefixed dataLayer key would force a naming exception. With it, the convention covers every dataLayer push your developers will ever write, including the ones that follow JavaScript private-property conventions. No exceptions, no special cases, no “well, for these keys we have to do something different.” That’s the kind of robustness only visible after you’ve used a system across hundreds of dataLayer schemas.
get verb on functionsget carries no information. Every GTM variable fetches a value when referenced — that’s what variables do. Putting get in the name tells the reader nothing they didn’t already know, and trains their eye to skip the first three characters of every function variable. Drop it.
The verbs that earn their place tell the reader something get can’t: what kind of work the variable does, and what shape of value it returns.
| Verb | Work | Returns |
|---|---|---|
is, has | Boolean test | true / false |
calculate, normalize, format, parse, build | Transform input | Single computed value |
find | Scan and return | Single match or null |
filter, map | Collection operation | Array |
The reader sees hasProductList and knows it returns a boolean before opening it. They see filterValidItems and know it returns an array. They see normalizeOrderId and know it returns a single processed string. Compare getProductList — could be a dataLayer read, a DOM query, a computed array, a constant. The verb told you nothing.
In a 500-variable container, you will end up with three variants of the same value. Different team members create variables at different times. The previous analyst left and the new one didn’t see what already existed. The marketing team needed a similar value for a different campaign and didn’t realize a variable already covered it.
The predictable result: // Three variables, all returning the same dataLayer value DLV - user_id User ID DLV - userId
Now your tags reference all three. Which is canonical? Which is being read by the misfiring purchase tag? Which can you safely delete? The names don’t tell you — and worse, the names don’t even surface as duplicates in the search box, because no shared substring connects them.
The convention either makes duplicates self-evident through search, or it doesn’t. Most don’t. Competing guides treat search as an afterthought — they recommend “be consistent” without designing names against how the search box actually behaves.
GTM’s search filters by substring. So design the convention so that typing the base name reveals every variation of it. Concretely: pick a canonical name, then attach variations with a separator that lets the base name remain searchable on its own.
I use the section sign (§) as the within-name separator. Type the base name in the search box and every variant appears in the same result list:
| Type this | And you see |
|---|---|
purchase | purchase, purchase § Processed, purchase § notProcessed |
user_id | $user_id, $_user_id, HashedUserId, isUserIdValid |
ParentClassNames | ParentClassNames § upto5Levels, ParentClassNames § upto10Levels |
If a duplicate exists, it shows up next to the canonical version, side by side. The search box becomes a duplicate finder — without any extra tooling, audit script, or convention-enforcement bot.
§ specificallyThe deepest reason — and the one that matters most at scale — is that § nests safely with |. Tag names use | as the outer separator (covered in the tags section below). Trigger names that contain § can be embedded inside tag names without breaking the structure: the outer | parser can never confuse itself with the inner §. Two distinct characters, two distinct levels of structure, no ambiguity.
The other reasons are smaller but earn their place too:
§. The separator can never collide with content inside the segments it separates.This is the part most conventions get wrong: separators aren’t decoration, they’re parsing characters. You choose them for what they’re guaranteed not to collide with, and you design them in pairs so outer structure (tags) can contain inner structure (triggers) without the parser ever getting confused. Most guides pick a separator that “looks nice.” The right separator is chosen for collision avoidance and nesting safety.
Every GTM tag, trigger, and variable has a description field. Most conventions don’t mention it — and force the name to carry source documentation, business rationale, and edge-case notes that don’t fit. The result is names that are simultaneously too long and too vague.
The name answers what is this called and what category does it belong to? Everything else goes in the description field — but as structured key-value pairs, not free-text prose. Prose descriptions read fine for one asset and become useless across 200.
The recommended shape:
owner: AgencyXpurpose: Hashed email for Meta CAPIcampaign: always_onend_date: nullThe fields you’ll typically want are owner, purpose, campaign, end_date — who maintains this, why it exists, what it’s attached to, and when it expires. For permanent infrastructure, campaign and end_date can be left empty or marked permanent. The other fields fill in when relevant: source and transform for processed variables, depends_on and related when the asset connects to others.
GTM’s description field is plain text; nothing actually parses this. The structure is a convention, not a schema. But because it’s predictable, it unlocks things prose descriptions can’t:
owner: AgencyX returns every asset that agency maintains. Searching end_date: 2026 returns every asset that expires this year.campaign and end_date filled in, “is this still needed?” becomes a sort instead of a forensic investigation.Without descriptions like this, the name has to carry everything — and it carries it badly.
Open any GTM naming guide from before roughly 2022 and you’ll find names engineered around a limitation that no longer exists: the search box used to be weak. It matched on asset names but not on configuration details, so the only way to find every tag firing on purchase was to put the word purchase in every relevant tag’s name. The only way to find every GA4 tag was to start every GA4 tag’s name with GA4. Names became dumping grounds for parameters because the search couldn’t reach inside the assets.
That constraint is gone. GTM’s search now indexes most asset internals — trigger references, event names, variable references inside tag fields, parameter values. Type purchase in the search box and you find every tag that references the purchase trigger, every variable that contains purchase in its name, every trigger whose event matches purchase — without any of those assets needing the word purchase in their name.
A lot of the bloat in current naming conventions is fossilized workaround for this dead constraint. The DLV - prefix existed partly to make dataLayer variables findable when the type column wasn’t searchable. The Tool Name - Tag Type - Trigger concatenation template existed partly to surface tag relationships that search couldn’t reveal. And the worst offender: tag IDs encoded directly into the name — M | Floodlight | DC-1234567/cat0/conv123 | Purchase, A | GA4 | G-XXXXXXXXXX | PageView — fifteen characters of pure noise per tag, added so the ID could be found by searching the name instead of the configuration. Search now reaches the configuration directly. These conventions made sense at the time. They don’t anymore.
The convention rule is simple: if the search box can find it, don’t put it in the name. Trigger references, vendor names, event names, parameter values, tag IDs — search reaches all of them. The name is freed to carry what search can’t infer: identity (what is this asset called), category (what kind of value or behavior), and scope markers (§ variations, ! side effects, $ raw data) that the search box can’t construct on its own.
This isn’t a separate rule from the earlier mistakes — it’s the deeper reason most of them are mistakes. Once search works, prefixes that duplicate the type column (Mistake 2) are pure cost. Concatenation templates that pack five fields into a tag name (Mistake 1) are solving a problem that doesn’t exist anymore. The convention this page describes is what’s left after the search-era cruft is stripped out.
Triggers don’t get the strict |-separated tag template — they’re too varied. Instead, two simpler rules cover the realistic cases:
If the trigger fires on a custom event, the trigger name is the event name. No prefix, no decoration:
add_to_cartbegin_checkoutpurchaseview_promotionThe reason: GTM’s event reference inside the trigger configuration shows exactly the same string. Naming the trigger anything else creates a false distinction between the trigger and the underlying event. Search the trigger name and find the event; search the event and find the trigger. Same word, both contexts.
When the same event needs multiple triggers because of different filtering — fire on purchase only when the order is over a threshold, fire on pageview only on specific page categories, fire only when a feature flag is set — append § plus the variation:
purchase § Processedpurchase § notProcessedCampingCarePagesCampingCampaignPages § ExceptOptoutFacebookEvery variant of purchase appears when typing purchase in the search box. The base name is the searchable canonical; the § separator lets variations attach without becoming new things.
The § Except suffix marks blocking triggers cleanly. CampingCampaignPages § Except reads as “the variation of CampingCampaignPages that’s an exception” — which is exactly what a blocking trigger is. No separate EXC - prefix to break the convention; the variation marker handles it.
Tags are the only asset where the strict |-separated template earns its place. Tags need to sort and group well — when scanning a 200-tag list, you want all the analytics tags clustered, all the media tags clustered, all the personalization tags clustered. Strict structure achieves that; lighter conventions don’t.
Purpose | Vendor | Trigger | Detail
A for analytics, M for media (advertising/marketing pixels), P for personalization. Single letters because they’re at the start of every tag name and visual real estate matters.GA4, Facebook, Floodlight, Bing, TikTok. A controlled vocabulary — every container uses the same name for the same vendor.§ safely).PageView, PerProduct, Acquisition, etc.M | Facebook | SectionGEPages | PageViewM | Floodlight | purchase § Acquisition | PerProductA | GA4 | DownloadFile | click_elementA | GA4 | purchase | enhanced_ecommerceP | DynamicYield | view_item | recommendationsThe first character drives consent rule assignment. GTM’s Consent Overview screen lets you bulk-apply consent settings across tags, and being able to filter or sort by the leading letter makes that bulk work fast: select all M | ... tags, apply the marketing consent rule, done. Select all A | ... tags, apply the analytics consent rule, done.
Every other field order loses this. GA4 | A | ... would scatter analytics tags across vendor groups. Facebook | M | ... would scatter media tags. Purpose-first is what makes consent assignment a sort-and-select operation instead of a tag-by-tag review.
Triggers have their own naming convention with § for variations. Tags embed the trigger name in the third field. Using | as the tag separator and § as the trigger separator means the two conventions don’t collide — M | Floodlight | purchase § Acquisition | PerProduct is unambiguous at both levels.
The practical payoff is that the trigger embedded in a tag name matches the trigger column in the GTM UI exactly. Scan down the tag list with the trigger column visible, and any mismatch between the tag name’s third field and the actual trigger column is a wiring bug — instantly visible. One separator everywhere would lose that property the moment a trigger name contained the same character as the tag separator.
Renaming an asset in GTM doesn’t break anything. References between tags, triggers, and variables are tracked by internal ID, not by name — change a variable from DLV - user_id to $user_id and every tag that references it keeps working. The dependency graph follows the rename automatically.
Once that’s clear, the migration is just a matter of order:
Variables first. Most numerous, lowest risk, and the foundation everything else references. Work through them in batches — either directly in the GTM UI, with the bulk-rename spreadsheet from the convention’s GitHub repo, or by editing the container JSON export with an LLM or by hand and re-importing.
Triggers next. Smaller in number than variables, and renaming them is safe for the same reason — tags reference triggers by ID, not name.
Tags last. By the time you reach tags, the variables and triggers embedded in their names already follow the convention, so each tag rename is a single concatenation step rather than a multi-asset rewrite.
Update descriptions as you go. Each rename is a chance to add the structured description the convention assumes — owner, purpose, source, end_date. The fastest moment to write a description is when you’re already looking at the asset.
| Asset | Category | Convention | Example |
|---|---|---|---|
| Variable | Raw data | $ + lowercase, mirror source | $ecommerce.items.0.item_name $_user_id |
| Variable | Function | camelCase, verb-first. Never get. Verb describes work beyond fetching | hasProductList isFirstPurchase calculateOrderTotal |
| Variable | Processed data | TitleCase, descriptive, concise | NormalizedOrderID SelectedCourseName ParentClassNames § upto10Levels |
| Variable | Constant | UPPERCASE. Exact value in name when possible | FB4534455 G-XXXXXXXXXX CONSTANT=1 |
| Trigger | Raw event | Exact dataLayer event name | add_to_cart purchase |
| Trigger | Filtered / specific | Base name + § Variation. § Except marks blocking triggers | purchase § Processed CampingCampaignPages § Except |
| Tag | All | Purpose | Vendor | Trigger | Detail. Purpose is single letter (A / M / P) | M | Facebook | SectionGEPages | PageView A | GA4 | DownloadFile | click_element |
| Description | All assets | YAML-style key-value pairs. Typical fields: owner, purpose, source, transform, returns, campaign, end_date | owner: D4D purpose: Hashed email for Meta CAPI end_date: 2026-06-30 |
Use § anywhere a name has variants — raw data, processed data, triggers, tag detail fields — to keep the canonical name searchable and surface duplicates in the same search result.
There’s no single “best” convention, but there are universally bad ones. The right convention treats names as one information channel among many — GTM also gives you the type column, the search box, the description field, and the references panel. The name should carry only what those other channels can’t: identity (what the asset is called) and category (what kind of value or behavior it represents). Everything else belongs in the channel built for it.
Use them all — but assign each casing a meaning. lowercase with $ prefix for raw dataLayer values. camelCase verb-first for functions that return computed values. TitleCase for processed data. UPPERCASE for constants. The casing tells the reader what kind of variable it is at a glance, without opening it or checking the type column.
GTM’s interface already shows variable type in a dedicated column, and the search box now reaches inside asset configurations. Both reasons make type prefixes pure cost — they duplicate information that’s already visible and add visual noise to every reference like {{DLV - user_id}}. Let the type column and the search box do their jobs; let the name describe what the value is.
GTM doesn’t allow variable names to start with underscore, but $_ works. So if your dataLayer pushes _user_id, name the variable $_user_id. The $ prefix is the universal raw-data marker; the underscore that follows preserves the source key exactly. The convention works for every dataLayer push without exception.
Use pipe (|) between top-level tag-name fields and section sign (§) within those fields when subdivision is needed. Pipe is visually clear and almost never appears in real data. § doesn’t appear in real data either, so it can safely be used inside trigger names that get embedded in tag names. The two separators stay distinct, so a trigger name with a § variation embedded inside a tag name never confuses the structure.
Structured key-value pairs, not prose. The fields you’ll typically want are owner (who maintains the asset), purpose (why it exists), campaign (what it’s attached to, or null for permanent infrastructure), and end_date (when it expires, or null). Optional fields fill in when relevant: source, transform, returns, depends_on, related. The format isn’t enforced — GTM’s description field is plain text — but the predictable structure unlocks cross-asset audits like “find every tag owned by Agency X” or “find every asset that expires this quarter.”
If GTM’s search can find it, don’t put it in the name. Search now indexes trigger references, event names, variable references inside tag fields, parameter values, and tag IDs — so encoding any of those into the name is dead weight. The name carries what search can’t infer: identity, category, and scope markers (§ variations, $ raw-data marker, casing as a typing system).
Renaming assets in GTM doesn’t break references — GTM tracks them by internal ID, not by name. So the migration is just a matter of order: variables first (most numerous, foundation everything else references), triggers next, tags last. Work in batches, either directly in the GTM UI or by editing the container JSON export. Update descriptions as you go — each rename is the cheapest moment to add the structured description fields.