The Growth Brief

Email & Deliverability

Braze migration playbook: what actually takes the time

A Braze migration from SFMC or SendGrid, from someone who moved 200 campaigns in an afternoon. The inventory, the identity key, the domain, the cutover.

October 16, 202611 min readKodie Critzer
BRCG

The Growth Brief

Email & Deliverability

In this article10 sections
  1. 01Key takeaways
  2. 02Step 1: find out what you actually send.
  3. 03Step 2: decide the identity key.
  4. 04Step 3: build the content blocks, then convert the templates.
  5. 05Step 4: journeys become Canvases.
  6. 06Step 5: the sending domain is the real asset.
  7. 07Step 6: cut over one trigger at a time.
  8. 08What a national retailer's move looked like.
  9. 09Should you move at all?
  10. 10FAQ

A year ago we scoped a platform migration for a client. 400 campaigns, cross-channel. The aggressive timeline was six months.

That same project is four weeks now. Including QA.

That's not a projection. On the most recent one I moved 200 production email campaigns in an afternoon. Full logic, dynamic content, platform-specific syntax, all of it. When the client asked for every original template carried over too, so they'd have a real control to test the new platform against, we delivered it in hours inside the existing retainer.

So the campaign rebuild is no longer the hard part. This is a walk through what is.

Key takeaways

  • Start with what actually sends, not what the team thinks sends. On one account that was 8,545 campaigns and 59 of them had sent anything in 30 days.
  • Decide `external_id` before you touch a template. Every alias, merge and triggered send depends on it.
  • Build the content blocks first. 115 templates become a handful of patterns and the conversion becomes a script.
  • Reputation lives on the sending domain and the IPs. Move those and you skip the warm-up. Start new ones and you don't.
  • Cut over one trigger at a time while the old platform keeps sending. The old account is the rollback plan.

Step 1: find out what you actually send.

Every migration starts with an inventory and almost nobody can produce one from memory.

We pulled a client's account to find out what they actually sent. 8,545 campaigns. 59 had sent an email in the last 30 days. Nobody in the building could have told me that. Not because anyone was careless. The program had been through 3 teams in 5 years, and each handover passed on the campaigns without passing on the reasoning. You inherit 8,545 things and no way to tell which ones are load bearing, so nobody deletes anything.

That's the real condition of CRM at scale, and a migration is the one moment you get to fix it, because you only move what you can justify moving.

So the inventory is a pull from the platform, not a workshop. Every campaign, journey and triggered send, with its trigger, audience, the data fields it references, when it last sent and how much. Then the same thing for events. Another account was sending 143 custom events into its marketing platform. 6 of them triggered anything. The other 137 arrived, got stored, got billed for, and were never read by a campaign, a segment or a report.

You migrate the 59 and the 6. You document the rest and leave it behind.

One more pull while you're in there: flows in draft. A client's account had 99 flows and 75 said draft. Those 75 held 72% of the flow revenue, because the habit is to clone a working flow, edit the clone, turn the clone on and leave the parent in draft for safekeeping. Your inventory has to follow the revenue, not the status label.

Step 2: decide the identity key.

This is the step that gets skipped, and it's the one that breaks things.

Per Braze's docs, a known user is identified by `external_id`, a permanent key you set, and anonymous users carry an alias until they're identified. Once set, it doesn't change. So the question is simple and unforgiving: which id does the product already use?

The answer is almost always the user id in the production database, the same one in the warehouse. Not the email address. Emails change, get shared and get mistyped, and a key that changes is not a key. Not the legacy subscriber key either, unless it already is the product id, because every downstream system would have to translate it forever.

Get this wrong and you find out slowly. Duplicate profiles. A triggered send that fires to the wrong person. A segment count that doesn't match the warehouse and nobody can say why.

Then the data model. Things that are true about a person go on the profile as attributes. Things that happen become custom events with properties. Reference data that's the same for everyone (products, plans, store locations) goes in Catalogs so templates look it up at send time. And anything that has to be correct at the moment the email lands comes from your own API through Connected Content.

