Customer story · CRM Architecture & Migration

Replatforming a decade-old marketing automation estate: the audit is the project

A late-stage software company committed to moving off a legacy marketing automation platform onto a new one. The single most valuable thing we did was count what was actually running: the live surface turned out to be about 2% of the inventory, and the first estimate of that live surface was wrong by roughly seven times.

ProofWhat happened on a real engagement.
Legal TechnologySector
Late-stageStage
Ongoing engagementDuration
8Min read

Anonymized. The company is described by sector and stage only — no customer is named, and quotes are attributed by role.

#The challenge

More than a decade of marketing automation had accumulated into an estate nobody could describe: roughly 15,000 programs, tens of thousands of smart campaigns, ~2,300 lead fields, ~600 forms, hundreds of email templates and thousands of lists, sitting on ~2.5M contacts with roughly 400k duplicates in them. The signed scope named a handful of integrations. The real integration footprint was about fifty, plus sixteen webhooks. Nobody could say which automations still fired, because the legacy platform marks a program 'active' if it ran once years ago and was never switched off, and exposes no last-used signal on a list. Every scoping decision downstream depended on numbers that did not exist yet.

#The approach

Audit for the live surface, not the inventory

The first pass counted everything and reported the number that mattered: a few hundred active smart campaigns out of roughly fifteen thousand. The stated principle was that auditing all fifteen thousand programs row by row is wasted hours. A deeper second pass then corrected the first: the true active surface was closer to ~2,200 automations (about 1,850 trigger campaigns plus a few hundred recurring batch jobs) across ~15,000 programs and ~54,000 smart campaigns. Roughly 96% of the estate was dead. Being wrong by seven times on your own first audit, and saying so, is the part most migrations skip.

Work around what the platform API will not tell you

The legacy platform's REST API does not expose the integration registry or the webhook list, and does not return smart-list criteria, flow steps, or campaign run results. The live footprint had to be reconstructed from the activity-type catalog, the full lead-field schema, and the custom objects, with webhooks read out of the admin UI by hand. The API also caps at a fixed daily call budget instance-wide, which is why the full asset backup had to be scheduled after cutover, once the limits reset, rather than before it.

Treat field mapping as the thing that prevents silent loss

Every lead field was inventoried and flagged for CRM origin using the custom-field API naming convention: roughly 1,600 of ~2,300 fields came from the CRM. Each row carries data type, length, read-only state and an explicit migrate-or-not decision. Alongside it, a type-compatibility pass, because the legacy platform's idiosyncratic field validation does not all have an equivalent on the new side. A second, non-obvious trap: fields you think of as lead fields can land on the company object in the new platform, so filters have to be written against the object they actually live on.

Rebuild the CRM sync as per-property rules, not one global lever

The legacy platform's only real control was a per-field 'block updates' switch inside a bidirectional sync. The new platform allows a per-property rule: two-way, one-way in either direction, or don't sync at all. That per-property configuration was treated as the single most important config in the migration, with the CRM as system of record, plus explicit sync-direction rules per object and validation against the lead-handoff SLA. Two failure modes were designed around specifically: migrated records error as duplicates on sync unless the CRM record ID is backfilled onto them first, and running both marketing platforms against the same CRM during co-existence has duplicated an entire database before.

Collapse the scoring model instead of porting it

Behavioural and demographic scoring lived across dozens of separate scoring campaigns feeding grade thresholds. In the new platform, one rule-based score property replaces dozens of campaigns. The existing model was frozen for the remainder of the fiscal year and the new one runs in parallel behind it, so qualification targets stay consistent while routing changes underneath. The grade scale itself is preserved letter-for-letter, and the null-preservation rule carries across: when the underlying scores are empty or have decayed to zero, no grade is stamped at all rather than a default low grade.

Translate asset types honestly, and name the ones with no equivalent

Dynamic lists become active lists; static lists move as-is; segmentations have no direct equivalent and get rebuilt as single-select properties rather than as lists. Trigger campaigns become workflows with enrollment triggers; batch campaigns become scheduled or list-enrolled workflows. Program member status has no equivalent at all and becomes a stamped property, with a flag on every report that depended on it. The biggest behavioural difference is re-enrollment: the old platform defaults to run-once, the new one makes re-enrollment opt-in per trigger, so every migrated workflow has to be checked or contacts either never re-enter or re-enter constantly.

Split the work 80/20 by risk, not by volume

Email, webinar and nurture programs are roughly 80% of the volume and the low-risk part; they migrate in waves with a QA gate at the end of each. The remaining 20% is the high-risk tail: campaigns with custom scripting (over a hundred scripted assets, a couple of dozen genuinely complex) and campaigns that fire external API calls. Those get an architect-authored rebuild plan per campaign, translated into workflow webhooks, custom-coded actions or external middleware, and verified individually. The two tracks run in parallel from different starting lines rather than one after the other.

