On a marketing automation migration involving Adobe Marketo Engage, HubSpot, Account Engagement (Pardot) or Oracle Eloqua, The InHouse Marketers runs the site as a separate workstream so its forms keep capturing: hidden fields inventoried, forms repointed in batches, both platforms taking posts until a set date, and a daily zero-submission alert. The failure this guards against is a form that loads, validates and shows its thank-you message while the UTM, GCLID and referrer values never reach the new database. A value lost that way can't be recovered, because the landing URL that carried it has gone.
Why the website is a separate workstream
The platform team owns programs, fields and the CRM sync. The web team owns the code, the CMS, the tag manager and the release pipeline, and a form post crosses between them: the page decides what gets sent, and the platform decides what it accepts. The script that fills hidden fields lives in the web repository, on the web team's release calendar, so a plan that lists forms as platform assets misses it.
Give the website its own owner, inventory, register and an acceptance test that reads records in the destination. Two rows in our migration track record include website work: Eloqua to Marketo, where the site's data-collection layer was rebuilt and version-controlled so capture never stopped, and Segment.io to Marketo, where the site pages were updated to carry the tracking. If nobody on your side owns the form code, we take it on as website development work.
The hidden-field inventory
A hidden field carries a value the visitor never types, so a missing one doesn't stop the form submitting. Inventory these five before any form moves:
- UTM parameters, read from the landing URL. If the form sits on a later page, hold them from landing until submit in a first-party cookie set on your root domain, so a form on a subdomain such as go.example.com can read them. Session storage is per origin and per tab, so it loses them on a new tab or a subdomain.
- GCLID, which Google Ads auto-tagging appends to the landing URL so it can tie a click to what the person did next (Google's description). Store it with the UTMs.
- Referrer, read at landing. Read
document.referreron the form page and anyone who browsed first shows your own domain. - Source and sub-source, the internal values a page or campaign stamps. Check which lifecycle, scoring and routing rules read them before you rename anything.
- Consent flags, with the wording the visitor saw and the date. The evidence fields have to exist in the destination before the form that fills them moves.
For each field, record where the value comes from, which forms carry it, its API name in both platforms, which system masters it and which direction it syncs to the CRM. Map by API name, since two fields sharing a display label can hold different things. Then take a baseline: per form and per hidden field, the share of last month's records that hold a value.
If the destination is HubSpot or Marketo, MarqIQ, the software we build, creates the UTM and referrer fields on its Pro plan and above, and adds them as hidden fields to forms built in that platform. GCLID, consent, your own source values and forms coded into the site stay with your web team. In HubSpot it also builds the first-touch workflow, created switched off for you to review and turn on; in Marketo you add the trigger and flow steps to the smart campaign it creates. Its script fills those fields only while a MarqIQ plan is active, so record MarqIQ in the inventory as their source.
The repointing register
The register lists every form the site posts before the first one moves. It's the release-engineering document in how we run migrations, and the only record of which endpoint a form should post to on a given day:
| Form | Pages | Current endpoint | New endpoint | Batch | Move date | Owner | Submissions per day |
|---|---|---|---|---|---|---|---|
| Contact us | /contact, site footer | Old platform's form handler URL | Destination form ID | 1 | YYYY-MM-DD | Web lead | Last full month ÷ days |
| Gated report | /resources/* | Old handler URL in page code | Destination form ID | 3 | YYYY-MM-DD | Web developer | Last full month ÷ days |
Column notes:
- Pages: crawl the site and the tag manager for form embeds and posts to the old endpoint, since memory misses campaign landing pages.
- Current endpoint: export it the day before the batch moves. The rollback restores that copy.
- Batch: start with low-volume forms that still submit daily, such as the contact form. A gated report that submits twice a week makes a bad first batch, because a silent day from it proves nothing.
- Owner: by role, whoever repoints the form and reverts it if the rollback trigger fires.
- Submissions per day: last month's count in the old platform, divided by its days. This figure sets the alert.
Where the form script ships through the tag manager, repoint it with a new container version. One person can revert it, and it's the one rollback step you can rehearse on the live site.
Dual posting on purpose, then a date it stops
Through the parallel period both handlers are live by design: moved batches post to the new platform, the rest to the old one, and for a short overlap a moved form can post to both. The overlap tests the field map with live traffic, because the same submission lands twice, and a value that differs between the two records is a mapping fault unless a normalisation campaign or workflow in the destination changed it on purpose.
Plan for the costs: the same person in both platforms, and twice in the CRM wherever both sync to it; two autoresponders and two sales alerts unless the old platform's are switched off for forms in the overlap; two instances under change control; and a daily import of the unsubscribes the old platform records into the single suppression list, so an unsubscribe given on an old form reaches the platform that's sending.
The end date is the last day of the parallel period, when the old instance stops taking posts. Set it before the first form moves, with its exit criteria: a full campaign cycle through the old instance, clean diffs, and the last batch repointed. Before that date, a post to the old endpoint is expected; after it, the same post is a live defect, and the register is how you tell them apart. An old form handler accepts posts until someone disables it, so the switch-off needs a named owner.
Single-page apps and Marketo Forms 2.0
In a single-page app the landing URL has gone after the first route change, so capture UTMs, GCLID and referrer when the app boots and hold them until submit. Marketo's embed code also expects to render and validate its own markup, inside a view the framework already owns.
On the Eloqua to Marketo migration we wrote up, the pattern that held was a Marketo form loaded off-screen, with the app rendering its own form and handing the values over with addHiddenFields() before calling submit(). In outline, using the calls in Adobe's Forms 2.0 API reference:
// At boot, once. The embed code's <form> element sits in a hidden container.
// Build that Marketo form with no visible fields, so every value arrives
// through addHiddenFields() and submit() has no empty required field to reject.
let mktoForm = null
MktoForms2.loadForm(BASE_URL, MUNCHKIN_ID, FORM_ID, form => {
// Returning false stops Marketo forwarding to the follow-up page.
form.onSuccess(() => false)
mktoForm = form
})
// On the app's own submit. Keys are the names Marketo forms post: the SOAP API
// names in Field Management's export (Email, FirstName), not the REST names.
// `stored` holds what the app captured at landing.
function postToMarketo(answers, stored) {
if (!mktoForm) return false // not loaded yet: queue the post or show an error
mktoForm.addHiddenFields({ ...answers, ...stored })
mktoForm.submit()
return true
}
Per Adobe's reference, onSuccess returning false stops both the redirect and the page reload, so show your own confirmation from that callback. Keep the form object from the loadForm callback instead of calling MktoForms2.whenReady() on each submit. whenReady runs its callback once for every Marketo form that is or becomes ready, so a footer newsletter form on the same page would post too, and each earlier submit fires again, with its old answers, when the app loads another form on a later route. submit() runs the form's validation and any onSubmit handlers before it posts, which is why the off-screen form carries no visible fields: an empty required field on it blocks a post nobody sees. Keep the code in the site's repository under review, because a mistyped key means that value never reaches its field.
Which history restarts at go-live
Person records and field values move with an export and a field map. Activity history mostly stays behind: opens, clicks, form fills and page views remain in the old platform, so every report or scoring rule with a lookback window restarts at cutover.
Anonymous browsing starts again too, because each platform's tracking script sets its own cookie. Eloqua's visitor cookie and Marketo's _mkto_trk are unrelated, so a known person's pre-cutover browsing doesn't follow them into Marketo, and their history there starts the day Munchkin goes live. For the web side:
- Put the destination's tracking script live at the start of the parallel period, ahead of the first form batch, so anonymous history builds before cutover. Add its cookie to your consent tool first.
- Get it onto every page, including landing pages and microsites outside the CMS; a section without it drops out of source data.
- On a move to Marketo, test the two browser-side ways Munchkin ties a visitor to a known person, starting with the gated pages: a click from a tracked Marketo email, which lands with
mkt_tokin the query string, and a Marketo form fill (Adobe's lead tracking guide). Munchkin'sassociateLeadis deprecated and no longer available, so replace any page code still calling it with a server-side call to the REST API's Associate Lead endpoint.
Warn whoever owns the dashboards before the first report lands; the revenue continuity lens sets out who hears what.
The alert that catches a silent form
A daily submission count per form, with an alert on zero, is cheap to build and catches the failure that looks like a slow week: a form that submits while its posts land nowhere. Count in the destination, from its record of yesterday's form fills per form. A counter on the page sees the click, and the destination sees the record sales works from. Route it to the hypercare rota so it reaches whoever is on duty.
Where the register shows a form averaging under one submission a day, a zero is normal, so give it a weekly count or a scheduled submission from the QA seed list. Then pair the count with the inventory baseline: the share of yesterday's records from each moved form holding each hidden value. A form posting empty hidden fields passes the zero check and fails this one.
Frequently asked questions
How do you switch marketing automation platform without losing form submissions?
Move forms in batches from a register holding each form's current and new endpoint, keep the old handler live until a switch-off date fixed before the first batch, and alert when a form that normally submits every day records none. Submissions go missing from repointed forms nobody watches, so the alert goes live first.
Why are UTM values missing after a marketing automation migration?
The hidden fields weren't recreated on the new form, were mapped by display label instead of API name, or weren't held from landing until submit. The form still submits, so nothing looks broken. Compare each hidden field's fill rate on moved forms against its old-platform baseline.
Should website forms post to both platforms during a migration?
For a fixed period, yes. The same submission lands in both platforms, so a value that differs, once normalisation has run, exposes a mapping fault. The cost is the person twice in the CRM wherever both platforms sync to it, doubled alerts and a daily import of unsubscribes into the suppression list. Set the switch-off date before the first form moves; after it, any post to the old endpoint is a defect.
How do you use Marketo Forms 2.0 hidden fields in a single-page app?
Capture UTMs, GCLID and referrer when the app boots, because the landing URL has gone after the first route change. Load the Marketo form once, off-screen, and let the app render its own. On submit, pass the answers and stored values to that form object with addHiddenFields(), keyed by SOAP API name, then call submit(). Return false from onSuccess to stop the follow-up redirect.