GTM variables are named placeholders that Google Tag Manager fills with a value at the moment an event happens: the page URL, the clicked link, a product price from the data layer, a cookie, a constant like your GA4 measurement ID. Built-in variables come ready-made and you just switch them on. User-defined variables are the ones you create yourself, and the Data Layer Variable is the one you'll use most.

Tags decide what to send. Triggers decide when. Variables decide what's actually in the payload.

That's why a broken variable rarely breaks anything you can see. The tag still fires. The trigger still matches. And GA4 happily records undefined, or worse, a value left over from an earlier step.

Key Takeaways
  • Built-in variables (Page URL, Click URL, Video Percent) are switched on in Variables > Configure. User-defined ones are created in Variables > New.
  • Variables aren't calculated once per page. GTM evaluates them for each event that needs them, so the same variable can hold different values on gtm.js and on a click.
  • The Data Layer Variable reads GTM's merged data model, not just the latest push. A missing key returns the previous value if one exists. That's the source of half the revenue bugs I find.
  • Lookup and RegEx Tables replace stacks of near-identical tags. Always set a default value on purpose.
  • Name variables by type and purpose (DLV - ecommerce.value, CONST - GA4 ID). In a container with 140 variables, names are your only documentation.

Built-in vs user-defined: the two halves

Built-in variables are predefined by Google. You don't configure them, you just enable them in Variables > Built-In Variables > Configure. A fresh web container has five on: Page URL, Page Hostname, Page Path, Referrer and Event. The rest sit in groups you switch on when needed:

  • Pages: Page URL, Page Hostname, Page Path, Referrer.
  • Utilities: Event, Container ID, Container Version, Environment Name, Random Number, HTML ID, plus the newer Analytics Client ID and Analytics Session ID.
  • Clicks: Click Element, Click Classes, Click ID, Click Target, Click URL, Click Text.
  • Forms, History, Errors, Scroll, Visibility and Videos (Video Percent, Video Title, Video Status and five more).

Most built-ins simply read a gtm.* key that a trigger pushed. Click URL reads gtm.elementUrl. Video Percent reads gtm.videoPercent.

No click trigger, no Click URL. That's why a Click URL used in a page view tag is always empty.

User-defined variables live in Variables > User-Defined Variables > New > Variable Configuration. This is where the real work happens. You read the data layer, map values, split URLs, write small functions. Google groups them in five sets: Navigation, Page Variables, Page Elements, Utilities, Container Data.

The GTM variable types, at a glance

Here's the cheat sheet I keep open during audits. Not every type, but every type I see in real containers.

Variable type What it reads Typical use Watch out for
Data Layer Variable A key in GTM's data model ecommerce.value, transaction_id, user_type Stale values from earlier pushes, wrong version.
Constant A fixed string GA4 measurement ID, Ads conversion ID Hardcoding the same ID in 12 tags instead.
Lookup Table Exact match on another variable Hostname to measurement ID, path to page type No default value, so unmatched input returns undefined.
RegEx Table Pattern match, top to bottom Grouping URLs into sections First match wins, order matters.
URL A part of a URL Query parameter, hostname of Click URL Source defaults to Page URL.
1st-Party Cookie document.cookie by name Consent cookie, A/B test bucket Can't read HttpOnly cookies.
DOM Element Text or attribute of an element Product name in an h1, data- attributes Element not in the DOM yet at Container Load.
JavaScript Variable A global JS variable navigator.language, document.title Global renamed in a site release.
Custom JavaScript Return value of a function Small transforms, closest() lookups Side effects, runs many times per page.
Google tag: Configuration settings Config-level parameters Shared settings for all Google tags Changing it changes every tag that uses it.
Google tag: Event settings Event-level parameters page_type, logged_in on every GA4 event Parameters you forgot it was adding.

The Data Layer Variable, and why its version matters

You'll create more Data Layer Variables than anything else. Variables > New > Data Layer Variable, type the key name, done. Except for one field that most people never touch: Data Layer Version.

Version 2 (the default) treats dots as nesting. ecommerce.value means "the value key inside the ecommerce object". You can reach into arrays too: ecommerce.items.0.item_id gives the first item's ID.