Design suppression rules before the import, not after

Historical qualified leads from two adjacent product lines are suppressed from the current scoring model at import so the load does not mass-update them, and are not re-evaluated until they re-qualify after the migration. The rule was stated plainly by the customer: net-new gets scored, legacy gets ignored. A separate already-processed suppression list is built as a pre-cutover QA step specifically to prevent double-sends on the first live run.

Hit the platform ceilings early and re-scope around them

Three hard limits shaped the plan. The new platform caps active and static lists, and with roughly 12,000 dynamic and 8,000 static lists in the legacy estate, about three quarters of them cannot come across at all regardless of intent. Custom behavioural events are capped at 25 definitions against ~85 legacy activity types, so most interesting moments become property stamps instead. And native marketing analytics on the new platform is read-only and cannot be backfilled, so historical activity has to come in through the custom-events API and will never look native. Each of these was surfaced as a decision for the customer rather than absorbed silently.

Empirically test the scary technical claim rather than arguing about it

The web team raised that the site's cookie header had grown large enough to intermittently break forms on the legacy platform, and that the new platform's documented limit looked smaller. Rather than debate it, we measured the new platform's form endpoint directly: it accepts roughly 16 KB per cookie header against the legacy platform's ~8 KB, with the ceiling enforced upstream at the CDN. Risk on the new platform was low, and the real fix was trimming the cookie jar. Ten minutes of measurement closed an issue that had been open for months.

Plan cutover as a parallel run with one short blackout

Both platforms run live against the CRM simultaneously during co-existence, with a delta sync built so the final cutover is 'off Friday, live Monday': roughly a 48-hour blackout to move ~7-8M records including activities and associations. Ahead of it, a full dress rehearsal in sandbox with a go/no-go decision, then post-cutover smoke tests, an architect sign-off gate, and staged monitoring through the first three weeks before hypercare closes. Sending-domain warm-up is an eight-week ramp scheduled to overlap the migration rather than follow it.

Keep an audit delta running while the migration runs

The customer made the sharpest process point of the project: an audit taken at the start goes stale over a multi-month migration, because new programs keep getting built in the platform being deprecated. A monthly net-new program and field audit tab runs for the whole migration window so anything created mid-flight is re-evaluated for inclusion rather than silently missed at cutover.

Say out loud what the real blocker is

By the point the inventory was complete, the scripts written and every asset assigned a unique ID, the project was not blocked on engineering. It was blocked on a business decision only the customer could make: nobody had defined the criteria for which assets come over. The escalation was framed as three options and only three — give us a filter, fund a multi-month manual triage, or move the date.

#Outcomes

A full estate audit delivered in three review-ready workbooks

Activities (roughly 2,500 live automations out of ~54,000 smart campaigns, ~85 activity types, ~29 channels, ~30 segmentations); Objects and Fields (~2,300 fields, ~1,600 of CRM origin, ~870 flagged unused); Other Assets (~600 forms, ~300 email templates, ~390 snippets, the scripted-campaign tail, ~50 integrations and ~16 webhooks).

The scope was corrected before it became a cutover surprise

The integration count went from a handful named in scope to roughly fifty plus sixteen webhooks, and the milestone was renamed to match. Several integrations existed only as webhooks and would not have appeared on any connector list. The active-automation count was restated upward by roughly seven times after the deeper audit.

Platform access, sandbox and staging landed ahead of target

New-platform access came in ahead of the planned date and a sandbox was provisioned with a content staging area, connected to the CRM staging org. Sample migrations run against a small random lead cohort, with idempotency and retry behaviour wired in and association behaviour verified before any volume load.

Mid-flight, and honest about it

At the latest evidence there was no production cutover, no contacts migrated, no domain warm-up started and no UAT run. Duplicate cleanup and the asset-selection criteria were both still open on the customer's side, and both were named as the major unlocks. This card is a method, not a completed migration.

In their words

What the customer said

“I'm just worried that if scope is not extremely clear this can balloon into something huge.”
“I don't want to lead with that because I want us to work as if we are deprecating it and only extend it if we see like there's absolutely no way, like there's some things that we just have not gotten to. That's my preference.”
“And so apparently our cookie header was so large that our Marketo forms would be failing Occasionally depending on how long the cookie was getting. And when I looked up HubSpot, it seemed like they had an even smaller cookie limit that they could process on their forms.”
“Because I'm also thinking like we're not the largest company that has ever existed. How come this seems like a problem that we're facing and no one else is facing?”
The method behind it

This ran the CRM Migration playbook

The delivery standard this engagement followed.

Connected

In the knowledge graph

Every entity below has its own page, aggregating what we measured, what we recommend and what guests said.

Companies
Related

More on these topics