That last one matters more than people expect. A client of ours runs a marketplace with millions of live listings that change by the hour. The normal way to put three of those in an email is to pick them on Monday, screenshot them, and hope nothing changes by Thursday. It always changes by Thursday. So the campaign carries which three to show, and at delivery it asks their own public API what those three look like right now. Same email, correct when it lands instead of when it was built. A migration is the right time to design for that, because you're already deciding what lives where.

Step 3: build the content blocks, then convert the templates.

AMPscript and Liquid solve the same problems with different grammar. There's no converter, so every template is a rebuild. The way to make 115 rebuilds reasonable is to stop thinking in templates.

Here's what the 200-campaign afternoon actually looked like:

  • Bulk created reusable content blocks on the destination platform. Header, footer, the body shells, the order table, the one-button call to action.
  • Converted all template syntax to Liquid for the new payload structure.
  • Every template, every variable, every conditional accounted for.
  • Built everything for reuse, so the blocks work across future campaigns and the second migration is faster than the first.

Per Braze's docs a Content Block is reusable Liquid and HTML referenced from any message, so a legal footer change happens once and propagates everywhere. That's also the reason a client can ask for all 200 originals as a control after the fact and get them in hours. The blocks were already there.

Here's the shape of a transactional body on shared blocks, with a fallback and a guard:

liquid
{{content_blocks.${txn_header}}} {% assign name = ${first_name} | default: "there" %} <p>Hi {{ name }},</p> {% if event_properties.${order_number} != blank %} <p>Order {{ event_properties.${order_number} }} is on its way.</p> {% else %} {% abort_message("order_number missing") %} {% endif %} {{content_blocks.${txn_footer}}}

The `abort_message` line is the one people skip. If the event arrives without the field the message needs, don't send a broken email. Stop and log it. One line, and it prevents the sends you'd otherwise be apologizing for.

QA is where the old timelines went to die, so automate it. Every link, every fallback, every conditional, rendered against a full profile, an empty profile and the edge case (a name with an apostrophe, an order with twelve line items). Built once, runs on every deploy.

Step 4: journeys become Canvases.

Triggered sends become API-triggered campaigns or event-triggered Canvas entries. Per Braze's docs an API-triggered campaign takes the send request plus `trigger_properties`, which is where order details and one-time tokens travel, so your product team has a clear contract to build against.

Rebuild the highest-revenue journey first and the most complex one second. Those two surface every architectural question: entry logic, re-eligibility, exit on conversion, quiet hours, frequency caps. Get those right once and the rest is assembly.

Two things to check while you're in there, because both showed up in real accounts.

A welcome series where 146,823 people entered over 97 days and 1,021 got a welcome email. No bug, no outage. A double opt-in gate somebody added years ago because it sounded responsible, eating 99% of the program to protect against a legal risk the US doesn't impose on people who've already accepted terms. If a gate like that exists, the migration is when it goes.

And a daily email sent 129 times in a row. 3,464,523 delivered, 43% open rate, zero unique clicks. The body was a single image with no link in it. Every dashboard said the program was healthy. Don't port the structure of a send without reading the send.

A standard transactional or lifecycle Canvas, copy and Liquid and QA and a deliverability check included, ships in five to ten business days. The multi-channel ones with Connected Content and Catalog dependencies take longer.

Step 5: the sending domain is the real asset.

Reputation lives on the sending domain and the IPs. Not in your account, not in your templates.

So the first question is whether you're keeping them. On one recent move the client's existing sending domain and dedicated IPs transferred into the new Braze workspace with reputation intact. The platform executed the transfer. No new subdomain, no warm-up. What we ran instead was verification on both sides (DKIM alignment, SPF, DMARC, the link domain) and a canary ramp with hard pause thresholds: complaint rate above 0.1% or hard bounces above 2% and the ramp stops.

If you're starting on a new domain, you warm, and the mistake we see most is switching all volume on cutover day. The old platform stops, the new domain goes from zero to full send, and the mailbox providers read it as an unknown sender blasting a list. Weeks of throttling follow. The boring version works: the old platform keeps most of the volume while Braze carries a growing share, starting with transactional and the most engaged recipients.

