1 · What a data layer is
A data layer is one JavaScript object — usually called digitalData,
dataLayer, or adobeDataLayer — that exposes the meaning of a page
in a form that any analytics, personalisation, or advertising tag can read.
window.digitalData = {
page: {
name: "Product Detail: ASTRO-3000 Telescope",
type: "product",
category: "telescopes",
language: "en-US"
},
user: {
loginStatus: "logged-in",
customerId: "c_847221",
segment: "premium"
},
product: [{
sku: "ASTRO-3000",
name: "Astro 3000 Telescope",
category: "telescopes",
price: 499.00,
inStock: true
}]
};
That is the entire idea. A single, well-named object, populated by the application before any tag fires, so that the tag's job is reduced to copying values into Adobe variables.
2 · Why it exists
Without a data layer, your tag manager ends up scraping the DOM:
// please do not do this
const productName = document.querySelector('h1.product-title').innerText;
const price = document.querySelector('.price-now').innerText.replace('$', '');
const inStock = document.querySelector('.add-to-cart').disabled === false;
Every one of those selectors is a time bomb. A designer renames a CSS class and your analytics
silently stops working. A new template ships and prices are suddenly reported as "$499.00"
instead of 499. The analytics team and the front-end team are now in a permanent
state of low-grade conflict.
A data layer ends this. The application makes a promise — "I will put the product price at
digitalData.product[0].price as a number" — and the tag manager makes a promise — "I
will only read from there, never from your HTML." Both teams can now refactor independently.
Think of the data layer as the standardised shipping container of the analytics world. The application packs whatever it wants into the container. The tag manager doesn't care what's inside, only that the container's shape is fixed. As long as both ends agree on the shape, the contents can change without coordination.
3 · The three shapes you will encounter
3.1 The flat object
window.digitalData = {
pageName: "Home",
pageType: "home",
userStatus: "anonymous",
cartValue: 0
};
Simplest possible. Works for small sites. Breaks down once you have multi-valued data (a list of products, multiple search facets).
3.2 The nested object — CEDDL-flavoured
The W3C Customer Experience Digital Data Layer spec from 2013. Not officially adopted, but every major analytics consultancy uses something resembling it.
window.digitalData = {
page: {
pageInfo: { pageID: "abc123", pageName: "Home", destinationURL: "https://…" },
category: { primaryCategory: "home", subCategory: "" },
attributes: { country: "US", language: "en" }
},
user: [{
profile: [{
profileInfo: { profileID: "u_001", returningStatus: "new" },
address: { country: "US" }
}]
}],
product: [],
cart: { price: { basePrice: 0, currency: "USD" }, item: [] }
};
Yes, user is an array. Yes, user[0].profile is also an array. The
CEDDL spec allows multiple users (e.g. a parent buying for a child) and multiple profiles per
user. In practice most teams use index [0] and pretend the arrays aren't there. The
deep nesting is intentional — it gives a stable home for every piece of context — but it does make
the Launch data elements verbose.
3.3 The event-driven layer — ACDL
Modern. Instead of a static object that gets re-written on each page, the application pushes events into the layer and listeners pick them up.
window.adobeDataLayer = window.adobeDataLayer || [];
// On page load
adobeDataLayer.push({
event: "page loaded",
page: { name: "Home", type: "home" }
});
// Later, when the cart changes
adobeDataLayer.push({
event: "cart updated",
cart: { value: 49.99, itemCount: 1 }
});
This is the Adobe Client Data Layer (ACDL), Adobe's modern recommendation. We will cover its mechanics in section 5.
4 · CEDDL in detail
If you inherit an existing implementation, it is overwhelmingly likely to look like CEDDL. So you must be able to read it fluently.
The standard top-level keys
| Key | Holds |
|---|---|
pageInstanceID | A unique ID for this page render. |
page | Page-scoped metadata — name, type, category, language. |
product (array) | Products displayed or interacted with. |
cart | Cart contents and totals. |
transaction | Order data on the confirmation page. |
event (array) | Events fired on this page (search, video play, error). |
component (array) | Reusable components present on the page. |
user (array) | Visitor identity & profile. |
privacy | Consent flags. |
version | The data layer schema version. Track this. |
The pattern in practice
The application populates digitalData before the tag manager loads. This is
the rule. If the tag manager loads first, it reads an empty object and your tracking is wrong.
<head>
<!-- 1. Build the data layer -->
<script>
window.digitalData = {
page: { pageInfo: { pageName: "Home" } },
user: [{ profile: [{ profileInfo: { returningStatus: "new" } }] }]
};
</script>
<!-- 2. Load the tag manager -->
<script src="https://assets.adobedtm.com/.../launch-EN12345.min.js"></script>
</head>
In Launch you then build a data element for each value you care about:
// Data Element "Page Name" — Custom Code
return digitalData?.page?.pageInfo?.pageName || "";
And your tracking rule sets the AppMeasurement variable from the data element. No DOM scraping. No fragile selectors. If the data layer changes shape, you fix it in one place.
5 · Adobe Client Data Layer (the modern way)
ACDL is a small open-source library that gives you an event bus on top of a data layer. The application pushes events; consumers subscribe; the library handles the bookkeeping.
Setting up
<script>
window.adobeDataLayer = window.adobeDataLayer || [];
</script>
<script src="https://cdn.adobe.io/adobe-client-data-layer/2.0.0/adobe-client-data-layer.min.js"></script>
Pushing data and events
// Set state — no event, just an update
adobeDataLayer.push({
user: { loginStatus: "logged-in", segment: "premium" }
});
// Fire an event — anything subscribed to it runs
adobeDataLayer.push({
event: "cm:product viewed",
eventInfo: { path: "product" },
product: { sku: "ASTRO-3000", name: "Astro 3000", price: 499.00 }
});
Subscribing
adobeDataLayer.addEventListener("cm:product viewed", function(event) {
// event === the object that was pushed
s.eVar4 = event.product.sku;
s.eVar5 = event.product.name;
s.events = "prodView";
s.linkTrackVars = "events,eVar4,eVar5";
s.linkTrackEvents = "prodView";
s.tl(true, "o", "Product View");
});
The ACDL extension for Launch wraps this for you, so in the Launch UI you can simply create rules with the event "Adobe Client Data Layer — Event" and pick the event name from a dropdown.
ACDL replays events to listeners that subscribe late. If the application pushed
page loaded at t=200ms and Launch subscribed at t=900ms, the
listener still fires. This solves the eternal race-condition problem.
6 · Design checklist
Before you commit a data layer to a release, run this checklist. Each item is the result of someone, somewhere, painfully learning a lesson.
Naming
- One name to rule them all. Pick
digitalDataoradobeDataLayerand never have both. Two data layers is two sources of truth, which is zero sources of truth. - Lower-case keys. JavaScript is case-sensitive and humans are not.
pageNameandpagenamewill cohabit a real codebase until you stop them. - Spell out abbreviations.
productCategory, notprodCat.
Types
- Numbers as numbers.
price: 499.00, notprice: "$499.00". - Booleans as booleans.
inStock: true, not"yes". - Dates as ISO 8601.
"2026-05-19T10:30:00Z", never a regional format. - Empty means absent. Use
nullor omit the key. Never put the string"unknown"or"n/a"in a value field.
Timing
- Populated before tag manager loads. Inline
<script>ahead of the Launch embed code. - For SPAs, push events on route change. Module 07 covers this.
- Asynchronous values (auth, geo) push when ready. Don't block page render to wait for them — push an event when they resolve and let the listeners react.
Scope
- Page-scoped values reset on each page. Don't let stale cart data from page A appear on page B.
- Session-scoped values persist. User identity, AB test variant, consent.
- Document what is which. Future-you needs this.
Versioning
- Include a
versionfield. When you change the schema, bump it. Subscribers can branch on version.
7 · When the developers say no
You will spend a non-trivial amount of your career trying to get engineering teams to populate a data layer that they do not consider their problem. Three approaches, in order of how much you should prefer them:
Approach 1 — Make it cheap
Don't ask for a data layer. Ask for their existing internal data — the props they pass
to their components, the JSON they fetch from their API — to be exposed as a global. If the page
already has a window.__INITIAL_STATE__, that is your data layer. Hide your scraping
inside Launch data elements and present them with a one-line ask.
Approach 2 — Build it together
Sit with a senior front-end engineer and design the schema in their language. Use TypeScript interfaces if they like types. Get them to own the data layer as a piece of public API, the way they own their API contracts. They will protect it once it's theirs.
Approach 3 — Resort to selectors
You will sometimes have to. When you do, write the selectors as a shim in Launch's custom-code data elements, not as ad-hoc code inside rules. Centralise the fragility. When the CSS class names change you fix one file, not eighteen.
8 · Practice
Open the Data Layer Builder lab. You will design a data layer for a fictional e-commerce site (the AstroCart example we'll use throughout the course). The lab generates the JSON for you and shows how a Launch data element would consume each field.
Sketch the data layer for the three most important page types on a site you know: the home page, a product page, and a confirmation page. What's the same on all three? What changes? When you've written it down, compare against the CEDDL spec in section 4 — and notice how often you converge.