Skip to content

Number Variables

Number Variables

CoreCategory22 Variables

Numbers in a GTM container are almost always money, scores, or gates on what reaches your tags. One day a backend deploy pushes prices in cents instead of euros. Your conversion values are suddenly 100× too high. Smart Bidding learns from it. GA4 won’t let you delete the events. Number is the category that catches this class of bug before it ships — price parsing, currency conversion, tax extraction, outlier clamping, lead scoring. The math GTM forgets, and the math that protects your ad spend.

22 variables100% sandbox-safeWeb + ServerMIT licensed

Numbers in a GTM container serve three distinct jobs. Each has its own failure modes, its own query clusters, its own Custom JavaScript patterns that everyone reinvents. The Number category covers all three.

Money hygiene

Making currency values the shape ad platforms want

Parsing prices from DOM strings, converting currencies, extracting tax, rounding to two decimals without breaking numeric type. Every e-commerce container does this; most do it with copy-pasted Custom JS.

Data quality gates

Guarding against bad values before they reach tags

Clamping, range validation, invalid-value defaulting. The failure mode is catastrophic: wrong currency units or malformed parses produce permanent damage in GA4 and Smart Bidding that can’t be undone.

Scoring

Building numeric signals from behavioral inputs

Lead scoring, engagement scoring, custom conversion values. Send the quality of a lead to Google Ads — not just its existence — and Smart Bidding optimizes for leads that close.

The three domains share more than you’d expect. A scoring pipeline ends in money when the score is used as a conversion value. A money pipeline needs data quality gates to catch unit errors. Data quality checks run on both money and scores. The variables compose across the whole category.

Numbers arrive in the dataLayer in every shape except the one you want:

  • As strings. “129.00” instead of 129. “$12,000.00” with currency symbols and thousand separators. “12.000,00” in European locales where the comma is the decimal.
  • In the wrong unit. Cents when you expected euros, or basis points when you expected percentages. Silent until someone notices the reports are off by 100×.
  • With tax included. The revenue field typically includes VAT or sales tax, but affiliate networks and some ad platforms want the ex-tax value.
  • With the wrong precision. Floats that need toFixed(2) — which returns a string, which breaks the next calculation.
  • As invalid values. null, undefined, NaN, Infinity. A malformed parse propagates through every downstream variable.
  • As outliers. A test order for $9,999,999 that someone forgot to exclude. A refund pushed as a positive value.

Every one of these is a silent failure in GTM. No error, no warning — just bad numbers flowing into your tags and then into permanent reports.

Twenty-three variables across three hero groups and supporting primitives. Each links to its reference page with full config details.

The DOM hands you “$12,000.00”. The dataLayer pushes “12.000,00 €”. Some backend stringifies its prices for JSON safety and sends “129.00”. Every ad platform wants a raw number. Most tutorials solve this with parseFloat plus a regex hack that fails silently on the first European-locale value it meets.

Recipe — Parse any price string to a number
Variable: parseCurrency · Direct or Apply mode
FieldValue
Input{{DL - revenue}}
Localeauto (detect thousand/decimal separators) or en-US, de-DE, etc.
“$12,000.00” becomes 12000, “12.000,00 €” also becomes 12000

Unparseable input returns undefined rather than NaN, consistent with the library’s silent-skip pattern. Chain defaultIfInvalid downstream to substitute a fallback when the parse fails.

Most dataLayer implementations put the total revenue — inclusive of VAT or sales tax — into ecommerce.purchase.actionField.revenue. That’s the right value for GA4 and Google Ads. It’s the wrong value for most affiliate networks, some offline conversion uploads, and any reporting where you need to compare like-for-like with ex-tax P&L numbers. TradeTracker’s documentation explicitly requires the ex-tax amount.

Recipe — Revenue minus tax
Variable: excludeTax · Direct or Apply mode
FieldValue
Total (incl. tax){{DL - ecommerce.purchase.actionField.revenue}}
Tax amount{{DL - ecommerce.purchase.actionField.tax}}
129.00 − 21.50 = 107.50

When your dataLayer pushes a tax rate instead of a tax amount (e.g. 0.20 for 20% VAT), configure the variable to divide instead: total / (1 + rate). Both modes are safe on zero-tax inputs and on malformed inputs — chain defaultIfInvalid downstream if you want a specific fallback value.

Round money values without losing the numeric type

Section titled “Round money values without losing the numeric type”

