Integrate Tealium with Optimizely Web Experimentation
TL;DR
Tealium is a customer data platform built around two surfaces: Tealium iQ Tag Management, the client-side tag manager that loads vendor tags and exposes the utag JavaScript API, and Tealium EventStream / AudienceStream, the server-side CDP that ingests events, builds visitor profiles, and forwards data to downstream connectors. Integrating Optimizely Web Experimentation with Tealium means pushing each experiment decision into Tealium's data layer so the variation a visitor saw travels with every event Tealium collects — and from there to any analytics, advertising, or warehouse destination you have wired up in Tealium.
This guide covers two integration directions and is careful to keep them separate. Optimizely publishes several official Tealium integrations, but every one of them moves data the other way — Tealium iQ loading the Optimizely snippet, Tealium AudienceStream audiences targeting Optimizely experiments, and the Tealium Optimizely Events Connector forwarding Tealium events into Optimizely's Event API. None of those send an Optimizely decision out to Tealium. To do that — to label Tealium's data with the experiment and variation a visitor received — you build a Custom Analytics Integration that calls utag.link() from Optimizely's track_layer_decision callback. This guide documents the official inbound integrations briefly for context, then covers the custom outbound integration in full.
How the Integration Works
When Optimizely buckets a visitor into an experiment or personalization campaign, it fires a decision callback. The custom integration captures this callback and pushes the experiment context into Tealium two ways: it writes the experiment and variation onto the utag_data data layer object so any subsequently loaded tag can read it, and it fires a utag.link() event carrying the experiment metadata. Tealium iQ then distributes that event to whichever vendor tags and EventStream connectors you have configured, and the variation property is available as a mapped attribute downstream.
flowchart LR
A[Visitor lands on page] --> B[Optimizely makes bucketing decision]
B --> C[track_layer_decision callback fires]
C --> D["utag_data updated with experiment + variation"]
C --> E["utag.link tealium_event = optimizely_decision"]
D --> F[Data layer available to all tags]
E --> G[Tealium iQ tag distribution]
G --> H[Vendor tags: GA4, Floodlight, etc.]
G --> I[Tealium EventStream / AudienceStream]
I --> J[Connectors: warehouse, ad platforms, CDP]
Tealium's value in this integration is that it is a distribution layer, not a destination. You instrument the experiment decision once, in Optimizely, and Tealium fans it out to every tool you have connected — so a single custom integration labels your entire downstream stack with variation data instead of integrating Optimizely separately with each vendor.
Custom Integration Event Properties
The following properties are sent with the utag.link() call and written to the data layer in the custom integration:
Property | Type | Description |
|---|---|---|
| string | Reserved Tealium key identifying the interaction; set to |
| string | The campaign (layer) ID in Optimizely |
| string | The experiment ID within the campaign |
| string | Human-readable experiment name |
| string | The assigned variation ID |
| string | Human-readable variation name or |
| boolean | Whether the visitor is in the holdback group |
The optimizely_experiment_name/optimizely_variation_name pair is also written onto utag_data so tags that read the data layer at load time (rather than listening for the utag.link event) can still pick up the variation.
Prerequisites
Before configuring the integration, confirm the following:
Tealium iQ is installed and
utag.jsis loaded on the pages running experiments. Confirm by checking thatwindow.utagexists in the browser console.Optimizely Web Experimentation snippet is deployed on the same pages.
Mask descriptive names is disabled (Optimizely Settings > Privacy), otherwise Tealium receives numeric IDs instead of readable experiment and variation names.
You have publish access to your Tealium iQ profile to add or configure the tags and extensions that will consume the data layer attributes.
You have admin access to your Optimizely project (Settings > Integrations) to create the custom analytics integration.
Load Order
Optimizely recommends loading its snippet outside a tag manager, but Tealium iQ can also load the Optimizely snippet for you (see Method 1). However the snippet is loaded, the custom integration needs the utag object to exist before it calls utag.link(). Because Tealium and Optimizely load independently, the callback uses window.optimizely.get('utils').waitUntil() to defer the utag.link() call until utag is present, which guards against the case where Tealium's utag.js has not finished loading at decision time.
A typical site-wide <head> configuration, with both snippets loaded directly on the page, looks like this:
<head>
<!-- 1. Tealium iQ utag.js (loads first).
Copy the loader snippet from Tealium iQ > your profile.
Replace ACCOUNT, PROFILE, and ENV with your values. -->
<script type="text/javascript">
(function(a,b,c,d){
a='//tags.tiqcdn.com/utag/ACCOUNT/PROFILE/ENV/utag.js';
b=document;c='script';d=b.createElement(c);d.src=a;
d.type='text/java'+c;d.async=true;
a=b.getElementsByTagName(c)[0];a.parentNode.insertBefore(d,a);
})();
</script>
<!-- 2. Optimizely Web snippet (loads second) -->
<script src="https://cdn.optimizely.com/js/YOUR_PROJECT_ID.js"></script>
</head>
Copy the exact loader snippet from your Tealium iQ profile — it encodes your account, profile, and environment (dev, qa, or prod). Do not hand-reconstruct the URL.
Choosing an Integration Direction
Optimizely and Tealium can be wired together in several officially documented ways. Be clear about which problem you are solving before picking one:
Tealium iQ loads the snippet | Tealium AudienceStream targeting | Optimizely Events Connector | Custom JSON Integration (this guide) | |
|---|---|---|---|---|
Direction | Tealium deploys Optimizely | Tealium audiences → Optimizely | Tealium events → Optimizely Event API | Optimizely decisions → Tealium |
Use case | Manage the Optimizely snippet in your tag manager | Target experiments by AudienceStream audiences | Forward Tealium-collected conversions into Optimizely results | Label all Tealium-downstream data with variation |
Official Optimizely doc | Yes | Yes | Yes | No — custom |
Where it is built | Tealium iQ template | Optimizely Settings > Integrations + Tealium Tools | Tealium EventStream connector | Optimizely Custom Analytics Integration |
This guide's primary subject is the rightmost column — sending Optimizely decisions to Tealium. The other three are summarized in Method 1 because teams frequently confuse them.
Method 1: Official Optimizely–Tealium Integrations (Inbound)
These are Optimizely's documented Tealium integrations. All of them move data toward Optimizely or deploy Optimizely through Tealium. If your goal is to get experiment data into Tealium, skip to Method 2 — none of these do that.
Loading the Optimizely Snippet Through Tealium iQ
Optimizely's developer documentation describes deploying the Optimizely snippet from within Tealium iQ, synchronously or asynchronously. Optimizely generally recommends loading the snippet outside a tag manager to minimize flicker, but supports the Tealium path:
In Tealium iQ, open Manage Templates and select uTag Sync (Profile) UID:sync for synchronous loading.
Add the Optimizely loader to the template config — for synchronous loading Optimizely documents
document.write('<script src="//cdn.optimizely.com/js/YOUR_PROJECT_ID.js"></script>').For asynchronous loading, add the Optimizely (Async) tag from Tealium's tag marketplace, paste your snippet via Extract from Code, set the Wait Flag to No, and ensure Optimizely is first in the tag list so it loads as a blocking tag and reduces flicker.
This only controls how the Optimizely library loads. It does not send experiment decisions to Tealium.
Tealium AudienceStream Targeting
Optimizely Web Experimentation has a built-in Tealium AudienceStream integration under Settings > Integrations. Toggling it on and entering your Tealium Account ID lets you use AudienceStream audiences as Optimizely audience conditions, so you can target experiments at segments enriched in Tealium. This requires the Tealium iQ utag to be installed and the Tealium Collect tag configured with frequent data enrichment. This is audience targeting, not decision reporting.
Optimizely Events Connector (Tealium → Optimizely)
Tealium's Optimizely Events Connector (configured in Tealium, not Optimizely) forwards events from Tealium EventStream or AudienceStream into Optimizely's Event API via its Log endpoint. This is the inverse of Method 2: it sends conversions collected by Tealium into Optimizely so they can feed Stats Engine. Configure it with the Track Event (Activate Users) action and your Optimizely Account ID and Access Token. Use this when Tealium is your event-collection layer and you want those events counted in Optimizely results.
Method 2: Custom JSON Integration (Optimizely Decisions to Tealium)
To label Tealium's data with the experiment and variation a visitor received, use a Custom Analytics Integration. Optimizely's track_layer_decision callback fires on each bucketing decision and calls utag.link() to push the decision into Tealium's data layer and event pipeline. This is the supported pattern for sending Optimizely decisions to Tealium; there is no native plugin that does it.
Creating the JSON Integration
In your Optimizely project, go to Settings > Integrations.
Click Create Analytics Integration > Using JSON.
Paste the following configuration:
{
"plugin_type": "analytics_integration",
"name": "Tealium (Custom)",
"form_schema": [],
"description": "Pushes Optimizely experiment decisions into the Tealium data layer and fires a utag.link event so variation data flows to all Tealium-connected destinations",
"options": {
"track_layer_decision": "var state = window['optimizely'].get('state');\nvar expName = String(campaignId);\nvar varName = String(variationId);\n\nif (state) {\n try {\n var decisionObj = state.getDecisionObject({ campaignId: campaignId });\n if (decisionObj) {\n expName = decisionObj.experiment || expName;\n varName = decisionObj.variation || varName;\n }\n } catch (e) {}\n}\n\nvar variationValue = isHoldback ? 'holdback' : varName;\n\nwindow.utag_data = window.utag_data || {};\nwindow.utag_data['optimizely_experiment_name'] = expName;\nwindow.utag_data['optimizely_variation_name'] = variationValue;\n\nvar utils = window['optimizely'].get('utils');\nutils.waitUntil(function() {\n return typeof window.utag !== 'undefined' && typeof window.utag.link === 'function';\n}).then(function() {\n window.utag.link({\n tealium_event: 'optimizely_decision',\n optimizely_campaign_id: String(campaignId),\n optimizely_experiment_id: String(experimentId),\n optimizely_experiment_name: expName,\n optimizely_variation_id: String(variationId),\n optimizely_variation_name: variationValue,\n optimizely_is_holdback: isHoldback\n });\n});\n"
}
}
Click Create Extension (or Save Integration).
Toggle the integration to Enabled in Settings > Integrations.
Optionally check Enable this integration by default for all new experiments.
For existing experiments, open each experiment's Integrations tab and enable the "Tealium (Custom)" integration.
What the Callback Does
The options.track_layer_decision callback fires each time Optimizely makes a bucketing decision. Here is a breakdown of the logic:
Retrieving human-readable names: state.getDecisionObject({ campaignId }) looks up the experiment and variation names from Optimizely's state API. If Mask descriptive names is enabled, the lookup returns numeric IDs and the callback falls back to them.
var state = window.optimizely.get('state');
var expName = String(campaignId);
var varName = String(variationId);
if (state) {
try {
var decisionObj = state.getDecisionObject({ campaignId: campaignId });
if (decisionObj) {
expName = decisionObj.experiment || expName;
varName = decisionObj.variation || varName;
}
} catch (e) {}
}
Writing to the data layer: Tealium's Universal Data Object, utag_data, is the page-level data layer that tags read at load time. Writing the experiment and variation onto it makes the values available to any tag or extension that maps the data layer, independent of the utag.link event.
window.utag_data = window.utag_data || {};
window.utag_data['optimizely_experiment_name'] = expName;
window.utag_data['optimizely_variation_name'] = variationValue;
Waiting for Tealium to be ready: optimizely.get('utils').waitUntil() defers execution until utag.link exists. Tealium's utag.js loads asynchronously, so the decision data is held until Tealium initializes and then sent.
var utils = window['optimizely'].get('utils');
utils.waitUntil(function() {
return typeof window.utag !== 'undefined' && typeof window.utag.link === 'function';
}).then(function() {
// utag.link runs here once Tealium is ready
});
Firing the event: utag.link() tracks a non-page-view interaction. It takes a single data object whose tealium_event key names the interaction — here optimizely_decision. The remaining keys carry the experiment metadata. Tealium iQ distributes this event to every tag and EventStream connector mapped to it.
window.utag.link({
tealium_event: 'optimizely_decision',
optimizely_campaign_id: String(campaignId),
optimizely_experiment_id: String(experimentId),
optimizely_experiment_name: expName,
optimizely_variation_id: String(variationId),
optimizely_variation_name: variationValue,
optimizely_is_holdback: isHoldback
});
Note:
utag.link()passes data into Tealium's pipeline, but it does not route anything by itself. You still configure, in Tealium iQ, which tags fire on theoptimizely_decisionevent and which attributes map to each destination. The integration's job is to get clean variation data into Tealium; deciding where it goes is a Tealium-side configuration step.
Mapping the Decision in Tealium iQ
Once the event reaches Tealium, configure routing on the Tealium side:
In Tealium iQ, go to the Data Layer and add the
optimizely_experiment_name,optimizely_variation_name, and related attributes as UDO variables so they are recognized.For each tag that should receive variation data (for example a GA4 or ad-platform tag), add a mapping from the Optimizely attributes to that vendor's expected fields.
Optionally add a Load Rule so a tag only fires on
tealium_eventequal tooptimizely_decision, or include the variation attributes on all events so they ride along with normal page views and conversions.Save and publish the Tealium profile to the appropriate environment.
Verifying the Integration
After enabling the integration, verify that data reaches Tealium before treating results as reliable.
Console Verification
Open your browser's developer console on a page with an active experiment:
// Check that Tealium is loaded
console.log('Tealium loaded:', typeof window.utag !== 'undefined' && typeof window.utag.link === 'function');
// Inspect the current data layer
console.log('utag_data:', window.utag_data);
// Check Optimizely experiment state
var state = window.optimizely && window.optimizely.get('state');
if (state) {
var campaigns = state.getCampaignStates({ isActive: true });
for (var id in campaigns) {
var c = campaigns[id];
console.log('Campaign ' + id + ':', {
experimentId: c.experiment && c.experiment.id,
variationId: c.variation && c.variation.id,
isHoldback: c.isInCampaignHoldback
});
}
}
// Manually fire a test event to confirm utag.link is wired up
window.utag.link({ tealium_event: 'optimizely_decision', optimizely_experiment_name: 'console_test' });
Tealium Web Companion / Trace
Tealium provides two browser-side debugging surfaces:
Web Companion (append
?tealium=trueto your page URL, or use the Tealium Tools Chrome extension) shows the live data layer and every tag that fires. Browse a page with an active experiment and confirm theoptimizely_experiment_nameandoptimizely_variation_nameattributes appear in the data layer, and that anoptimizely_decisionlink event is logged.Trace (in the Tealium server-side UI) lets you start a trace session, attach it to your browser, and watch events arrive in EventStream in real time. Confirm the
optimizely_decisionevent reaches EventStream with the expected attributes if you are forwarding to the server side.
Network Tab
Tealium tags fire their own network requests. With the Optimizely decision active, watch the network tab for the calls made by the tags you mapped (for example a collect.tealiumiq.com request if the Tealium Collect tag is enabled) and confirm the variation attributes are present in the query string or payload.
Analyzing Experiments in Tealium
Tealium is a data pipeline, not an analytics reporting tool — you do not "read experiment results" in Tealium itself. Instead you use Tealium to enrich and route the variation data, then analyze it in the destinations Tealium feeds.
Visitor Attributes in AudienceStream
If you forward the optimizely_decision event to EventStream, you can build AudienceStream visitor attributes from it — for example a string attribute "Last Optimizely Variation" set from optimizely_variation_name, or a boolean badge "In Checkout Redesign". These attributes then persist on the visitor profile and can drive audience membership.
Audiences and Downstream Activation
Create AudienceStream audiences that combine an Optimizely variation attribute with behavioral criteria (for example, "Saw Variation B AND abandoned cart"). Tealium connectors can then activate that audience into an ad platform, an email tool, or a warehouse, letting you act on experiment exposure across channels.
Warehouse Export for SQL Analysis
If you run a Tealium connector to a data warehouse (Snowflake, BigQuery, Redshift) or to cloud storage, the optimizely_experiment_name and optimizely_variation_name attributes land as columns on the event rows. You can then compute conversion rates by variation in SQL, joining the optimizely_decision events to downstream conversion events on the visitor ID. This is the most flexible analysis path: Tealium standardizes identity across surfaces, so the variation label is attached to a unified visitor record.
Gotchas
Optimizely Has No Native "Decision to Tealium" Plugin
The most common misconception is that one of Optimizely's official Tealium integrations sends experiment decisions to Tealium. None of them do. The snippet-loading, AudienceStream targeting, and Events Connector integrations all move data toward Optimizely or deploy Optimizely. Sending decisions to Tealium is exclusively the custom integration in Method 2.
utag.link Versus utag.view
Use utag.link() for the decision event, not utag.view(). utag.view() is for page views and virtual page views and will be interpreted by Tealium (and downstream analytics tags) as a new page load, inflating pageview counts. The experiment decision is an interaction, not a navigation, so utag.link() is correct.
utag_data Is Read at Load Time, the Event Fires Later
The Universal Data Object utag_data is captured by Tealium when utag.view() runs at initial page load. Writing to utag_data after the page has loaded — as the callback does — updates the object for subsequent utag.link/utag.view calls, but does not retroactively re-fire tags that already read it. That is exactly why the integration also calls utag.link(): the event delivers the data to tags that fire after the decision, while the utag_data write covers tags that map the data layer on later interactions. Tealium's own documentation notes that utag_data is not automatically re-purposed by utag.link/utag.view — you must pass the data object explicitly, which the callback does.
Mask Descriptive Names Hides Readable Values
If Mask descriptive names is enabled in Optimizely's privacy settings, Tealium receives 16-digit numeric IDs instead of experiment and variation names. Disable it for readable values — but note that doing so exposes your variation names in client-side source code.
16-Digit IDs Can Be Mistaken for Card Numbers
Optimizely's experiment and variation IDs are 16-digit numbers. Tealium-connected tools — and any redaction layer in your stack — may flag a bare 16-digit string as a credit card number and strip it. The callback sends IDs as strings (String(...)) and always pairs them with a name, which avoids the bare-number pattern.
Holdback Visitors
When an experiment runs below 100% traffic, Optimizely still makes a bucketing decision for held-back visitors and sets isHoldback to true, even though they see the original. The callback labels these optimizely_variation_name: 'holdback' rather than dropping them, so you can analyze or exclude holdback traffic downstream. If you prefer to exclude them entirely, gate the utag.link() call on !isHoldback.
Multiple Concurrent Experiments Overwrite the Data Layer
The callback writes optimizely_experiment_name/optimizely_variation_name to fixed keys on utag_data. If a visitor is in several experiments on one page, each decision overwrites those keys, so only the last decision's values remain on the data layer. The utag.link() events do not collide — each fires its own event — but the data-layer snapshot reflects only one experiment. If you need all concurrent experiments on the data layer, write to experiment-specific keys (for example optimizely_var_<experimentId>) instead of fixed ones.
Preview Mode Does Not Fire the Callback
Optimizely does not execute custom integration code in Preview Mode, consistent with its built-in integrations. To verify the integration, use a real bucketed session (for example a QA audience or a forced variation via URL parameter), not Preview Mode.
Troubleshooting
No optimizely_decision Event in Tealium
waitUntilnever resolves: Ifwindow.utag.linknever becomes a function — Tealium blocked by an ad blocker, wrong profile/environment in the loader, orutag.jsfailing to load — the callback waits indefinitely and nothing is sent. Confirmtypeof window.utag.link === 'function'in the console at page load.Integration not enabled: Confirm the integration is toggled on in Settings > Integrations and enabled for the specific experiment.
Visitor not bucketed: The callback fires only on an active bucketing decision. A visitor who does not meet the experiment's audience conditions triggers no decision.
Preview Mode: Custom integration code does not run in Preview Mode. Use a live bucketed session.
Event Fires but No Tag Receives the Data
The utag.link() call only puts data into Tealium's pipeline. If a downstream tag is not receiving the variation, the issue is Tealium-side mapping, not the integration:
Confirm the Optimizely attributes are declared in the Tealium Data Layer.
Confirm the destination tag has a mapping from the Optimizely attributes to its fields.
Confirm any Load Rule on the tag matches the
optimizely_decisionevent.Publish the Tealium profile to the environment your page loads (
dev,qa, orprod).
Values Are Numeric Instead of Names
Mask descriptive names is enabled. Disable it in Optimizely Settings > Privacy, then generate a fresh session.
Data Discrepancies Between Platforms
Differences between Optimizely visitor counts and the counts you see in Tealium-fed destinations are expected:
Counting unit: Optimizely counts unique cookie-based visitors; Tealium resolves identity through its own visitor stitching, and each downstream destination counts differently again.
Ad blockers: Privacy extensions may block Optimizely, Tealium, or an individual vendor tag independently, skewing counts in either direction.
Tag firing rules: A Load Rule or consent gate in Tealium may suppress the decision event for some visitors, lowering downstream counts relative to Optimizely decisions.
Preview and QA sessions: Only real bucketed sessions fire the callback (Preview Mode does not), so QA done through Preview will not appear downstream.
Expect a 5–15% discrepancy between Optimizely visitor counts and the counts in any single Tealium destination for the same experiment. Investigate further if the gap exceeds 20% or if an entire destination shows zero, which usually points to a missing Tealium mapping rather than a data loss.