Integrate Adobe Marketo Engage with Optimizely Web Experimentation
TL;DR
Adobe Marketo Engage measures a landing page by what it does to the lead database. Optimizely Web Experimentation measures it by what happens in the browser. Both are correct, and left unconnected they will disagree — a variation that produces more form submissions can produce fewer qualified leads, and neither tool can see the other's half of that sentence.
Connecting them is two pieces of code and one agreement about naming. This guide covers forwarding Marketo form submissions into Optimizely as custom events using Munchkin's form callbacks, and writing the active variation back onto the lead record so Marketo's own reporting can segment on it. If the test in question is a landing-page headline with a form beneath it — which is most of them — read the experiment design guide first, because the hardest part of these tests is usually deciding what counts as a win.
How Marketo and Optimizely Web Experimentation Meet in the Page
Munchkin is Marketo's tracking script. It identifies the visitor by cookie, records page visits, and exposes the Forms 2.0 API, which is the piece that matters here: MktoForms2 gives every embedded form a JavaScript object with lifecycle callbacks, including one that fires on successful submission.
flowchart TD
A[Visitor loads a landing page] --> B[Optimizely snippet decides the variation]
B --> C[track_layer_decision adds hidden fields to Marketo forms]
B --> D[Munchkin loads and identifies the lead]
D --> E[Visitor submits a Marketo form]
E --> F{Form id in the allowlist?}
F -->|yes| G[Push onto the Optimizely queue]
F -->|no| H[Dropped, silently]
G --> I[Custom event recorded against the variation]
C --> J[Variation stored on the lead record]Note which direction each arrow runs. Marketo tells Optimizely that a conversion happened; Optimizely tells Marketo which arm the lead was in. Neither replaces the other, and a team that only builds the first half ends up unable to answer the question the sales team will ask.
Prerequisites
The Optimizely Web Experimentation snippet URL for your project.
The Munchkin tracking code on the pages under test, and the Forms 2.0 embed for each form.
The numeric form ids. Marketo forms are identified by id, not by name, and the id is what arrives in the browser. Collect them from
Marketing Activities → Design Studio → Formsbefore you write anything.Custom events created in Optimizely for each form you forward, with API names noted. An event pushed under a name that does not exist in Optimizely is discarded without an error.
Two custom lead fields in Marketo, string type, for the experiment and variation names, added to the forms you intend to instrument as hidden fields.
Step 1: Load the Snippet Before Munchkin
Put the Optimizely snippet first in the <head>, above the Munchkin embed. On Marketo-hosted landing pages this goes in the landing page template rather than in an individual page, so that every page built from the template inherits it.
<head>
<script src="https://cdn.optimizely.com/js/YOUR_PROJECT_ID.js"></script>
<!-- Munchkin embed below -->
</head>
No async, no defer. Both hand the browser permission to paint before the variation exists, and on a landing page — where the headline is the thing being tested and is above the fold — the flash is unmissable.
Step 2: Forward Marketo Form Submissions as Custom Events
MktoForms2.whenReady runs a callback for every form on the page as it becomes ready, and form.onSuccess fires after a successful submission. That pair is a stable public API and is a much better hook than listening for the DOM submit, because Marketo forms are re-rendered by their own runtime.
The Event Map
The allowlist is keyed by form id, prefixed so the keys read as identifiers rather than as numbers.
{
"events": {
"form-1042": "demo_requested",
"form-1109": "whitepaper_downloaded",
"form-1204": "newsletter_signup"
},
"numericProperty": "employeeCount",
"properties": [
{ "from": "Industry", "to": "Industry" },
{ "from": "LeadSource", "to": "Source" },
{ "from": "Country", "to": "Country" }
]
}
Form ids are per-instance. Cloning a Marketo form to make a seasonal variant produces a new id, and the clone will forward nothing until somebody adds it to the map. That is the single most common way this integration goes quiet, and it goes quiet without an error — which is why the demo below deliberately includes a cloned form that is not in the allowlist.
There is no revenue on a form fill, so no revenue tag is emitted. value is Optimizely's reserved plain-number tag and an employee count is a defensible thing to put in it, if only so that a metric can be read as "demo requests, weighted by company size".
The Munchkin Form Listener
const FORWARDED_FORMS = {
'form-1042': 'demo_requested',
'form-1109': 'whitepaper_downloaded',
'form-1204': 'newsletter_signup',
};
MktoForms2.whenReady(function (form) {
form.onSuccess(function (values) {
const key = 'form-' + form.getId();
const eventName = FORWARDED_FORMS[key];
if (!eventName) {
return true;
}
const tags = {};
const employeeCount = Number(values.employeeCount);
if (Number.isFinite(employeeCount)) {
tags.value = employeeCount;
}
window['optimizely'] = window['optimizely'] || [];
window['optimizely'].push({
type: 'event',
eventName: eventName,
tags: tags,
properties: {
Industry: values.Industry,
Source: values.LeadSource,
Country: values.Country,
},
});
return true;
});
});
The return true at the end of onSuccess is load-bearing. Returning a falsy value from that callback tells Marketo to suppress its own follow-up behaviour — the thank-you redirect — so a forwarder written without it silently changes what the visitor sees after submitting.
What the Payload Looks Like
A demo request from a 250-person manufacturer produces this:
{
"type": "event",
"eventName": "demo_requested",
"tags": { "value": 250 },
"properties": { "Industry": "Manufacturing", "Source": "Paid Search", "Country": "GB" }
}
The Marketo integration demo runs the same allowlist and mapping over Munchkin-shaped submissions, including the cloned form that gets dropped.
Step 3: Write the Variation Back to the Marketo Lead
Marketo's Forms 2.0 API lets you add hidden fields to a form at runtime, which is the cleanest way to attach the variation to the lead: the value travels with the submission, so it lands on the lead record with no API call and no server-side component.
Optimizely Web Experimentation supports this through a custom analytics integration. The track_layer_decision field is a top-level script, not a function body — a bare return in it raises 'return' outside of function and the integration never runs.
{
"plugin_type": "analytics_integration",
"name": "Marketo Hidden Fields",
"options": {
"track_layer_decision": "var experimentName = campaign.name;\nvar variationName = decision.variation_name;\nwindow['optimizely'].get('utils').waitUntil(function () {\n return window.MktoForms2 && typeof window.MktoForms2.whenReady === 'function';\n}).then(function () {\n window.MktoForms2.whenReady(function (form) {\n form.addHiddenFields({\n optimizelyExperiment: experimentName,\n optimizelyVariation: variationName\n });\n });\n});"
}
}
The state lookups sit above waitUntil deliberately: campaign and decision are in scope when the callback is invoked, and reading them later — inside the deferred function — is how this pattern breaks when the analytics library loads slowly.
Gotchas
Munchkin loads after Optimizely and that is fine. The waitUntil above exists precisely because Munchkin and Forms 2.0 arrive late. Do not solve it by moving the Optimizely snippet down.
Hidden fields must exist on the form.addHiddenFields only populates fields the form already has. Adding the two fields in Design Studio is a separate step, and skipping it means the code runs cleanly and stores nothing.
Marketo landing pages versus your own. If half your forms are embedded on your CMS and half are on Marketo-hosted landing pages, the snippet has to be installed in two places. A test that appears to have no traffic on one arm is usually a template that never got the snippet.
Progressive profiling changes the fields. Marketo can show different fields to a known lead. A property mapping that assumes Industry is always present will simply omit it when it is not, which is correct behaviour — but do not read a missing property as a broken integration.
Marketo's own form A/B testing. Marketo can test two landing pages against each other natively, and running that at the same time as an Optimizely experiment on the same page splits the traffic twice. Neither report is then trustworthy, because each tool believes it owns the assignment. Pick one per page.
Non-form conversions are invisible here. Everything above hangs off onSuccess, which fires for form submissions and nothing else. A conversion that is a click on a phone number, a chat opened, or a calendar booked through an embedded scheduler produces no Marketo form submission, so it needs its own push. That is not a limitation of the integration so much as a reminder to check what the primary metric actually is before assuming this code covers it — on a lot of B2B sites the highest-intent action on the page is not the form.
Verifying the Integration
Load a landing page with the console open and confirm window.optimizely exists before Munchkin's script has finished. Then submit each allowlisted form once with test data and confirm the matching push appears on the queue, carrying the properties you expect and no revenue tag.
For the return direction, activate an experiment, submit a form, and open the resulting lead in Marketo: both custom fields should be populated on the record itself, not merely visible in the browser. Finally, submit a form that is not in the allowlist and confirm nothing is pushed and the thank-you redirect still happens — that second half is what tells you the return true is doing its job. Then compare a week of Optimizely conversions against the Marketo form's own submission count; a persistent gap usually means one template is missing the snippet rather than that the forwarder is wrong, and the results page will show it as one arm quietly under-counting.