Items Variables
Your dataLayer pushes product data in one shape. GA4 wants it one way, Meta another, Google Ads a third, TikTok a fourth. Items is the adaptation layer — 12 sandbox-safe variable templates that reshape any array of product objects into whatever the next tag actually needs. No Custom JavaScript, no .forEach() loops, no container rot.
On this page
- Why this category exists
- Your input probably looks like one of these
- The variables — at a glance
- Calculate cart value (price × quantity)
- Extract a flat array of product IDs
- Transform items to Meta Pixel
contents - Send items to TikTok Events API
- Prepare items for Google Ads dynamic remarketing
- Map items to the Pinterest Tag
- Migrate UA products to GA4 items
- Filter items by category, brand, or price
- Deduplicate items in the cart
- Fill missing fields with defaults
- Strip fields you don’t want to send
- Normalize a single property
- Composed patterns — chaining variables
- Why you don’t need Custom JavaScript
- Cheatsheet
- FAQ
Why this category exists
Section titled “Why this category exists”Every ecommerce site pushes product data into the dataLayer in one shape — whatever shape the backend happens to produce. GA4, Google Ads, Meta, TikTok, Pinterest, and every other ad platform want it in a different shape, with different key names, different types, different structures, different required fields. That mismatch is the single most common cause of broken ecommerce tracking.
The Items category is 12 variables that do one job: reshape arrays of product objects. Compute totals from them, extract flat lists from them, rename their keys, drop fields, fill defaults, filter by condition, deduplicate them. One input, many outputs. One source of truth, as many destination shapes as you need.
Here’s what each platform actually expects on an add_to_cart event — four platforms, zero overlap in key names:
item_iditem_namepricequantitycontent_idscontentscontent_typevalueidgoogle_business_verticalecomm_prodidecomm_totalvaluecontent_idcontent_typecontents[]valueThe job of your GTM container isn’t to store this data — your dataLayer does that. The job is to adapt it on the fly, per tag, per destination. That’s what these variables do.
Your input probably looks like one of these
Section titled “Your input probably looks like one of these”The Items variables don’t care where your product data comes from. They operate on any array of objects with named properties. In the wild, that array shows up in three dominant shapes — plus a long tail of platform-specific variants.
GA4 native
items: [
{
item_id: “SKU_001”,
item_name: “Sweater”,
price: 89,
quantity: 2
}
]Enhanced Ecommerce
products: [
{
id: “SKU_001”,
name: “Sweater”,
price: 89,
quantity: 2
}
]Custom (Shopify, etc.)
cart_products: [
{
sku: “SKU_001”,
title: “Sweater”,
unit_price: 89,
qty: 2
}
]All three work. The same variables — sumProductOfProperties, pluckProperty, ⚡ ITEMS › Transformer — run against any of them. Only the property names you reference change. This is the point of operating at the structural level rather than the schema level: the adapter doesn’t care about vocabulary, only about shape.
Where examples in this guide use GA4 keys (item_id, price, quantity), substitute your own. The recipes don’t change.
The variables — at a glance
Section titled “The variables — at a glance”Twelve variables, grouped by the job they do. Each links to its reference page with full config details.
Calculate cart value (price × quantity)
Section titled “Calculate cart value (price × quantity)”The value parameter of every GA4 ecommerce event — add_to_cart, begin_checkout, purchase — expects the total monetary value of the transaction. Google’s guidance is explicit: value should reflect price multiplied by quantity, summed across all items. Most implementations either hard-code it at the dataLayer push (fragile when the dataLayer is built from CMS fragments) or reach for a Custom JavaScript variable.
| Field | Value |
|---|---|
| Input Array | {{DL - ecommerce.items}} |
| Property 1 | price |
| Property 2 | quantity |
| Output Function | {{Apply - toFixed(2)}} |
178.00 for [{price: 89, quantity: 2}]Non-numeric values are ignored silently, so a missing or malformed price won’t throw — it just doesn’t contribute. Chain itemAssignDefaults upstream if you want explicit defaults instead of silent skips.
Common mistake. Using sumProperty with price looks right but ignores quantity entirely. A cart of one $50 item with quantity 3 returns $50, not $150. Use sumProductOfProperties for any real cart value.
Extract a flat array of product IDs
Section titled “Extract a flat array of product IDs”A large share of ad-network integrations come down to one operation: give me an array of the product IDs in this event. Meta wants content_ids. Google Ads wants ecomm_prodid. TikTok wants content_id. Pinterest wants product_ids. Same data, four different keys.
| Field | Value |
|---|---|
| Input Array | {{DL - ecommerce.items}} |
| Property | item_id |
[“SKU_001”, “SKU_002”, “SKU_003”]When IDs need to be strings (and when they need to be numbers)
Section titled “When IDs need to be strings (and when they need to be numbers)”Meta accepts both numeric and string IDs but warns that inconsistent types between pixel events and the catalog feed cause match failures. Google Ads dynamic remarketing is stricter: the id must match the product feed’s format exactly, which is usually a string. If your dataLayer sometimes pushes numeric item_id values, normalize them before plucking — chain itemMapProperty with toString upstream.
Transform items to Meta Pixel contents
Section titled “Transform items to Meta Pixel contents”The Meta Pixel’s AddToCart, ViewContent, and Purchase events accept a contents parameter — an array of { id, quantity, item_price } objects. This is the format Meta uses for catalog matching in Advantage+ campaigns, so getting it right materially affects ad delivery.
Three things happen at once: item_id becomes id, price becomes item_price, and quantity must be an integer. One variable handles all three.
| Source | Output key | Type |
|---|---|---|
item_id | id | String |
price | item_price | Number |
quantity | quantity | Integer |
[{id: “SKU_001”, item_price: 89, quantity: 2}]Pair this with pluckProperty on item_id for the sibling content_ids parameter, and sumProductOfProperties for value. One dataLayer push, three variables, complete Meta payload.
Send items to TikTok Events API
Section titled “Send items to TikTok Events API”TikTok’s Pixel and Events API accept a contents array structurally similar to Meta’s — { content_id, content_type, content_name, price, quantity } — with one meaningful difference: content_type must be present on every item, not just the event. It’s either "product" or "product_group".
| Source | Output key | Type |
|---|---|---|
item_id | content_id | String |
item_name | content_name | String |
price | price | Number |
quantity | quantity | Integer |
This two-step pattern — Transformer for rename + type, itemSet for static injection — is the canonical shape for any network that demands a fixed per-item field your dataLayer doesn’t natively carry.
Prepare items for Google Ads dynamic remarketing
Section titled “Prepare items for Google Ads dynamic remarketing”Google Ads dynamic remarketing is the strictest major platform because it joins to your Merchant Center product feed. The id on each item must match the id in your feed exactly, character-for-character, or match rate collapses and dynamic creatives show the wrong products. The Google Ads tag also expects google_business_vertical on every item — typically "retail", but "hotel", "flights", "education" per business type.
| Source | Output key | Type |
|---|---|---|
item_id | id | String |
price | price | Number |
quantity | quantity | Integer |
The legacy ecomm_prodid / ecomm_totalvalue / ecomm_pagetype parameters used by older Google Ads remarketing tags are derived from the same items array:
ecomm_prodid→pluckPropertyonitem_idecomm_totalvalue→sumProductOfPropertiesofprice × quantityecomm_pagetype→ event-derived, typically alookupValueon the GTM event name
Feed consistency is non-negotiable. If your Merchant Center feed uses "shopify_US_1234_567" as id, sending "1234" from your dataLayer shows up as 0% match rate in the Data Manager. Transformer’s String type coercion gives you format stability; itemMapProperty with a format template gives you composed IDs when the feed uses them.
Map items to the Pinterest Tag
Section titled “Map items to the Pinterest Tag”The Pinterest Tag expects a product_ids flat string array and a line_items array of { product_id, product_name, product_price, product_quantity, product_category, product_brand } objects. Every key prefixed with product_ — a convention unique to Pinterest.
| Source | Output key | Type |
|---|---|---|
item_id | product_id | String |
item_name | product_name | String |
price | product_price | Number |
quantity | product_quantity | Integer |
item_category | product_category | String |
item_brand | product_brand | String |
For the sibling product_ids parameter, a separate pluckProperty on item_id is cleaner than trying to derive it from the transformed array.
Migrate UA products to GA4 items
Section titled “Migrate UA products to GA4 items”Legacy containers still push Enhanced Ecommerce-style data — ecommerce.purchase.products or ecommerce.add.products with fields like id, name, category, brand, variant, price, quantity. Converting to GA4 shape is the most common first task when modernizing a tracking stack, and the transformation is mechanical: rename, retype, fix the wrapper path.
| Source | Output key | Type |
|---|---|---|
id | item_id | String |
name | item_name | String |
category | item_category | String |
brand | item_brand | String |
variant | item_variant | String |
price | price | Number |
quantity | quantity | Integer |
Running this in every GA4 tag that still reads from the legacy dataLayer buys migration time without requiring engineering to redesign the dataLayer synchronously.
Filter items by category, brand, or price
Section titled “Filter items by category, brand, or price”Two classic scenarios need item-level filtering. The first is category-specific pixels — a performance campaign that only tracks apparel, or a category exclusion pixel that needs only “Gifts.” The second is financial: excluding items below a threshold, dropping “shipping” or “fees” pseudo-items that some dataLayers inject as line items.
| Field | Value |
|---|---|
| Input Array | {{DL - ecommerce.items}} |
| Property | item_category |
| Mode | equals |
| Value | Apparel |
Chain the filtered array into sumProductOfProperties (Apply → Direct) to compute category-specific revenue for reporting, or use it as the items source of a dedicated pixel tag with a filtered scope. The six filter modes — equals, not equals, contains, regex, truthy, falsy — cover virtually every dataLayer variation you’ll encounter.
Deduplicate items in the cart
Section titled “Deduplicate items in the cart”Some ecommerce platforms push duplicate item objects when a user rapidly clicks “add to cart” on the same SKU, or when a single-page-app re-renders state. Purchase events end up with item_id: "SKU_001" appearing three times — once with quantity: 1, once with quantity: 2, once again with quantity: 1. Revenue on that array is wrong.
| Field | Value |
|---|---|
| Input Array | {{DL - ecommerce.items}} |
| Property | item_id |
First occurrence wins by default. If your dataLayer pushes the correct total quantity on the last occurrence, reverse the array first with arrReverse before deduping — keeps only the last push per SKU.
Fill missing fields with defaults
Section titled “Fill missing fields with defaults”Real-world dataLayers are inconsistent. Quantity missing on some single-item events. Price zero for free samples. item_brand present only for some product types. Ad networks don’t forgive missing fields gracefully — Meta reports empty content_ids, Google Ads shows 0% match, Pinterest silently drops the event.
The defensive pattern is one upstream normalization variable that guarantees a known shape before anything downstream touches the array.
| Key | Default |
|---|---|
quantity | 1 |
price | 0 |
item_brand | ”Unknown” |
Place this at the start of every item-processing chain. Costs nothing when the dataLayer is already clean, saves the entire pipeline when it isn’t.
Strip fields you don’t want to send
Section titled “Strip fields you don’t want to send”Ad networks routinely reject item payloads with unrecognized keys, or — worse — silently accept them and use them against their matching logic. GA4-native fields like item_list_id, index, affiliation, item_list_name, coupon are meaningful to GA4 and noise to everyone else.
| Field | Value |
|---|---|
| Input Array | {{DL - ecommerce.items}} |
| Properties to omit | item_list_id, item_list_name, index, affiliation, coupon |
In practice most pipelines use Transformer with Keep unmapped: OFF instead, which achieves the same result by allowlist. Use itemOmit when you want to keep the original field set minus a few — a denylist pattern — rather than enumerate everything to keep.
Normalize a single property
Section titled “Normalize a single property”Sometimes the transformation is surgical: uppercase every item_brand for a case-sensitive feed match, strip leading zeroes from item_id, prefix every SKU with a market code, or force price to always be a number even when the dataLayer pushes strings.
| Field | Value |
|---|---|
| Input Array | {{DL - ecommerce.items}} |
| Property | item_id |
| Map Function | {{Apply - toString}} |
When two or more properties need transforming, Transformer is almost always the better answer: one variable, one mapping table, clear intent. itemMapProperty is the right tool when the transformation is one property, one function, no renaming.
Composed patterns — chaining variables
Section titled “Composed patterns — chaining variables”The framework’s real power shows up when variables compose. Because every Items variable supports Apply mode, the output of one becomes the input of the next — no intermediate GTM variables, no Custom JavaScript glue. Three patterns come up over and over:
Defensive cart value
Section titled “Defensive cart value”Fill missing quantities and prices, then compute. Works even when the dataLayer is unreliable.
Category-specific revenue
Section titled “Category-specific revenue”Revenue for just one product line — useful for category-level goal tracking in GA4 or category-specific conversion values in Google Ads.
Clean, deduped Meta payload
Section titled “Clean, deduped Meta payload”Remove duplicate items before reshaping. The Transformer output goes straight into the Meta tag’s contents field; a parallel pluckProperty feeds content_ids.
These chains are why you’d use a composable framework instead of one-off Custom JavaScript. Every chain is readable end-to-end from the variable list; nobody has to open code to understand what the container is doing.
Why you don’t need Custom JavaScript
Section titled “Why you don’t need Custom JavaScript”Every competing tutorial on calculating cart value, sending items to Meta, or building Google Ads remarketing arrays reaches for the same pattern:
function() { var contents = []; {{DLV - ecommerce.items}}.forEach(function(el) { contents.push({ id: el.item_id, quantity: el.quantity }); }); return contents;}It works. It also creates three problems that compound across a container:
- Audit opacity. The next analyst has to read every Custom JavaScript variable line by line. Across 20 ad networks and 8 ecommerce events, no one fully understands the container.
- Sandbox mismatch in sGTM. Server-side Google Tag Manager runs in a far stricter sandbox; many ad-hoc Custom JavaScript patterns from web containers simply don’t transfer.
- Duplication. “Extract item_id as a string array” is the same transformation on every site. No reason to rewrite it each time.
Named templates like pluckProperty and sumProductOfProperties carry intent in their name. An audit becomes a scan, not an investigation. The container stays legible when the analyst who built it is gone.
Cheatsheet
Section titled “Cheatsheet”| Goal | Variable |
|---|---|
Total cart value (price × quantity) | sumProductOfProperties |
| Sum one property (no quantity factor) | sumProperty |
| Sum nested property path | sumPropertyNested |
| Flat array of one property (IDs, names) | pluckProperty |
| Rename + retype + default multiple fields | ⚡ ITEMS › Transformer |
| Transform one property across all items | itemMapProperty |
| Filter items by a property condition | itemFilterByProperty |
| Remove duplicate items | itemUniqByProperty |
| Inject a static property onto every item | itemSet |
| Fill missing properties with defaults | itemAssignDefaults |
| Remove specific properties from every item | itemOmit |
| Split a delimited string property into an array | itemSplitProperty |
Can I calculate cart value in GTM without Custom JavaScript?
Yes. sumProductOfProperties multiplies two numeric properties (typically price and quantity) across every item in the array and returns the sum. It runs inside GTM’s sandboxed JavaScript environment — no Custom JavaScript needed.
What’s the difference between sumProperty and sumProductOfProperties?
sumProperty adds one property across items: price + price + price. sumProductOfProperties multiplies two properties per item then sums the results: (price × quantity), summed. For any real cart value, use sumProductOfProperties — sumProperty ignores quantity.
Do these variables work with non-GA4 dataLayers?
Yes. They operate on any array of objects with named properties. Whether your dataLayer uses GA4 shape, legacy Enhanced Ecommerce, or a custom platform-specific shape, the same variables apply — only the property names change.
Do these variables work in server-side GTM?
Each variable is marked for Web, Server, or both on its individual page. The Items category is primarily web-container oriented, but most transformation variables have server equivalents for outbound payload shaping.
Are these templates sandbox-safe?
Yes. All templates run inside GTM’s sandboxed JavaScript environment using only the permissioned APIs exposed to custom templates. They pass GTM’s built-in permission checks without modifications.
How do I chain multiple Items variables together?
Most Items variables have an Apply mode that returns a reusable function rather than a final value. You chain these templates together by feeding that function into another variable’s Input Function field (to transform the array before processing) or into its Output Function field (to transform the result after processing). For example, using itemAssignDefaults (Apply) in the Input Function of sumProductOfProperties (Direct) fills missing quantities before the total is summed.
Why not just use a Custom JavaScript variable?
Custom JavaScript works but creates audit opacity — the next analyst has to read code to understand intent. Named templates like pluckProperty carry meaning in the name. Containers become scannable instead of investigative.