One update worth knowing. Google removed domain reputation and IP reputation from Postmaster Tools this year, and wrote down why: the score wasn't actionable, it lagged behavior, and it misled people into treating one factor as the whole picture. So the number to watch now is the one that always mattered, the spam complaint rate, alongside Microsoft SNDS and your own bounce reasons.

Step 6: cut over one trigger at a time.

Cutover is a stage, not a switch.

Once a template is rebuilt and QA'd, the product routes that trigger to Braze and stops the old equivalent. One template type at a time, highest confidence first. During the overlap you watch the counts: if the old system fired a thousand shipping confirmations yesterday, Braze should fire about a thousand today.

Nothing gets deleted in the old platform until the parallel run ends. The old account is the rollback plan. Export templates, audiences and send logs before it's shut down, because a year from now someone will ask what the old welcome series said.

Currents, Braze's streaming export, gets wired to the warehouse in this step so reporting runs on real send and engagement data from day one. And check the revenue webhook, because this is where it breaks. A client's revenue reporting went to zero in April and stayed there until August. 6,196 people were paying every month. The purchase webhook was never built when the store moved payment providers, and the one event that did fire sent the amount in a field called `value` while the platform reads `$value`. One character, five months of nothing.

What a national retailer's move looked like.

One national department store chain moved 115 transactional templates from Salesforce Marketing Cloud to Braze. Order confirmations, shipping, the sends a retailer can't miss. We inherited the migration mid-flight.

Every data taxonomy mapped. Every template rebuilt in Liquid on shared content blocks. Parallel writes to confirm the data matched before anything sent. Zero send disruption through the cutover.

The migration was the start, not the deliverable. A consumer health app with millions of users came onto Braze from SendGrid with us four years ago, and we're still their CRM team. Their users get a weekly email that isn't a newsletter. It's their week: hours fasted, longest fast, goal hit or not, pulled from the product at send time. That's the kind of thing a migration makes possible, and it's the reason to do one.

Should you move at all?

Not always. A client was about to sign with a new SMS platform because 70 campaigns to 11.5 million recipients had made $6,879. Then we looked at their flows. Same platform, same audience, 17 times the revenue per recipient. The tool wasn't the problem, and we told them not to buy the thing.

If the platform you're on fits how your team operates, a migration spends a month to arrive where you already are. We'll say so. If it doesn't, the old calculus of a six-figure SOW and a six-month freeze on new campaigns isn't the reality anymore. We wrote up why migrations don't take six months, and what we do with the program afterward is the part that pays for it.

FAQ

How long does a Braze migration take? A year ago we scoped a 400-campaign, cross-channel migration at six months, and that was the aggressive timeline. The same project is four weeks now, QA included. The template conversion used to be the bottleneck and is now the fastest part. What still takes time is the identity decision, the data model, and the sending reputation.

Can SFMC templates be converted to Braze automatically? Not by a converter. AMPscript and Liquid are different languages. But the conversion is scriptable once you build for reuse. We moved 200 production email campaigns in an afternoon by creating the shared content blocks first, then converting every template, variable and conditional to Liquid against the new payload structure.

Do you lose sender reputation when you move to Braze? Only if you start a new sending domain. Reputation lives on the domain and the IPs, not in the account. On one recent move the existing domain and dedicated IPs transferred to the new workspace with reputation intact, so there was no warm-up at all, just a canary ramp with pause thresholds. If you do start new, you warm.

What is external_id in Braze and why does it matter? It is the permanent key Braze uses to identify a known user. If it does not match the id your product and warehouse already use, you get duplicate profiles and triggered sends that fire to the wrong person. Decide it before you touch a template.

BRCG is a Braze partner and runs production Braze accounts across retail, health apps, marketplaces and community platforms. If a migration is on your roadmap, we'll pull your inventory and tell you what's actually load bearing before anyone writes a SOW. Book a call, or read the work.

30-min call

Talk through where you're stuck.

No deck, no pitch. We'll dig into where your growth has stalled and tell you what we'd ship in the first 30 days.

Pick a time

Clients we run programs for

Discord
Twitch
Rhoback
Owner
HubSpot
Iterable
See the work

Want this in your inbox?

No newsletters that apologize for being newsletters. Just sharp takes on what's working in growth.