JavaScript’s native toFixed has a famous footgun: it returns a string, not a number. Chain it before any further arithmetic and you get string concatenation where you expected addition. In a GTM container, this means your value parameter on a conversion event arrives at the tag as “178.00” — a string — and some tag implementations accept it, some reject it, some silently convert it with locale-dependent parsing that breaks in European locales.

Recipe — Round to 2 decimals, keep as number
Variable: toFixedNumber · Direct or Apply mode
FieldValue
Input{{Apply - sumProductOfProperties}}
Decimals2
178.456789 becomes 178.46, still a number

Use this as the final step of any money pipeline that feeds a tag’s value field. Native toFixed is fine if the output is purely for display; if any tag, variable, or calculation downstream treats the result as a number, use toFixedNumber.

Multi-currency sites face a specific problem: GA4 has one reporting currency per property. Events with different currencies either need conversion before sending, or they pollute your reports. The classic pattern is fetching exchange rates into the dataLayer (typically via a Custom HTML tag on page load) and then applying them to revenue values. convertCurrency replaces the Custom JavaScript variable that every tutorial walks you through.

Recipe — Convert AUD to EUR using a rates object
Variable: convertCurrency · Direct or Apply mode
FieldValue
Amount{{DL - revenue}}
From currency{{DL - currency}} (e.g. “AUD”)
To currencyEUR
Rates object{{DLV - rates}}
Fallback behaviorpassthrough (return input unchanged if rate missing)
100 AUD with rate 1.6467 becomes 60.73 EUR

The passthrough fallback is the safer default: if rates haven’t loaded yet due to an async race condition, the original amount flows through rather than collapsing to zero. Pair with a Logic-category gate on rates availability if you need hard guarantees before tag firing.

Catch dataLayer unit drift before it hits Smart Bidding

Section titled “Catch dataLayer unit drift before it hits Smart Bidding”

This is the most important section on this page. Read it even if nothing else on it applies to you.

What happens when a purchase event has the wrong value

A backend deploy changes the currency unit in your dataLayer from euros to cents. ecommerce.value now pushes 12900 instead of 129.00. Your GA4 purchase events all report values 100× too high. Your Google Ads conversion_value uploads feed Smart Bidding values 100× too high.

You cannot undo this. GA4 does not support deleting specific events — only full property resets. Your revenue report is permanently contaminated for the period. Smart Bidding has learned from the inflated values and is now bidding aggressively on the wrong signal. The fix takes weeks of relearning.

This is not hypothetical. It happens on every multi-team e-commerce site, usually at least once.

The defense is a gate. Before a value reaches a conversion tag, clamp it to a range of plausibility. If the clamp ever triggers, you’ve caught a unit-drift bug before it shipped.

Recipe — Clamp purchase value to a sane range
Variable: clamp · Direct or Apply mode
FieldValue
Input{{DL - ecommerce.value}}
Minimum0 (no negative purchases)
Maximum10000 (highest plausible single-order value for your business)
A value of 12900 gets clamped to 10000, which is obviously wrong in the report and triggers an investigation before Smart Bidding learns from it

The trick is choosing a maximum that’s clearly too low for real outliers but catches the typical unit-drift mistakes (×100 or ×1000 multipliers). A fashion retailer with a normal AOV of €80 might clamp at 1000. A B2B SaaS with enterprise contracts might clamp at 100000. Pick a threshold an order of magnitude above your real maximum.

An even stricter pattern uses isInRange with a blocking trigger: if the value falls outside the sane range, the conversion tag doesn’t fire at all, and a separate alerting tag fires instead. You lose the one bad event rather than letting it contaminate your data:

isInRangeDirectTrigger conditionmust be trueConversion tag fires

Why not validate in the backend? You should. Container-side validation is defense in depth — the backend is where unit errors happen, so backend validation alone has a blind spot for its own bugs. A GTM-layer sanity check catches what gets through.

Beyond unit drift, numeric values routinely arrive as NaN (from a malformed parse), null (from a missing field), or undefined (from a race condition). Any of these reaches a tag unmodified and either breaks the tag, breaks the report, or both. Two variables handle the defensive layer.

Recipe — Substitute a default when input is invalid
Variable: defaultIfInvalid · Direct or Apply mode
FieldValue
Input{{Apply - parseCurrency}}
Fallback value0
Treat as invalidNaN, null, undefined, Infinity
Any invalid input becomes 0; valid numbers pass through unchanged

Use isFinite in trigger conditions when you want to block a tag from firing entirely rather than substitute a value. For refund-handling use cases where the dataLayer can push negative values, chain abs to normalize — or deliberately don’t, if your destination platform treats refunds as negative conversion values.

