6. Tags, Triggers & Variables
GTM's execution model in depth — per-message trigger evaluation, how click and visibility triggers really work, variable resolution timing, tag sequencing, firing options, and consent settings.
You already click these three things around the console daily. This chapter is about the machine underneath: when triggers evaluate, when variables resolve, what order tags run in — the questions that decide whether your purchase tag fires with the right value or fires at all.
The evaluation model
Everything GTM does is a reaction to one data layer message. For every push
(yours, or GTM's own gtm.js, gtm.click, …), the same cycle runs:
dataLayer.push(message)
│ 1. merge message into the internal model (Chapter 5)
▼
2. evaluate EVERY trigger's conditions against this message
│ — stateless predicates: matched or not, no memory
▼
3. for each matched trigger → fire its tags
│ — unless a blocking exception also matched
▼
4. each firing tag NOW resolves the variables it references
— lazily, at fire time, against the model as of this messageThree rules fall out of this, and they explain most confusing behavior:
- Triggers have no memory. A trigger isn't "active" or "armed"; it's a
condition checked per message. "Fire when
page_typeequalscheckout" matches messages on checkout pages, not "from checkout onward". - Variables resolve at fire time, per event. The same variable can return different values for two tags on two events — it's re-evaluated each time, against that event's snapshot.
- A tag fires per matched event — by default once per event; you can restrict to once per page (dedupe across repeat events — handy for base pixels) or allow unlimited (re-fire even within one event if multiple triggers match).
Triggers in depth
The page-lifecycle triggers (Consent Init → Init → Page View → DOM Ready → Window Loaded) were Chapter 4's territory. The interesting ones are the listener triggers — because GTM has to put real event listeners on the page to power them, and their quirks come from exactly how it does that.
Click triggers
Enable any click trigger and GTM attaches a document-level listener
(event delegation — one listener, catching every click that bubbles up). Each
click becomes an auto-event push — gtm.click (All Elements) or
gtm.linkClick (Just Links) — with the click's facts captured into built-in
variables: Click Element, Click Classes, Click ID, Click URL,
Click Text.
The quirks that follow from the mechanism:
Click Elementis the actual target — click the<span>inside your<a>and the span is what you get. Filtering by "Click Classes equalsbuy-button" misses every click that lands on a child element. The robust operator is "matches CSS selector" with a descendant-aware pattern:.buy-button, .buy-button *.- Just Links walks up the DOM to find the enclosing
<a>(solving the child-element problem for links) and offers "Wait for Tags" — it briefly intercepts navigation (up to a timeout) so tags can finish before the page unloads. Less critical now that GA4 uses beacons; still relevant for vendors that don't. - A click that an app handles with
stopPropagation()never bubbles to GTM's listener — it's invisible. Another point for app-pushed events.
Form Submit triggers
Same delegation idea, listening for submit events — which only standard,
browser-native form submissions emit. A React form that preventDefault()s
and sends fetch() fires no submit GTM can rely on. Treat the Form Submit
trigger as legacy-form-only; for anything modern, push a custom event from
the app's success handler — which also means you only track successful
submissions, not attempts. (Noticing the theme? Push > scrape, third time.)
The rest of the listener family
- Element Visibility — backed by
IntersectionObserver; firesgtm.elementVisibilitywhen an element (by ID or CSS selector) is on screen ≥ your percentage. Options: once per page / per element / every appearance. Great for "saw the pricing table" funnels. - Scroll Depth (
gtm.scrollDepth) — percentage or pixel thresholds. - History Change (
gtm.historyChange) — SPA route detection (Chapter 5's patch-not-fix). - Timer, YouTube Video, JavaScript Error — niche but occasionally perfect.
Custom Event triggers — the workhorse
Matches the event key of your own pushes (purchase,
virtual_page_view, …). Name matching is exact and case-sensitive, with an
opt-in "use regex matching". In a mature setup, the majority of meaningful
triggers are custom events fed by the application — the listener triggers
cover what the app can't (or won't) tell you.
Trigger groups and blocking exceptions
- A Trigger Group fires once after all member triggers have fired at least once on the page ("scrolled 50% and saw the demo video").
- Exceptions are blocking triggers attached to a tag: if both a firing trigger and an exception match the same event, the exception wins. Standard uses: block everything for internal traffic, block per consent state, block a noisy event on one path. Exceptions are how you say "always, except…" without rewriting firing conditions.
Variables in depth
A variable is a named lazy lookup — nothing executes until a tag, trigger, or another variable references it during an event.
Built-in variables (Page URL, Click Classes, Event, Container Version…)
must be explicitly enabled per container — an off-by-default catalogue. If
Click Text shows undefined in a condition, step one: is it enabled?
The user-defined types you'll actually use:
| Type | What it does | Watch out |
|---|---|---|
| Data Layer Variable | dot-path read from the model (v2) | set a default value for cleaner conditions |
| Custom JavaScript | anonymous function, returns a value | runs on every resolution; ships publicly in gtm.js (Ch. 4); keep pure, fast, defensive |
| JavaScript Variable | reads a global, e.g. window.shop.currency |
read-only path lookup — much cheaper than Custom JS |
| Lookup Table | input variable → output mapping | the workhorse for env switching (see below) |
| RegEx Table | like Lookup, with regex patterns | first match wins; capture groups usable in output |
| Constant | a single value with a name | use for every repeated ID (GA4 ID, pixel ID) |
| URL | parsed page URL components, incl. query keys | the standard gclid/fbclid reader |
| First-Party Cookie | reads a cookie by name | how tags read _fbp/_fbc (Ch. 13) |
| DOM Element | element text/attribute by ID or selector | scraping — last resort, breaks silently |
The pattern that shows the machinery nicely — one GA4 tag, environment-aware:
Lookup Table variable: {{GA4 Measurement ID}}
input: {{Page Hostname}}
├─ shop.example → G-LIVE1234
├─ staging.shop.example → G-STAGE567
└─ default → G-DEV0000Variables can reference variables (the lookup above consumes a built-in), and chains resolve recursively at fire time. The cost lives in Custom JavaScript: a slow function referenced by a busy trigger condition runs on every single message. Profile-by-deletion is a real debugging technique.
Tags in depth
A tag template is a parameterized code generator — fill in fields, GTM generates the vendor call. Three sources, in order of preference:
- Built-in templates (GA4, Ads, Floodlight…) — maintained by Google.
- Community Template Gallery (Meta, TikTok, LinkedIn, Cookiebot…) — sandboxed: templates run against a restricted API with a declared permissions model (what domains it may call, what cookies it may touch), reviewable before you add them.
- Custom HTML tags — arbitrary
<script>injected into the page. No sandbox, full page access, invisible to the permissions model. The escape hatch when no template exists — and the first thing to audit in an inherited container.{{Variables}}interpolate inside them, which is powerful and a classic XSS-shaped footgun if the variable carries user-controlled content.
Ordering: sequencing, priority, and groups
When one event fires several tags, they start in parallel-ish order with no completion guarantees. When order matters, the tools are:
- Tag sequencing — per tag, a setup tag (runs before) and a cleanup tag (runs after), with "don't fire if setup fails". The canonical case: vendor base/init code must precede the event call — e.g. a Meta base pixel as setup for the purchase event tag. Chains can nest (setup tags can have their own setup).
- Tag priority — a number; higher starts earlier within the event. It does not wait for completion, so it's a nudge, not a guarantee. Sequencing is the contract; priority is a hint.
- Trigger groups (above) order across events, sequencing orders within one event. Different problems.
Consent settings
Every tag carries consent metadata (Tag → Advanced → Consent Settings):
built-in consent checks — e.g. the GA4 tag declares it honors
analytics_storage, behaving differently when it's denied — plus additional
consent checks you can require ("don't fire at all without
ad_storage"). This is half of how Consent Mode works; the model, the
signals, and the legal side are Chapter 17.
One event, end to end
Tie it together with the purchase from Chapter 5, on a SPA:
app (checkout success handler)
└─ dataLayer.push({ event:"purchase", ecommerce:{...} })
│
├─ trigger [Custom Event: purchase] → matches
├─ trigger [Custom Event: regex .*] → (you don't have this)
└─ exception [DLV internal_traffic = true] → not matched, no block
│
├─ tag GA4 – Event – purchase
│ variables resolve now: {{DLV ecommerce.value}} = 138.0,
│ {{GA4 Measurement ID}} via lookup table → G-LIVE1234
│ → POST /g/collect en=purchase epn.value=138.0 …
│
└─ tag Meta – Event – Purchase
setup tag: Meta – Pixel – Base (once per page)
→ fbq('track','Purchase',{value:138.0,currency:'EUR'})
→ GET facebook.com/tr?ev=Purchase&fbp=…Every concept in this chapter is in that diagram: one message, stateless trigger matching, an exception that didn't block, fire-time variable resolution, a lookup table, sequencing with once-per-page dedupe.
Habits that separate clean containers from haunted ones
- Revenue events come from the app (custom event pushes). Listener triggers (clicks, visibility) are for behavioral analytics — never for the conversion that bills the client.
- Constants for every ID. A measurement ID pasted into nine tags is nine places to miss during a migration.
- Name triggers and variables like code:
CE – purchase,DLV – ecommerce.value,LT – GA4 ID by hostname. The prefix tells the type at a glance in any dropdown. - Enable built-ins sparingly, delete dead weight — everything in the
container ships in
gtm.js(Chapter 4). - When a tag misbehaves, walk the evaluation model: which message? did the trigger match it? what did variables resolve to on that event? That's exactly the workflow Preview mode automates — Chapter 8.
First, though: the bread-and-butter platform setups, done properly once — Chapter 7 — The Standard Setups.