Version 1 treats dots as part of the key name. A push of {"a.b.c": "value"} stays a single key literally called a.b.c. I've seen exactly one legitimate reason to use it in the last five years, on a legacy site whose developers pushed keys with dots in their names. If your container has Version 1 variables you didn't choose deliberately, they're probably reading nothing.

The behavior that bites people is how Version 2 remembers. GTM doesn't read "the last push". It keeps a merged data model: every push gets merged into what's already there. If a key isn't in the current push, the variable returns whatever an earlier push left behind.

Damian runs an online supplement store, roughly 2,600 orders a month. His purchase event used DLV - ecommerce.value.

After a checkout redesign, the developers moved the order total to a new key, ecommerce.order_total, and stopped sending value. Nothing errored. The variable kept returning the value from the last add_to_cart push on the page, the price of one product, somewhere between 60 and 90 PLN.

For six weeks GA4 revenue was about 41,000 PLN under the shop backend. The tag fired on every order. DebugView showed a value on every purchase. It was just the wrong number.

Two fixes, both cheap:

  1. Push { ecommerce: null } before every ecommerce event, as Google's own GA4 ecommerce examples do. It wipes the old object from the data model.
  2. Reconcile GA4 revenue with the backend weekly. It's the only test that catches a plausible wrong number.

The GA4 data layer and GTM guide covers the push side of this in detail.

Lookup and RegEx Tables: fewer tags, one decision

A Lookup Table takes an input variable and maps exact values to outputs. The classic: send staging traffic to a test property.

When {{Page Hostname}} equals Set {{LT - GA4 measurement ID}} to
shop.example.com G-PROD12345
staging.example.com G-TEST67890
(default value) G-TEST67890

That default row is the whole point.

Justyna's furniture store in Łódź had this exact table, but the default was set to the production ID "so nothing gets lost".

Then the dev agency spun up preview-482.example.com for a redesign and ran automated checkout tests against it. In March, 3,400 extra sessions and 212 test purchases landed in her production GA4. The table did exactly what she told it to.

I'll be blunt: the default value of a Lookup Table should be the safe value, never the production one. Unknown input means you don't know where the traffic comes from. Treat it as test data.

A RegEx Table does the same with regular expressions. Patterns run top to bottom and the first match wins. By default, patterns must match the full input and ignore case.

Under Advanced Settings you can turn off Full Matches Only. You can also turn on Enable Capture Groups and Replace Functionality and use $1 in the output.

Google's own docs warn that capture groups with full matching off can return odd results. They're right. I keep full matching on and write .* where I need it.

Custom JavaScript, DOM, cookie and the rest

Custom JavaScript must be an anonymous function that returns a value:

function() {
          var path = {{Page Path}} || '';
          if (path.indexOf('/blog/') === 0) return 'blog';
          if (path.indexOf('/product/') === 0) return 'product';
          return 'other';
        }
        

Two rules. First, no side effects: no dataLayer.push(), no DOM changes, no cookies written. GTM can evaluate the same variable many times on one page, once for each event that references it, so a push inside a variable creates a loop of extra events.

Second, keep it short. Every line counts toward the container size limit of 200 KB, and a 40-line function for a page type is a RegEx Table in disguise.

DOM Element takes a selection method (ID or CSS Selector) and an optional attribute name. Without the attribute, it returns the element's text. It reads the DOM when the event happens, so on Container Load (gtm.js) the element might not exist yet. Use it on DOM Ready or later.

1st-Party Cookie reads a cookie by name, with an option to URI-decode it. If two cookies share the name, it returns the first. It can't see HttpOnly cookies, because no JavaScript can.

JavaScript Variable reads a global by its path, like navigator.language. Not code, just a path. Fine for values a platform puts on the page. Fragile when a theme update renames the global.

Constant is the most underrated type. One CONST - GA4 measurement ID used everywhere means one place to change when you migrate properties.

Google tag: Configuration settings and Google tag: Event settings hold groups of parameters you reuse. The first goes into Google tags, the second into GA4 Event tags. They're great for page_type or logged_in on every event. Just document them, because a parameter quietly added through a settings variable is invisible when you open a single tag.

When variables are evaluated (and what undefined really means)

This is the part almost nobody explains. GTM doesn't compute variables at page load and store them. It evaluates a variable when something needs it: a trigger checks a condition, or a firing tag reads a field. Each event in Preview (Container Loaded, DOM Ready, Link Click, your custom events) has its own Variables tab with that moment's values.