Not all leads are equal. A form submission from someone who visited 8 product pages and watched a pricing video is worth more to Google Ads’ Smart Bidding than a form submission from a user who landed on the contact page and submitted immediately. If you send both to Google Ads as value: 1, the algorithm can’t distinguish them. If you send them as value: {computed score}, the algorithm optimizes for high-intent leads.

🧩 WEIGHTED is the scoring primitive. Multiple condition-value pairs, summed when the condition evaluates true. One variable replaces a chain of IF ELSE IF blocks in Custom JavaScript.

Recipe — Lead score from behavior signals
Variable: 🧩 WEIGHTED · Direct or Apply mode
ConditionWeight
{{pages_viewed}} > 510
{{pricing_page_visited}}15
{{video_watched}}8
{{session_duration_s}} > 1805
{{returning_visitor}}3
Score between 0 and 41, higher = hotter lead

The typical pipeline wraps the score: cap it with clamp, bucket it with classifyByThreshold, and send the value (or the bucket) as a Google Ads conversion value or GA4 custom parameter.

🧩 WEIGHTEDApplyclampApplyscaleToRangeDirect

This is the pattern that turns a container-level score into a Smart Bidding signal. Without the scoring layer, your conversion events are indistinguishable; with it, you’re optimizing for quality, not volume.

Bucketing turns a continuous number into a named category — critical for analytics segmentation in GA4 custom dimensions, where string tiers (“high_value”) are much more useful than raw values (142.50) for reports and audiences.

Recipe — Cart value tier for GA4 custom dimension
Variable: classifyByThreshold · Direct or Apply mode
FieldValue
Input{{DL - ecommerce.value}}
Thresholds[50, 100, 500]
Labels[“under_50”, “50_to_100”, “100_to_500”, “over_500”]
127.50 becomes “100_to_500”

Use the labels as values for custom dimensions, audience definitions, or content grouping. classifyByThreshold is the clean alternative to a ladder of IF ELSE IF statements in a Custom JavaScript variable — and when thresholds change for a new campaign, you update one config table instead of hunting through code.

Sometimes a score or value needs to fit a specific range that a destination expects. A lead score of 0-41 might need to become 0-100 for a scoring dashboard, or 1-10 for an ad-platform quality signal. A session duration in seconds (0-3600) might need to become 0-1 for a normalized weight. scaleToRange handles the linear mapping.

Recipe — Map a lead score to a 0-100 scale
Variable: scaleToRange · Direct or Apply mode
FieldValue
Input{{Apply - WEIGHTED}}
Source range[0, 41]
Target range[0, 100]
A raw score of 20 becomes 48.78 on the 0-100 scale

The linear mapping is (value - sourceMin) / (sourceMax - sourceMin) × (targetMax - targetMin) + targetMin. Values outside the source range map outside the target range unless you clamp first — which is almost always what you want for scoring inputs.

Three operations that don’t fit a hero category but come up often enough to deserve dedicated templates.

Format a number for display

formatNumber produces a human-readable string with locale-aware thousand separators and decimals. Use it for GA4 custom dimension values that will appear in reports, for event labels, and for any analytics output where the display matters. Different from toFixedNumber (which returns a number for further math) — this one returns a string for display.

Sum, min, max of an array

sum, min, max operate on flat arrays of numbers. Different from the Items category’s sumProperty — those work on arrays of objects; these work on arrays of raw numbers. Useful when the output of a pluckProperty or a split is a flat array you want to aggregate.

Random integer

randomInteger in a specified range. Use cases: generating event IDs for deduplication between GA4 and Meta CAPI, A/B test bucket assignment, sampling rates. Everyone writes Math.floor(Math.random() * n) in a Custom JavaScript variable; this replaces it.

Four chains that come up over and over in real containers:

Safe revenue pipeline

From raw dataLayer price to a validated, rounded, tag-ready value. Catches malformed input, unit drift, and precision bugs in one chain.

parseCurrencyApplyclampApplytoFixedNumberDirect

Ex-tax revenue for affiliate networks

Total revenue to ex-tax amount, rounded and type-safe.

excludeTaxApplytoFixedNumberDirect

Multi-currency revenue

Foreign-currency purchase converted to the reporting currency with safe fallback and rounding.

convertCurrencyApplydefaultIfInvalidApplytoFixedNumberDirect

Lead quality conversion value

Behavioral signals to a Smart Bidding conversion value. The most ambitious chain in the category — it turns a container-level scoring system into ad-platform optimization input.

