Not verified hands-on. This article was written from primary documentation rather than reproduced in an org, so it carries no tested-release badge. If you run it and something differs, say so in the comments and it gets tested and corrected.

In one sentence: load in dependency order, and the order is decided by your relationships, not by your project plan. Users and roles first, because ownership and access depend on them. Then parents before children, all the way down. Junction objects after both sides exist. Activities, files and history last. Get the order wrong and you do not get an error you can fix — you get a load that half-succeeded and a decision about whether to unpick it or start again.

Migration load orderThe order objects have to load in, decided by relationships and ownership rather than by the project plan.01Users, roles, profiles, permission sets02Reference and configuration data — price books, products, custom settings03Top-level parents, usually Accounts04Children, in relationship depth order05Junction objects, once both sides exist06Activities, notes, files, history07Second pass for self-references: parent Account, User managerSort every child file by parent Id before loading, and reconcile three counts after each step— source, loaded, rejected — before starting the next.
Relationships and ownership decide this order, not the project plan.

Draw the dependency graph before you touch a loader

Take every object in scope and write down what each one needs to exist first. Two things create a dependency: a lookup or master-detail field, and ownership.

The graph is usually shallower than people fear and wider than they expect. A typical migration comes out roughly as:

  1. Users, roles, profiles and permission sets. Everything else references an owner, and an owner that does not exist yet becomes the loading user by default, which is how a migration quietly ends with every record owned by the admin.
  2. Reference and configuration data. Price books, products, record types in place, custom settings, anything a validation rule reads.
  3. Top-level parents. Accounts almost always. Anything with children that has no parent of its own.
  4. Children, in relationship depth order. Contacts, then Opportunities, then line items.
  5. Junction objects. Both parents must exist, so these come after every branch they join.
  6. Activities, notes, files, and anything historical. They attach to records, so they need the records.

Where an object relates to itself (a parent Account, a manager on User) split it. Load the rows with the self-reference empty, then run a second pass that fills it in. There is no ordering that solves a cycle in one pass.

External IDs are what make the order survivable

The hard part of a migration is not the rows. It is the relationships, because the child needs the Salesforce Id of a parent that did not exist until an hour ago.

Two ways to do it. The bad way: load parents, export the results, VLOOKUP the new Ids into the child file, load children. It works, it is slow, and every re-run means redoing the mapping.

The good way: put an external ID field on each object holding the source system’s key, and relate records by that key instead of by Salesforce Id. Apex and the APIs both support relating records using an external ID rather than querying for the parent first, and upserting on that key makes the whole load repeatable.

Repeatable is the property that matters. A migration you can run twice without creating duplicates is a migration you can rehearse, fix, and run again on cutover weekend. One that inserts is a single attempt with no undo.

Turn off what will fight you, and know what you are turning off

Automation designed for one user clicking Save behaves differently against 200,000 rows arriving in batches. Before a bulk load, decide deliberately about:

  • Triggers, flows and processes. Disable what should not run for migrated data. Write down what you disabled — the list is also your post-load checklist.
  • Validation rules. Historical data will not satisfy rules written for today’s process. Trying to make it comply is usually a worse choice than switching the rules off for the load.
  • Workflow field updates and roll-ups. These recalculate anyway once the data lands. They do not need to fire per batch.
  • Duplicate rules. Fine to leave on if you want them enforcing. Understand that a blocked duplicate is a row that did not load, and it will not announce itself.
  • Sharing calculation. For a large load, ask Support about deferring it, then let it run once at the end. Recalculating per change is the expensive way to reach the same state.

The load user matters too. Give it a profile that can see and write everything in scope, and (this is the part that gets missed) keep it out of the role hierarchy if it is going to own a large number of records. That is ownership skew, and we wrote about why it hurts in data skew and the 10,000-record line.

Sort the file, and stop fighting lock errors

Sort every child file by its parent Id before loading. Children of one parent then land in one batch instead of being spread across parallel batches that fight for the same parent lock.

This one habit removes most UNABLE_TO_LOCK_ROW failures in a migration, costs nothing, and is skipped constantly. If sorting is impossible, switch the job to serial and accept the slower run — a slow load that finishes beats a fast one you restart twice. The mechanics are in what is actually locked.

Reconcile with counts, not with confidence

Bulk Data Load Jobs showing a completed job with records processed and records failed
Records Processed against Records Failed, per job. This page is the reconciliation you were going to do in a spreadsheet.

After each object, before the next one, check three numbers: rows in the source, rows loaded, rows rejected. They have to add up. Any gap is investigated before you build more data on top of it, because a child load against an incomplete parent set produces a second, worse problem.

Keep the error files. Loaders write a row per failure with the reason, and those files are the only cheap record of what did not make it. Deleting them after a rerun is how a team ends up guessing whether 1,200 contacts were rejected or never extracted.

The last check is one people skip because it feels pedantic: pick ten records at random and read them end to end in the interface. Automated counts will not notice that every date is a day out because of a timezone assumption, or that a currency field loaded as text into the wrong column. Ten records takes ten minutes.

What I would do differently on a second migration

Rehearse the whole sequence in a sandbox with real volume, timed. Not a sample — volume, because the two things you actually need to know are how long the sequence takes and where it breaks, and neither shows up at 500 rows.

Then write the runbook as a numbered list with the row counts you expect at each step. On cutover day the job is following a list you have already run twice, not making decisions at 2am.

Frequently asked

Can I load everything in parallel to save time? Only objects with no dependency on each other, and expect lock contention wherever two loads touch the same parents. The sequence is usually not your bottleneck. The automation you left enabled is.

What about record ownership. During or after? During, using an external ID on the owner where the API allows it, so you are not reassigning millions of rows afterwards. A mass owner change is its own long-running operation.

Do I need to migrate history and activities at all? Ask the business what they will genuinely open. Activity history is often the largest volume in a migration and the least read afterwards. A filtered subset, or a read-only archive elsewhere, is frequently the better answer.

How do I handle audit fields? Created and modified dates can be set on load if the feature is enabled for your org, and it has to be enabled before the load, not after. Decide early, because “we will fix the dates later” is not a thing you can do.