GTM Variables: Built-in, Constants, and User-Defined

If triggers decide when a tag fires, variables decide what data it works with. Learn GTM’s built-in, constant, and user-defined variables, and exactly when to reach for each one.

If triggers decide when a tag fires, variables decide what data it works with. They are the values flowing through your container: the URL of the page, the text of a clicked link, the revenue of a purchase, the measurement ID a tag sends to. Every condition you write and every parameter you populate reads a variable, which makes them the quiet foundation everything else stands on. GTM splits them into two families, and understanding the line between them is most of the battle.

Built-in variables are pre-packaged data captures that ship with GTM. You do not define them; you switch them on, and they populate automatically with the right value the moment a trigger evaluates. User-defined variables are the ones you create yourself, from a simple stored constant to a function that transforms data on the fly. The built-ins handle the common cases so you do not have to reinvent them; the user-defined ones cover everything else. This article walks all of it, from the checkbox-simple built-ins through to the Custom JavaScript that does real work.

Built-in Variables: Data Capture by Checkbox

The appeal of built-in variables is that they require zero code. When a user clicks a link, GTM automatically fills Click URLwith the link’s href, Click Text with its visible text, and Click Classes with its class attribute, all before your trigger even evaluates. You did not write any of that capture logic; Google maintains it, and it behaves consistently across every site. A click trigger that needs the destination URL just reads Click URL and moves on.

By default GTM enables only a small subset, the page-related ones. The rest you turn on under Variables, then Built-in Variables, then Configure, where a checklist of available captures waits. Tick a box and the variable is instantly usable everywhere: in trigger conditions, in tag fields as {{Click URL}}, and in Preview mode. The one discipline worth keeping is restraint. Enabling built-ins you never use does no functional harm, but it clutters the Preview panel and makes audits noisier, so enable what you actually reference and leave the rest off.

The built-ins come in categories that mirror the kinds of events you track. The Pages group is always on and the most used: Page URL (the full address with query string), Page Hostname (the domain alone), Page Path (just the path), Page Path + Query, and Referrer. These drive most trigger filtering, and the recurring advice is to filter on Page Path rather than Page URLfor path logic, because a stray query parameter on the full URL can break an otherwise correct match.

The Utilities group holds container-level values, the most important being Event, which returns the name of the current dataLayer event (gtm.clickpurchasegtm.dom). This is the variable that Custom Event triggers actually match against under the hood. Others include Container IDContainer VersionRandom Number, and Environment Name.

The Clicks group only populates during click events: Click Element (the DOM node itself), Click ClassesClick IDClick TargetClick URL, and Click Text. A canonical use is catching PDF downloads with a regex condition on Click URL:

Click URL matches RegEx (ignore case) \.pdf(\?.*)?$

That pattern matches .pdf at the end of the URL with an optional trailing query string, and the ignore-case flag catches .PDFtoo.

The Forms group mirrors the clicks group for form submissions: Form ElementForm ClassesForm IDForm TargetForm URL. A newsletter signup is as simple as Form ID equals newsletter-signup. But there is a critical caveat: these variables only populate on standard HTML form submissions. Modern sites that submit forms via JavaScript or AJAX never fire them, and for those you need a custom event pushed to the dataLayer instead. This is the single most common reason a form trigger “just doesn’t work.”

The remaining groups are event-specific. Videos covers YouTube embeds with Video StatusVideo PercentVideo Title, and friends (Vimeo and HTML5 need a custom approach). Scrolling gives you Scroll Depth ThresholdScroll Depth Units, and Scroll DirectionVisibility provides Percent Visible and On-Screen Duration for element-visibility tracking. History exposes the History API state for single-page apps, the foundation for the SPA tracking approach covered separately. And Errors surfaces Error MessageError URL, and Error Line, which let you forward JavaScript errors to GA4 for monitoring.

The Constant: The Simplest User-Defined Variable

The moment you step into user-defined territory, the gentlest first step is the Constant, which stores a fixed string you reference by name instead of pasting the literal value everywhere. It is the classic programming idea of a named constant replacing a magic string, ported straight into GTM.

The value becomes obvious the first time an ID changes. Picture a container with your GA4 measurement ID typed directly into a dozen tags. Migration day arrives, the property changes, and now you are editing twelve tags by hand and praying you did not typo or miss one. With a constant, the ID lives in exactly one place:

Variable Type: Constant
Value: G-ABCDE12345
Name: Const - GA4 Measurement ID