So {{Click Text}} is empty on Container Loaded and filled on Link Click. {{DLV - user_type}} might be undefined on gtm.js and "logged_in" three events later, after the site pushed it. If your tag fires on All Pages, it reads the early value.

undefined usually means one of three things:

  • The key was never pushed, or was pushed after the event your tag fires on.
  • The key name doesn't match. transactionId and transaction_id are different keys. Case matters.
  • The variable reads the wrong source, like a URL variable still pointed at Page URL instead of Click URL.

Two settings help. Set Default Value on Data Layer Variables and tables gives a fallback. Format Value (in every variable's settings) can convert case and replace undefined, null, true or false with your own text. I use "Convert undefined to" not_set for parameters I want to see as gaps in reports, rather than silently missing.

A naming convention that survives 140 variables

The containers I audit hold anywhere from 60 to 150 user-defined variables. Without a convention, you get price, Price 2, dl price NEW and price (do not use). All four active.

My format: TYPE - what it holds.

  • DLV - ecommerce.value (the key path, exactly).
  • CONST - GA4 measurement ID.
  • LT - hostname to GA4 ID.
  • RT - page path to page type.
  • URL - Click Hostname, URL - query utm_campaign.
  • CJS - link location.
  • COOKIE - cmp consent.
  • DOM - product h1.
  • GT EVENT - common params.

It sorts by type. It tells you where the value comes from. And a junior can read a tag without opening six variables.

Unused variables go. My GTM audit checklist has the cleanup steps. Stale variables that feed live tags are one of the 5 GTM mistakes that silently cost conversions.

Want to see these in a real setup? The guide on how to track link clicks in GA4 with GTM uses Click URL, a URL variable for the clicked host and a small Custom JavaScript variable, step by step.

Frequently asked questions

What's the difference between built-in and user-defined variables in GTM? Built-in ones come from Google. You only switch them on in Variables > Configure. User-defined ones you build yourself, like a Data Layer Variable for transaction_id or a Constant for your measurement ID.

Why does my Data Layer Variable return undefined? The key was never pushed, was pushed after the event your tag fires on, or its name doesn't match exactly, including case. Open Preview, select the event your tag fires on, and check the Data Layer tab for the key at that moment.

Should I use Data Layer Version 1 or 2? Version 2, the default, in almost every case. It reads dots as nesting, so ecommerce.value works. Version 1 treats dots as literal characters in the key name and only fits legacy setups that push keys like "a.b.c".

When should I use a Lookup Table instead of a RegEx Table? Use a Lookup Table when inputs are exact, known values like hostnames. Use a RegEx Table when you're matching patterns, like grouping thousands of URLs into page types. A RegEx Table stops at the first match, so order matters there. Both need a deliberate default value.

Can a Custom JavaScript variable push to the data layer? It can, and it shouldn't. GTM may run the variable once for every event that uses it, so a push inside it creates extra events. Keep Custom JavaScript variables pure: read, transform, return.

How many variables can a GTM container have? There's no fixed count I'd plan around. The real limit is the 200 KB container size, and your own ability to read the thing. Delete unused ones at every audit.

Next steps

Open your container's Variables screen and sort the user-defined list by name. If you can't tell what half of them hold without opening each one, that's your first job: rename to TYPE - purpose, and delete what nothing references.

Then pick your three most important tags (purchase, lead, sign-up) and check each variable they use in Preview, on the exact event they fire on. Not on Container Loaded. On the real event. That's where undefined and stale values hide.

Don't want to do it by hand? The free GTM audit flags the broken and unused variables it can see in your container. For containers that change every sprint, the GTM monitoring retainer watches them continuously, so a renamed data layer key doesn't cost you six weeks of revenue data.


Sources and further reading - User-defined variable types for web (Tag Manager Help) - Built-in variables for web containers (Tag Manager Help) - The data layer (Google for Developers)

How many of your GTM variables return undefined right now?

The free GTM audit lists unused, broken and duplicated variables in your container, and flags the ones feeding wrong values into GA4 and ad tags.

Start Free GTM Audit
Piotr Litwa

Piotr Litwa

GTM & Analytics Specialist

Piotr monitors GTM containers and GA4 properties for businesses across Europe, catching tracking issues before they cost you conversions. 120+ clients, 10+ years, zero outsourcing.