AI-Assisted Salesforce Data Migration: A Real Walkthrough
The first time I let an AI write a real chunk of a data migration, it produced a dedup rule that would have quietly merged around 1,800 contacts into a single record. I caught it in the sandbox dry run, three days before cutover. That is the honest frame for this whole article: AI made the migration faster in every phase, and exactly once it nearly turned a clean cutover into a week of remediation.
The migration was a consolidation of a legacy CRM into an existing Salesforce org — just over 1.9 million contacts, plus accounts, activities, and a decade of notes. I ran the whole thing with an AI assistant in the loop the way I now run most of them. The process itself did not change — profile, map, build, dry-run, cutover, reconcile. What changed is how much of the grunt work I hand off, and how carefully I check the parts I do.
The Profiling Pass Is Where AI Earns Its Keep
Profiling the source is the most valuable day of any migration, and it is also the most tedious. You are writing the same shape of aggregate query against forty columns to find out how bad the data actually is. This is exactly the kind of work AI is good at.
I load the source extract into staging objects in a scratch org first — Legacy_Contact_Staging__c, Legacy_Account_Staging__c — so I can profile with SOQL instead of wrestling CSVs. Then I ask the assistant to generate a profiling pass over every column: null counts, distinct counts, top values, length distribution.
SELECT State_Raw__c, COUNT(Id) records
FROM Legacy_Contact_Staging__c
GROUP BY State_Raw__c
ORDER BY COUNT(Id) DESC
That one query returned 31 distinct values for what should have been eight Australian states and territories. Two were typos, four were full suburb names in the wrong column, and one was a 90-character line of address that had shifted a field to the left across an entire import batch from 2019. None of it would have surfaced in a scoping call. All of it had to be resolved before the mapping made any sense.
The AI wrote the queries. It did not tell me the data was broken — I read the output and decided that. That division holds for the whole project: AI generates the instrument, I read the dial.
The Field Mapping Is a Draft, Not a Deliverable
Once profiling is done, I ask the assistant to draft the field mapping as a config file rather than a prose document. A structured mapping is easier to review, and it becomes the input to the transform code later.
# migration/contact-mapping.yml — first draft from the assistant, then hand-corrected
source_object: Legacy_Contact_Staging__c
target_object: Contact
external_id: Legacy_Id__c
fields:
- source: first_name
target: FirstName
- source: last_name
target: LastName
default: "(unknown)" # source null on ~3,400 rows
- source: email
target: Email
transform: lower_trim
- source: state_raw
target: MailingState
transform: au_state_lookup # 31 raw values -> 8 states + territories
on_unmatched: flag # do NOT guess; route to a review queue
dedup:
match_on: [Email] # this line is what nearly broke the migration
The draft it produced was about 80% right, which is roughly the hit rate I have come to expect. It correctly inferred most target fields from column names. It sensibly proposed lower-casing and trimming email. Where it went wrong was every place the right answer depends on the business, not the schema — and it went wrong confidently, which is the dangerous part.
My opinion here is firm: AI may draft any transformation, but it may never decide a match key. A mapping error on a normal field shows up in QA as a wrong value in one field on some records. A wrong dedup key merges distinct people irreversibly, and it does so silently. The blast radius is not comparable, so the two decisions do not get the same level of trust.
Generating the Transform Apex
For the actual load I write a batch class that reads staging, applies the mapping, and upserts against an External Id. Idempotency is the whole game — run it twice, get the same result, re-run a subset when you find a mapping bug a week later. This is the part where an AI assistant genuinely saves me an afternoon, in the same way it saves me time writing Apex generally: I describe the shape, it scaffolds, I correct the logic.
public class LegacyContactMigrationBatch
implements Database.Batchable<SObject>, Database.Stateful {
public Database.QueryLocator start(Database.BatchableContext ctx) {
return Database.getQueryLocator(
'SELECT Id, Legacy_Id__c, First_Name__c, Last_Name__c, ' +
'Email__c, State_Raw__c FROM Legacy_Contact_Staging__c ' +
'WHERE Migrated__c = false'
);
}
public void execute(Database.BatchableContext ctx, List<Legacy_Contact_Staging__c> rows) {
List<Contact> toUpsert = new List<Contact>();
for (Legacy_Contact_Staging__c row : rows) {
toUpsert.add(new Contact(
Legacy_Id__c = row.Legacy_Id__c, // External Id -> idempotent upsert
FirstName = row.First_Name__c,
LastName = String.isBlank(row.Last_Name__c) ? '(unknown)' : row.Last_Name__c,
Email = row.Email__c?.toLowerCase()?.trim(),
MailingState = StateMapper.toAuState(row.State_Raw__c)
));
}
// allOrNone = false so one bad row does not fail the whole batch
Database.upsert(toUpsert, Contact.Legacy_Id__c, false);
}
public void finish(Database.BatchableContext ctx) { }
}
The non-obvious parts are the two I always check by hand regardless of who wrote the draft. The allOrNone = false on the upsert means a single validation failure does not roll back the whole batch — but it also means you must capture the failed rows and reconcile them, which the AI happily left as a finish method that does nothing. And before any of this runs, I flip the trigger bypass in the framework so a decade of legacy records is not fired through automation that was written for one clean record at a time. AI will not remember to do that for you. It does not know your automation exists unless you tell it, and even then it treats it as optional.
The Near-Miss That Set My Rule for AI on Migrations
Back to the 1,800 contacts. The mapping draft proposed deduplicating on email, which is a completely reasonable default and wrong for this source. The legacy system had a shared inbox — an info@ address — sitting in the email field on roughly 1,800 contacts, because a receptionist years earlier had used it as the fallback when a real address was missing. Match on email, and every one of those distinct people collapses into a single Contact on upsert.
I did not catch this by being clever. I caught it because the dry run in the sandbox produced a Contact count about 1,800 lower than the source, and reconciliation is non-negotiable in my process. The fix was a composite match key — email plus last name plus postcode — and a review queue for anything that still looked like a collision.
That is the story behind my one hard rule: AI drafts transformations, a human owns the match key, and reconciliation counts get read on every dry run before anyone signs anything.
The Trade-Off I Accepted to Ship on Time
The composite key came with a trade-off I made with my eyes open. I had two options. Hold the fixed cutover date and keep tuning the match key toward zero collisions, or ship a key that was clearly better than email-only yet still imperfect and go live as planned. I chose the second, and it is worth being honest about what that cost.
The composite key is better, not perfect. A handful of genuine duplicates almost certainly slipped through where two records shared all three fields with slightly different spelling, and a handful of real people got flagged for manual review who did not need to be. I shipped it that way because the cutover date was fixed and the residual error sat in the low dozens against 1.9 million records — a rate I could defend to the business and clean up afterwards. It is still on the data-quality backlog. I have stopped pretending migrations end clean.
What I Do Not Hand to AI
A few things stay firmly with me, and it is worth being explicit about why.
Dedup and merge logic, as above. The failure mode is silent and irreversible, which is exactly the combination you never automate on trust.
The reconciliation itself. AI can generate the comparison queries — source count versus target count, field-by-field null checks, relationship integrity — but reading whether a 0.3% discrepancy is acceptable rounding or a broken batch is a judgement call about this specific data. I make that call.
The cutover go/no-go. When something looks subtly off an hour after a load, the decision to roll back or push through depends on how much you trust the reconciliation, how reversible the state is, and how much the business can absorb. That is not a question you delegate to something that has never felt the consequences of getting it wrong.
Everything else — profiling queries, mapping drafts, transform scaffolds, the tedious StateMapper lookup table with its 31 raw values — I hand over gladly. That is most of the keystrokes and almost none of the risk.
Faster, Not Automatic
If you are running a migration and want a second pair of eyes on the mapping and the reconciliation plan before you commit to a cutover date, that is the kind of review I do. The pattern I look for first is always the same one that nearly cost me here — a default that is reasonable in general and wrong for this source.
AI made the boring eighty percent of this migration disappear — the profiling, the scaffolding, the lookup tables I used to hand-type at midnight. The twenty percent that decides whether the cutover works is still mine to own, and after the info@ scare, I am glad it is.
Liked this?
New essays in your inbox when I publish. No cadence, no spam.
You might also like
How I Use Claude Code to Write Apex 5x Faster
The actual workflow I run for AI-assisted Apex development — what AI handles well, what it gets wrong, and the prompts that survive code review.
Read →AI-Generated SOQL: Surprisingly Good, Occasionally Dangerous
AI writes cleaner SOQL than I do at 5pm — right relationships, right field names. But it can't see your data volumes. Here's where it helps and hurts.
Read →Using AI to Generate Apex Test Classes: What Works and What Doesn't
AI can hit Apex code coverage in seconds, but coverage was never the point. Here is what AI test generation gets right, where it fails, and how I use it.
Read →