🧩 WEIGHTEDApplyclampApplyscaleToRangeDirect

Every tutorial on price parsing, currency conversion, revenue calculation, or lead scoring arrives at a variation of the same pattern:

function() {
var raw = {{DLV - revenue}};
if (!raw) return 0;
var parsed = parseFloat(String(raw).replace(/[^0-9.-]/g, ”));
if (isNaN(parsed)) return 0;
if (parsed < 0 || parsed > 10000) return 0;
return Number(parsed.toFixed(2));
}

Then another Custom JavaScript for tax extraction. Then another for currency conversion. Then another for lead scoring. Every piece of logic lives only in the code; the variable name is typically cjs - rev and nobody remembers why the 10000 cap is there or whether the regex handles European decimals correctly.

Named templates replace the whole pattern:

  • Intent in the name. parseCurrency means exactly one thing. clamp means exactly one thing. cjs - rev means whatever the code inside happens to do today.
  • Locale handling is encoded once. When you discover your regex doesn’t handle “12.000,00 €”, you fix parseCurrency once. Every container that imports it picks up the fix.
  • Gates are visible. A clamp variable in the chain tells the next analyst there’s a sanity check. A hardcoded if (parsed > 10000) return 0 buried in Custom JavaScript tells them nothing until they read the code.
  • Server-side portability. sGTM runs in a stricter sandbox than the web container. Named templates transfer; ad-hoc Custom JavaScript often doesn’t.
GoalVariableMode
Parse “$12,000.00” or “12.000,00 €” to numberparseCurrencyDirect / Apply
Convert between currenciesconvertCurrencyDirect / Apply
Revenue minus taxexcludeTaxDirect / Apply
Round to N decimals, keep as numbertoFixedNumberDirect / Apply
Percentage (discount, tax, margin)percentOfDirect / Apply
Force value into [min, max]clampDirect / Apply
Boolean: is in range?isInRangeDirect
Boolean: is finite number?isFiniteDirect
Fallback on invalid inputdefaultIfInvalidDirect / Apply
Absolute valueabsDirect / Apply
Weighted sum of conditions🧩 WEIGHTEDDirect / Apply
Bucket value into named tierclassifyByThresholdDirect / Apply
Map value from one range to anotherscaleToRangeDirect / Apply
Type coercion to number / integertoNumber / toIntegerDirect / Apply
Round nearest / up / downround / ceil / floorDirect / Apply
Sum / min / max of arraysum / min / maxDirect
Format number for displayformatNumberDirect / Apply
Random integer in rangerandomIntegerDirect

How do I parse a price string into a number in GTM?

parseCurrency takes a string like “$12,000.00” or “12.000,00 €” and returns the numeric value, handling locale-specific thousands and decimal separators. Unparseable input returns undefined rather than NaN, so downstream variables handle the failure cleanly.

How do I calculate revenue excluding tax?

excludeTax takes a total (tax-inclusive) and either a tax amount or tax rate, and returns the tax-exclusive value. Required by TradeTracker, many affiliate networks, and any offline conversion upload that expects pre-tax values.

How do I prevent wrong dataLayer values from reaching Google Ads?

Use clamp or isInRange to gate values before they reach a conversion tag. clamp forces any value into a [min, max] range. isInRange returns a boolean for use in trigger conditions. Pair with defaultIfInvalid to catch wrong currency units, malformed parses, and catastrophic outliers that would contaminate Smart Bidding.

Why does toFixed break my GTM calculations?

JavaScript’s native toFixed returns a string, not a number. Chain it before further arithmetic and you get string concatenation instead of addition. toFixedNumber rounds to N decimals and returns a number, preserving numeric type through the chain.

How do I compute a lead score in GTM?

🧩 WEIGHTED takes condition-weight pairs and returns the sum of weights where the condition is true. Combined with clamp to cap the maximum and classifyByThreshold to tier the result, it produces a complete lead scoring pipeline that runs entirely in the container.

Can I send a computed score as the value on a conversion event?

Yes — this is one of the most valuable patterns in the category. Compute the score with 🧩 WEIGHTED, optionally normalize with scaleToRange, and pass as the value parameter on a Google Ads or GA4 conversion event. Smart Bidding optimizes for high-quality leads instead of lead volume.

Are these variables sandbox-safe in server-side GTM?

Yes. All templates run inside GTM’s sandboxed JavaScript environment using only permissioned APIs. Money-hygiene variables are particularly valuable in sGTM where you reshape incoming event payloads before forwarding to destinations.