Every tag references {{Const - GA4 Measurement ID}}, and migration becomes a single edit followed by a publish. The decision rule is simple: if the same literal string would appear in two or more places in your container, make it a constant. Measurement IDs, pixel IDs, conversion IDs, partner IDs, a single-currency code, custom dimension indices, all qualify. One-off values used in a single tag do not; for those a constant just adds noise. The Const - name prefix is worth adopting so constants cluster together in the variable list.

Constants have one property that sets them apart from every other variable type: their value is known at edit time, not fire time. Every other variable resolves to something only when a trigger or tag actually runs. That distinction powers a genuinely useful pattern. A pure constant holds one value, but real sites often need different IDs across production, staging, and development. Pair constants with a Lookup Table and the right one gets chosen automatically:

Variable Type: Lookup Table
Input Variable: {{Page Hostname}}
Lookup Table:
www.example.com → {{Const - GA4 ID Production}}
staging.example.com → {{Const - GA4 ID Staging}}
localhost → {{Const - GA4 ID Development}}
Default Value: {{Const - GA4 ID Development}}
Name: LT - GA4 Measurement ID

Now a single tag references {{LT - GA4 Measurement ID}} and sends to the correct property per environment, with no duplicate tags and no risk of staging hits polluting production. Defaulting to the development ID rather than production means an unrecognized hostname fails safe.

User-Defined Variables: The Full Kitchen

If built-ins are pre-cooked meals, user-defined variables are the kitchen, where you build whatever the situation needs from the raw page state: the dataLayer, JavaScript globals, cookies, the URL, rendered HTML, or computed logic. They all share one behavior worth stating clearly: a variable is evaluated at fire time, so whatever the page state happens to be at the moment a trigger checks a condition or a tag reads {{Variable}} determines the value.

A consistent naming prefix turns the variable list into a navigable index, so DLV - for Data Layer Variables, JS - for Custom JavaScript, URL - for URL components, Cookie - for cookies, LT - and RT - for Lookup and Regex Tables, DOM - for DOM Element variables. A glance at the name tells you the type.

The Data Layer Variable is the most-used of all, reading any value the site pushes using dot notation. For a purchase you might read ecommerce.transaction_id or ecommerce.value, and for nested arrays the syntax uses dots rather than brackets, so ecommerce.items.0.item_name reaches the first item’s name. Always use the Version 2 API; V1 is legacy. The default value is what comes back when the path does not exist, and leaving it blank yields undefined. One practical note: for a full items array feeding GA4 ecommerce, just reference ecommerce.items directly, because GA4’s tag template already knows how to handle the array, an approach detailed in the enhanced ecommerce guide.

The Custom JavaScript Variable is the Swiss Army knife: an anonymous function that returns a value, used whenever you need to transform or compute something built-ins cannot:

function() {
var price = {{DLV - Product Price}};
if (typeof price !== 'number') return undefined;
return Math.round(price * 1.2 * 100) / 100; // add 20% tax, round to 2dp
}

The wrapper must be an anonymous function, GTM substitutes referenced variables at fire time, and the guard against an invalid value is not optional politeness; it prevents the tag from sending garbage. The golden rule here is to always return undefined for a value you cannot compute, never null0, or an empty string, because GTM treats undefined as cleanly “missing” while the others get recorded literally. Wrapping risky logic in try/catch and returning undefined from the catch keeps a single error from silently killing the tag.

The remaining types each read one kind of source. The JavaScript Variable reads a named global like myApp.user.idwithout running a function, cleaner than Custom JS for a simple read, though a dataLayer value should always be read with a Data Layer Variable instead. The URL Variable parses a piece of window.location, so you can pull utm_source out of the query string or grab just the hostname or path without writing parsing code, though it returns one query parameter at a time. The Cookie Variable reads a first-party cookie such as _ga or _fbp, with URI-decoding usually left on, though it cannot see HttpOnly cookies because those are invisible to JavaScript entirely.

The DOM Element Variable reads a value straight from rendered HTML via a CSS selector, useful when data lives only on the page and not in the dataLayer. It comes with two warnings. It fires only after the DOM is ready, so an element rendered later by lazy-loading returns undefined unless you pair it with a DOM Ready or later trigger. And it is fragile by nature: scraping a price or SKU from the DOM means a single CSS class rename breaks your tracking, so pushing the value to the dataLayer is almost always the better answer.

Finally, two mapping types replace long conditional chains. The Lookup Table maps an input to an output by exact match, ideal for grouping discrete values like country codes into regions, and editable by non-developers. The Regex Table does the same with pattern matching, perfect for classifying page paths into section names, with one rule that bites people: rows evaluate top to bottom and the first match wins, so the most specific patterns must come first.

