Best Practices for Migrating from Salesforce to Dynamics 365 (Dataverse)
A technical guide to planning and executing a large-scale Salesforce to Dataverse migration, from data mapping and record IDs to attachments, audit history, and cutover.
The best practice for migrating from Salesforce to Dynamics 365 (Dataverse) is to treat it as a remodel, not a copy. If the Salesforce contract ends in weeks, separate the two deadlines: in the first 72 hours take a complete export, including field history and files, and load it unchanged into an audit-grade read-only archive; then migrate live data first, in stages, from that export, because weeks is not enough for a full migration. For the migration itself, design the Dataverse model and mapping, carry every Salesforce Id into an alternate key, plan files as their own workstream, preserve created dates, disable automation during loads and reconcile counts per table before a delta cutover.
Your Salesforce contract ends in weeks: should you archive first and migrate to Dynamics 365 in stages?
Yes. A contract ending in weeks is a deadline for getting your data out, not for finishing a migration, and treating the two as one deadline is how history is lost. Weeks is not enough for a full migration done properly, for these reasons:
- The data model has to be designed and mapped field by field, with a business owner signing off each object, before any row moves.
- Files and activity history are the slowest parts of any migration and need their own loads and checks.
- Automation, security, forms and integrations in Dynamics 365 have to exist before users can work there.
- Each stage needs reconciliation and a rehearsal at full volume, and compressing those is how history gets lost without anyone noticing.
- Users need training on the new system, and doing it in the last days before a contract ends guarantees a poor start.
So secure everything first, prove it is complete, and then migrate on a timetable you control, from the export rather than from the live org.
In the first 72 hours, start a complete export
Request the built-in data export with images, documents, attachments and Salesforce Files included, and in parallel extract every object through the Bulk API or Data Loader, including the field history objects (account, opportunity, case and custom object history) that the standard export may not give you in a usable form. Retrieve the metadata as well: objects, fields, picklist values, validation rules, flows and Apex, so you know what the data meant. Large file volumes can take longer than 72 hours to come down, so start every request in the first hours and keep the earliest complete copy.
Verify the export before you rely on it
Compare row counts per object with counts taken from the org, compare file counts and total size, open a sample of records and files, and record who exported what, when and how. Store the export in two places you control. Check your contract for any period after termination in which data can still be retrieved, but plan as if there is none.
Load the export unchanged into a read-only archive
Put the raw export into storage you control, such as Azure SQL with a simple search over it, keeping original Salesforce Ids, created and modified dates, owners, field history and files linked by Id. Make it read-only, log access, keep the raw files immutable, and agree a retention period. This is what an auditor will ask for, whatever ends up in Dynamics 365.
Migrate live data first, from the export
Load what people work with every day into Dynamics 365 first: users, accounts, contacts, open opportunities, open cases and active products, each reconciled by count before the next. Because the source is your export, not the live org, the Salesforce contract end no longer drives the migration timetable.
Add history and files in later stages
Bring recent closed records and activities next, then decide object by object whether older history moves into Dynamics 365 or stays in the archive, linked from the Dynamics record by its Salesforce Id. Files are their own stage, with their own storage decision.
Scoping the staged migration and comparing partner quotes for it is covered in our guide to migrating to Dynamics 365 from a legacy CRM, and where Salesforce Files and attachments should land in Dataverse without a storage surprise is covered in migrating attachments and notes into Dataverse.
What are the common pitfalls in a Salesforce to Dataverse migration?
Treating it as a straight copy instead of a remodel
Salesforce and Dataverse do not share a data model. Objects become tables, fields become columns, picklists become choices, and Salesforce master-detail and lookup relationships have to be re-expressed as Dataverse lookups or many-to-many relationships. Trying to lift and shift the schema one to one produces a target that fights the platform. The mapping has to be designed deliberately, table by table, before a single row moves.
Losing record identity and breaking relationships
Dataverse keys every row on a GUID, while Salesforce uses its own 15 and 18 character Ids. If you do not carry the original Salesforce Id across, child records cannot find their parents and integrations that referenced the old Ids break. The fix is to keep the source Id as a dedicated column with an alternate key so relationships resolve reliably and re-runs stay idempotent.
Ignoring attachments and files until the end
Salesforce Notes, Attachments, and Files (ContentVersion and ContentDocument) do not come across with the record data and are usually the largest and slowest part of the move. Left to the end they blow the timeline. They need their own plan up front: where they land in Dataverse (notes, file columns, or SharePoint), how they are batched, and how throughput and size limits are handled.
Expecting to write into the Dataverse audit log
Dataverse audit history is system managed and cannot be back-filled with historical Salesforce field-history rows. Teams that assume they can rewrite the audit trail lose that history. The workable approach is to preserve original created and modified dates on the live records and archive the historical change log into a read-only table or an external store, so the past is retained without pretending it was audited in Dataverse.
Running the load with automation live
Leaving plugins, real-time workflows, Power Automate flows, and duplicate detection switched on during a bulk load fires side effects on every row, overwrites created dates with load dates, corrupts owner assignment, and slows the whole run to a crawl. Automation has to be disabled for the migration and re-enabled deliberately afterwards, with created and modified metadata written explicitly.
No reconciliation, so nobody trusts the result
Migrating without record counts, control totals, and spot checks means defects surface only after go-live, when the business has already started working in the new system. Every load needs a reconciliation step that compares source and target counts per table, samples key records field by field, and reports what did not import and why before cutover is signed off.
What do fifty custom objects really mean for a Salesforce to Dynamics 365 migration?
Fifty custom objects is not fifty tables to rebuild in Dataverse. It is fifty decisions about what still earns its place, and the fastest route to a fixed date such as a new fiscal year is to migrate the objects that carry live process and archive the rest read-only. The sequence is an object inventory with record and usage counts, a relationship and dependency map, field-level mapping signed off by a named business owner per object, staged loads with reconciliation after each stage, and audit-trail preservation for anything finance relies on. The work that always overruns is attachments, activity history, formula fields with no Dataverse equivalent, and validation logic hidden in Apex.
Each object in the inventory gets one of five decisions. The signal on the left comes from usage counts and the dependency map, not from whoever built the object originally.
| What the inventory shows | Decision | Why |
|---|---|---|
| Records created or edited in recent months, used by a live process, report or integration | Migrate and rebuild as a Dataverse table | It carries process. Users will look for it on day one, and the automation around it has to be rebuilt too. |
| Historical records only, still needed for reference, audit or occasional lookup | Archive read-only | A read-only Dataverse table or an external store keeps the data reachable without designing forms, security and automation for an object nobody edits. |
| Low record count, no recent activity, no report or integration depending on it | Export once and retire | Keep the extract for the record and do not carry it into the new system. The business owner confirms this in writing. |
| Duplicates a standard concept such as a custom object built before a standard one existed | Merge into the Dataverse standard table | Mapping it to a standard table such as account, contact, case or opportunity avoids rebuilding a workaround the platform no longer needs. |
| Junction object that only links two other objects | Re-express as a relationship | A plain many-to-many link can become a Dataverse many-to-many relationship; a junction that carries its own fields stays a table. |
The scoping sequence
Object inventory with usage counts
List every custom object with its record count, last created and last modified dates, the number of active users touching it, and the reports, flows, Apex classes and integrations that reference it. Counts and dates come from the org itself, not from memory. This is what turns a debate about fifty objects into a short list of objects that clearly carry process and a longer list that clearly do not.
Relationship and dependency map
Draw the lookups and master-detail relationships between the objects you are keeping, plus the automation that crosses them: Apex triggers, record-triggered flows, validation rules and roll-up summaries. An object you planned to archive often turns out to be the parent of one you are migrating, and it is far cheaper to find that on a diagram than in a failed load.
Field-level mapping with owner sign-off
For each migrated object, map every field to a Dataverse column, a choice, a lookup, or a documented decision not to bring it. A named business owner, not the migration team, signs off each object mapping. The sign-off is what stops scope reopening in week six, when someone discovers a field they assumed was coming.
Staged loads with reconciliation after each stage
Load in stages rather than one big run, and reconcile counts and sampled records after each stage before the next begins. The execution detail, including the load order across tables, is covered in the data migration guide linked below, so it is not repeated here.
Audit-trail preservation for finance-relevant objects
Objects that feed invoicing, commissions, contracts or revenue reporting need their change history kept, not only their current values. Extract the Salesforce field history for those objects before the org is switched off, store it read-only with the original user and timestamp on each change, and turn on Dataverse auditing for the rebuilt tables from go-live so the trail continues without a gap.
This section is the scoping that has to happen before the load itself starts.
What always overruns on a heavily customised Salesforce org?
These four are where plans built from an object list alone fall behind. Budget time for each of them explicitly in the scoping, rather than hoping the tool handles them.
Attachments and files
Files, legacy attachments and the links that share one file across several records are a separate extract with their own volume, throughput and storage decisions. Scoping them by record count rather than by total size is the usual underestimate.
Activity history
Salesforce tasks and events point at a person and a related record through polymorphic lookups that can target many objects, including your custom ones. Resolving each one to a Dataverse regarding record and activity parties means every parent must already be loaded, which is why activities overrun on heavily customised orgs.
Formula fields with no destination equivalent
Dataverse has calculated, rollup and formula columns, but not every Salesforce formula function or cross-object reference translates directly. Each formula field needs a decision: rebuild the logic, store the historical value as plain data, or drop it. There is no bulk answer.
Validation logic hidden in Apex
Validation rules are visible in setup. Checks buried inside Apex triggers and classes are not, and they only surface when someone reads the code. Every rule found there has to be rebuilt as a business rule, a plug-in or a flow, or consciously dropped, and it has to be found before users test rather than after go-live.
If the inventory shows that most of the customisation exists to work around licensing or platform limits rather than to run a real process, it is worth asking whether a custom-built CRM fits better than a second licensed platform.
How do you plan and execute the migration step by step?
Work through these in order. The early steps profile the source and design the target; the later steps move the data in dependency order, preserve its history, and validate it before go-live.
Where the problem is speed rather than fidelity, the service protection limits, bulk messages, and batching are covered in fast Dataverse bulk import for millions of rows.
Profile the Salesforce org before you design anything
Inventory every object, custom field, picklist, record type, relationship, and automation in scope, along with row counts and file volumes. Identify what is actually used versus what has accumulated over the years. This profile decides what is worth migrating, what should be archived, and where the real volume and complexity sit, so the plan is built on the org as it is rather than as it is assumed to be.
Design the Dataverse target model and field-level mapping
Map each Salesforce object to a Dataverse table and each field to a column and data type, converting picklists to choices, record types to the equivalent Dataverse pattern, and Salesforce relationships to Dataverse lookups or many-to-many relationships. Decide owner, business unit, and security mapping at the same time. Document the mapping table by table so it becomes the contract that both the build and the migration scripts follow.
Set the record-ID strategy up front
Add a dedicated column such as Salesforce Id on every migrated table and define it as an alternate key. Loading the original Id lets you resolve parent-child relationships by the source key, keep the migration idempotent so failed batches can be safely re-run, and preserve references from any integration that still points at the old Ids. Where you need deterministic GUIDs, set the Dataverse primary key explicitly on create rather than letting it be generated.
Sequence the load by dependency
Load reference and parent data before the records that depend on it: users and business units first, then reference tables, then parents ahead of children. Resolve every relationship through the alternate key on the source Id rather than trying to know the new GUIDs in advance. A correct sequence is what prevents orphaned child records and lets large tables load in dependency order without re-work.
Migrate notes, attachments, and files as their own workstream
Move Salesforce Notes, Attachments, and Files into their chosen Dataverse home (notes, file columns, or SharePoint document locations) in batches, associated back to their parent records through the source Id. Plan for base64 size, storage, and API throughput so the file volume does not stall the run, and reconcile file counts against the source the same way you reconcile records.
Preserve created dates and archive the history
Write the original creation date to the created-on field using the supported override so records keep their true age, and map created-by and modified-by to the migrated users where possible. For field-level history, load the Salesforce change log into a read-only archive table or external store rather than attempting to write the Dataverse audit log, which is system managed. This keeps history available without misrepresenting it.
Disable automation, load in batches, then re-enable
Turn off plugins, real-time workflows, Power Automate flows, and duplicate detection for the migration so no side effects fire and metadata is not overwritten. Load in controlled batches with retry and logging so throttling and transient failures are handled cleanly. Once the data is in and validated, re-enable automation deliberately and confirm it behaves correctly against the migrated records.
Reconcile, validate, and run a controlled cutover
Compare source and target counts per table, sample key records field by field, and produce an exception report of anything that did not import and why. Rehearse the full run in a sandbox, then plan a cutover with a final delta load so records changed in Salesforce during the migration window are brought across. Only sign off go-live once reconciliation is clean and the business has validated its critical processes.
What do teams ask about Salesforce to Dynamics 365 migration?
How do you preserve record IDs when migrating from Salesforce to Dynamics 365?
Dataverse keys every row on a GUID and does not reuse the Salesforce 15 or 18 character Id, so the practice is to store the original Salesforce Id in a dedicated column and define it as an alternate key. Relationships are then resolved by that source Id, which keeps parent-child links intact, lets integrations that still reference the old Ids keep working, and makes the migration idempotent so failed batches can be re-run safely. Where deterministic GUIDs are needed, you can set the Dataverse primary key explicitly on create.
How are Salesforce attachments and files migrated to Dataverse?
Salesforce Notes, Attachments, and Files (ContentVersion and ContentDocument) are a separate workstream from the record data because they are usually the largest and slowest part of the move. They are migrated in batches into their chosen Dataverse home, which is typically notes, file columns, or a SharePoint document location, and associated back to their parent records through the source Id. The plan has to account for base64 size, storage, and API throughput, and file counts are reconciled against the source just like records.
Can Salesforce audit history be migrated into the Dynamics 365 audit log?
No. The Dataverse audit log is system managed and cannot be back-filled with historical Salesforce field-history rows. The supported approach is to preserve the original created and modified dates on the live records so they keep their true age, and to archive the Salesforce change log into a read-only table or an external store. That keeps the historical record available for reference without misrepresenting it as native Dataverse audit data.
Our Salesforce contract ends in weeks. How do we move to Dynamics 365 and keep history for audit?
Separate the data deadline from the migration. In the first 72 hours start a complete export, including field history, attachments and files plus the metadata, verify it against counts from the org, and load it unchanged into a read-only archive you control. Then migrate live data first into Dynamics 365, in stages, from that export, and decide object by object whether older history moves or stays in the archive.
Can a full Salesforce to Dynamics 365 migration be done in a few weeks?
Rarely, and not safely. Mapping with owner sign-off, file and activity history loads, building security, automation and integrations, staged reconciliation, a full-volume rehearsal and training all take longer than a few weeks. What can be done in weeks is a verified complete export and an audit-grade archive, which removes the contract deadline from the migration.
What does an audit-grade archive of Salesforce data need?
The raw export kept unchanged with original Salesforce Ids, created and modified dates, owners, field history and files linked by Id; a record of who exported what and when with verified counts; read-only access with logging; immutable copies of the raw files; a simple search; and an agreed retention period.
What are the most common pitfalls in a Salesforce to Dynamics 365 migration?
The recurring ones are treating the move as a straight schema copy instead of a deliberate remodel, losing record identity so relationships break, leaving attachments and files until the end, expecting to write into the audit log, and running the load with plugins and flows still live so side effects overwrite created dates and owners. Skipping reconciliation is the other big one, because defects then surface only after go-live. Disabling automation during the load and reconciling counts before cutover avoids most of them.
Can Solzet run a large Salesforce to Dynamics 365 migration?
Yes. Solzet is a Dynamics 365 Customer Engagement and Power Platform consultancy based in Yerevan, Armenia, and cross-platform data migration is a regular part of that implementation and project rescue work. We profile the source org, design the Dataverse target model and mapping, build the ID and relationship strategy, migrate records and files in dependency order, preserve created dates, and reconcile before cutover. The work is delivered directly or on a B2B and white-label basis for other Microsoft partners.
How do you migrate a heavily customised Salesforce org with dozens of custom objects to Dynamics 365?
Treat each custom object as a decision rather than a table. Inventory every object with record counts, last activity dates and dependencies, migrate the objects that carry live process, archive historical ones read-only, and retire the rest with a one-time export. Then build a dependency map, get field-level mapping signed off by a named business owner per object, load in reconciled stages, and preserve change history for finance-relevant objects.
Should every Salesforce custom object be rebuilt in Dataverse?
No. Rebuilding objects nobody uses adds forms, security, automation and testing for no business return, and it is usually what makes a fixed deadline impossible. Objects with recent activity and a live process are migrated. Objects needed only for reference are archived read-only. Objects with no recent use and no dependencies are exported once and retired, with the business owner confirming that decision in writing.
What usually overruns when migrating custom Salesforce objects to Dynamics 365?
Four things. Attachments and files, because they are scoped by record count rather than size. Activity history, because polymorphic task and event lookups need every parent loaded first. Formula fields that have no direct Dataverse equivalent and need a per-field decision. And validation logic buried in Apex triggers and classes, which does not appear in setup and has to be found in the code and rebuilt or dropped.
Related services: Dynamics 365 consulting, Power Platform consulting, white-label Dynamics 365 subcontracting and custom CRM development.
Planning a move off Salesforce?
Solzet plans and runs Salesforce to Dynamics 365 migrations as part of its Customer Engagement and Power Platform work. Tell us the size and shape of your Salesforce org and we will map it to Dataverse and move it cleanly, directly or on a white-label basis for your team. We recommend the right solution: Dynamics 365 (Customer Engagement, Business Central or Finance and Operations), Power Platform, or a custom-built CRM. One team for CRM, ERP and the integration between them.