Choosing the Right Type

The decision almost always follows the data source. A fixed value you type becomes a Constant. Data the application controls belongs in the dataLayer and is read with a Data Layer Variable. Something in the URL uses a URL Variable, something in a cookie uses a Cookie Variable, and a JavaScript global uses a JavaScript Variable. Data that exists only in rendered HTML forces a DOM Element variable, though that should prompt you to ask the developers to push it to the dataLayer instead. When you need to map values you reach for a Lookup or Regex Table, and when you need to genuinely compute or transform something, Custom JavaScript.

The overarching pattern is that built-ins get you most of the way, and when you hit their ceiling you wrap a built-in inside a user-defined variable to extend it. Extracting the file extension from a clicked URL is a good example:

function() {
var url = {{Click URL}};
if (!url) return undefined;
var match = url.match(/\.([a-z0-9]+)(\?|$)/i);
return match ? match[1].toLowerCase() : undefined;
}

The built-in Click URL provides the raw value; the Custom JavaScript transforms it into something the built-in alone could not give you.

A Full Walkthrough: Purchase Tracking

Tying the pieces together, here is end-to-end ecommerce tracking built on user-defined variables. The site pushes a structured purchase to the dataLayer, clearing the previous ecommerce object first:

window.dataLayer = window.dataLayer || [];
dataLayer.push({ ecommerce: null });
dataLayer.push({
event: 'purchase',
ecommerce: {
transaction_id: 'T-12045',
value: 299.99,
currency: 'USD',
items: [
{ item_id: 'SKU-RS-01', item_name: 'Trail Running Shoes', price: 150, quantity: 1 },
{ item_id: 'SKU-YM-02', item_name: 'Yoga Mat', price: 149.99, quantity: 1 }
]
}
});

You then create Data Layer Variables for ecommerce.transaction_idecommerce.valueecommerce.currency, and ecommerce.items, plus a constant for the measurement ID. A Custom Event trigger listens for the purchase event, and a GA4 Event tag maps the variables into parameters:

Tag Type: Google Analytics: GA4 Event
Event Name: purchase
Event Parameters:
transaction_id → {{DLV - ecommerce.transaction_id}}
value → {{DLV - ecommerce.value}}
currency → {{DLV - ecommerce.currency}}
items → {{DLV - ecommerce.items}}
Triggering: CE - purchase

Then you verify in Preview mode that the event fires the tag, that every Data Layer Variable shows a populated value in the tag’s data panel, and that GA4’s DebugView and Realtime ecommerce report both confirm the transaction arriving with correct parameters.

The Pitfalls Worth Memorizing

A handful of failure modes account for most variable bugs. A Data Layer Variable returning undefined almost always means the dataLayer push happened after the trigger evaluated, or the path is misspelled; confirm the timing and the exact spelling. Custom JavaScript that returns null instead of undefined makes GA4 record the literal string "null", and Custom JavaScript that throws an uncaught error silently kills the tag, which is why defensive guards and try/catch matter. A DOM Element variable that fires before its element renders returns nothing, so it needs a late-enough trigger or, better, a dataLayer push. Lookup Tables miss on case differences, so us falls through to the default when the table has US. Regex Tables return the wrong category when a general pattern is listed before a specific one. And a Click URL filter built around a hardcoded domain misclassifies subdomain clicks; filtering with does not contain {{Page Hostname}} keeps it dynamic.

The throughline across all of them is the same discipline that makes the rest of GTM reliable: test every new variable in Preview mode’s Variables tab, where the live value for the current event is shown, before you publish.

The Short Version

GTM variables come in two families. Built-in variables are capture-by-checkbox values for pages, clicks, forms, video, scroll, visibility, history, and errors; enable only the ones you use. User-defined variables cover everything else, anchored by three workhorses: the Constant for fixed IDs (paired with a Lookup Table when you need per-environment switching), the Data Layer Variable for application data, and Custom JavaScript for transformations, always returning undefined for missing values. Choose the type by where the data lives, prefer the dataLayer over fragile DOM scraping, name everything by type prefix, and verify each variable in Preview before publishing. Get the variables right and triggers and tags become almost trivial, because by the time they run, the right data is already sitting exactly where they expect it.

View Comments (1)

Leave a Reply

Subscribe to My Newsletter

Subscribe to my email newsletter to get the latest posts delivered right to your email. Pure inspiration, zero spam.

Discover more from Discuss Data Science, Machine Learning and Analytics

Subscribe now to keep reading and get access to the full archive.

Continue reading