Moving from Salesforce to Dynamics 365: A Practical Data Migration Guide
How teams actually move years of Salesforce data into Dynamics 365 Customer Engagement without losing the parts that matter, what genuinely cannot be carried across, and how the same mechanics apply when the source is Bitrix24 or another entry level CRM.
This practical guide outlines the key steps and pitfalls of migrating large volumes of historical data from Salesforce to Dynamics 365 Customer Engagement. It covers planning, data mapping, using tools like KingswaySoft or Azure Data Factory, preserving audit timestamps (overriddencreatedon), handling attachments, and executing validation to ensure a complete transition without data loss. The short answer to how people do this without losing everything: they decide up front, in writing, exactly what "everything" is, they accept that a handful of things genuinely cannot be carried across, and they design around those instead of discovering them during cutover weekend.
The same mechanics apply when the source is an entry level CRM rather than Salesforce. If you are migrating from Bitrix24, the identity keys, the created date handling, the dependency order, and the reconciliation are identical, but the source is shaped differently: a fixed entity set with user fields bolted on, integer ids that are only unique inside one entity type, dictionaries that live outside the records, and an automation layer with no export at all. The Bitrix24 to Dynamics 365 section below covers that shape, a cutover plan sized for a small team, and an honest comparison of what the move actually buys.
Migrating from Bitrix24 rather than Salesforce? Skip ahead to Bitrix24 to Dynamics 365 for the mapping, the cutover plan, and an honest comparison of the two. The sections in between apply to both. If this migration is a takeover rather than a first attempt, start with migration during partner transition or project rescue, which covers the forensic audit of what the previous team left behind and the incremental sequence that keeps the business running while the data moves. Moving off spreadsheets, an Access database, or an old in-house system with no budget for tools or a developer? Zero-budget migration from Excel, Access, or legacy systems is the free tool sequence for that, end to end. Consolidating two or three home-built CRMs into one environment? Migrating several legacy CRMs at once covers the shared staging layer, the legacy key, and the load order.
What actually gets lost in a Salesforce migration, and what stops it?
Data loss on these projects is rarely dramatic. Nothing gets deleted. Things arrive stripped of the context that made them useful: a created date, a parent, an owner, a currency. Each of these has one specific mechanic behind it and one decision that prevents it, and every one of those decisions has to be made before the first batch runs.
| What goes missing | Why it happens | What prevents it |
|---|---|---|
| Every record shows the same created date: the day you loaded it | Dataverse stamps createdon at insert. The only way to change it is to supply overriddencreatedon in the same create request, and it cannot be set on an update afterwards. | Set overriddencreatedon on create, with the override privilege granted to the migration account before the first batch runs. A record loaded without it keeps the load date permanently. |
| Child records lose their parent, or attach to the wrong one | Dataverse keys rows on a GUID it generates. If the load resolves relationships by name, by email, or by a spreadsheet of new GUIDs, anything ambiguous silently lands somewhere wrong. | Carry the Salesforce 18 character Id into a dedicated column on every table, define it as an alternate key, and resolve every lookup through that key rather than through a display value. |
| Records that differ only in letter case collide during upsert | The Salesforce 15 character Id is case sensitive, so two distinct records can differ only in capitalization. Dataverse alternate key matching is not case sensitive, so those two become one. | Always extract and store the 18 character Id, never the 15 character form. This is a one line decision at extract time and an unrecoverable data merge if it is missed. |
| Notes, attachments, and files never arrive | They are a separate object graph in Salesforce (Attachment, Note, ContentVersion and ContentDocumentLink) and they do not come across with the record data. They are also the slowest part of the move. | Plan files as their own workstream from day one, with their own extract, their own throughput budget, their own Dataverse destination, and their own count and byte level reconciliation. |
| Owners collapse onto the migration account | Salesforce owners include people who have left. Their user records are inactive, so there is nothing valid to assign to in Dataverse and the load falls back to whoever is running it. | Build the user mapping before any data moves, and agree an explicit named fallback owner per team for leavers, so unassigned work is visible instead of hidden behind one service account. |
| Amounts change value or land in the wrong currency | A Dataverse money column needs transactioncurrencyid and an exchange rate. Load an amount without them and it is interpreted as base currency, and the base value columns are recalculated from that. | Map CurrencyIsoCode to the Dataverse transaction currency on every money bearing table, and reconcile with control totals per currency rather than only with row counts. |
| Long text is truncated at load | A standard Dataverse text column tops out at 4000 characters. A Salesforce long text area holds far more, and most loaders will happily cut the value or fail the row. | Profile the real maximum length per field in the source, not the declared length, and provision multiline text columns where the data actually needs them. |
| Closed work reopens itself | Many tables will not accept an arbitrary state on create, and closed opportunities and resolved cases have their own closure semantics rather than a plain status field. | Load records active, then run a deliberate second pass that sets state and status, and use the proper close operations for opportunities and cases so the closure is real. |
| Field history disappears | The Dataverse audit log is system managed. There is no supported way to write historical Salesforce field history rows into it, whatever a migration tool appears to offer. | Archive the Salesforce history objects into a read only Dataverse table or an external store, and say plainly in the plan that this is an archive rather than native audit. |
| Work done during the migration window is lost | Salesforce stays live while the bulk load runs. Anything created or edited between the extract and the switch exists only in the source unless somebody goes looking for it. | Design the delta load at the same time as the bulk load, using LastModifiedDate on the source and upsert on the alternate key so the delta is a repeatable operation and not a rescue. |
Notice how many of these are one way doors. Created dates, identity keys, and letter case on the source Id all have to be right in the create request, because there is no update that fixes them afterwards. That is why the sequencing below puts the boring decisions first.
What can you preserve in Dataverse, and what can you not?
The most useful thing a migration plan can contain is an honest list of what Dataverse will and will not carry. Several of the rows below are a no, or a yes with a condition attached, and knowing which on day one is worth more than any tool choice, because it turns a cutover surprise into a design decision.
| What | Preserved? | How it works in Dataverse |
|---|---|---|
| Original created date | Yes, on create only | Supply overriddencreatedon in the create request as a UTC value. Dataverse writes it through to createdon and keeps overriddencreatedon as the record of the override. The migration account needs the privilege that allows overriding created on and created by during import, and the value cannot be applied later, so a table loaded without it has to be reloaded. |
| Original created by user | Yes, with impersonation | Send the create as the migrated user by setting the caller on the request. The migration account needs the act on behalf of another user privilege, and the impersonated user has to exist and be enabled in Dataverse at the moment the row is written, which is why leavers have to be dealt with in the user mapping rather than at load time. |
| Original last modified date and user | No | Dataverse always stamps modifiedon and modifiedby with the load. There is no override. Where the business genuinely uses last modified, land the source values in custom columns such as legacy modified on and legacy modified by, and put them on the form so nobody is misled by the native fields. |
| Record owner | Yes | Set ownerid on create, resolved through the user and team mapping. Decide the fallback for inactive owners before the load rather than during it, and make the fallback a real named team so unowned work surfaces. |
| Record identity | Not the same value, but reliably linked | Dataverse rows are keyed on a GUID. Keep the Salesforce 18 character Id in a dedicated column with an alternate key on it, resolve all relationships through that key, and the migration becomes idempotent and re-runnable. Where an integration truly needs a deterministic GUID, set the primary key explicitly on create. |
| Open and closed state | Yes, in a second pass | Create records active, then set statecode and statuscode in a follow up pass, using the proper close operations for opportunities and cases. Attempting to set final state on create is the usual cause of closed work coming back to life in the new system. |
| Notes, attachments, and files | Yes, as a separate workstream | Extract Salesforce Attachment, Note, and ContentVersion content, then load into the Dataverse destination you chose: notes, file columns, or a SharePoint document location, associated to the parent through the source Id. Budget for encoding overhead, file storage capacity, and API throughput, and reconcile counts and total bytes. |
| Field level history | No, archive instead | The Dataverse audit log cannot be back-filled. Load the Salesforce history objects into a read only table or an external store, and keep audit switched off during the migration so the load itself does not fill your log capacity with entries nobody will ever read. |
| Formula and roll-up values | Recalculated, not copied | Dataverse calculated and roll-up columns are derived, so loading a value into them is not possible or not durable. Either re-express the logic in Dataverse and let it calculate, or, where the historical number matters as a point in time fact, load it into a plain column and stop calling it a formula. |
| Record types, sharing, and validation rules | Redesigned, not migrated | These have no one to one equivalent. Record types become some combination of forms, business process flows, choice columns, and security roles. Salesforce sharing becomes business units, teams, and access. Plan this as design work in the build, not as a data mapping row. |
The single most expensive mistake in this table is overriddencreatedon. It works only in the create request, so a table that goes in without it cannot be corrected by an update: it has to be deleted and reloaded, with every child relationship rebuilt behind it. Verify it on the first batch of the first table, not at the end.
Which migration tools should you choose?
There is no single tool that does a large Salesforce to Dataverse migration well from end to end. The bulk relational load, the file workstream, and the state and closure pass have different requirements, and the projects that go smoothly tend to be the ones that admitted this early rather than bending one tool around every problem.
KingswaySoft SSIS Integration Toolkit
Good at: The default choice for a large Dynamics 365 Customer Engagement migration. Purpose built Dataverse source and destination components, upsert on alternate keys, lookup resolution against the target, multithreaded writes, per row error output, and native handling of notes, attachments, and impersonation. Packages are code you can re-run, version, and hand over.
Where it runs out: Licensed, and it needs SSIS and Visual Studio, so somebody has to own that environment. It is a developer tool rather than something a business analyst drives, and package design quality decides your throughput.
Azure Data Factory or Synapse pipelines
Good at: Strong where the migration is already cloud shaped: large extracts staged into Azure storage or a SQL database, transformation at scale, scheduled and monitored runs, and a Dataverse sink that supports upsert against an alternate key. Good fit when the same platform will carry ongoing integration after go live.
Where it runs out: Thinner on the Dataverse specific mechanics. Relationship resolution, impersonated creates, and note and attachment handling take more building than they do in a purpose built toolkit, so complexity moves into the staging layer.
Dataverse dataflows (Power Query)
Good at: Low friction and in the box. Good for reference and configuration data, smaller tables, and one off corrective loads, with key column mapping for upsert and a familiar transformation experience.
Where it runs out: Not the tool for the high volume tables or for anything needing overriddencreatedon, impersonation, or careful ordering. Refresh behaviour and error reporting get uncomfortable as volume grows.
Custom code against the Dataverse Web API or SDK
Good at: Complete control, which is what the awkward parts need: setting overriddencreatedon on create, impersonating the original owner, chaining requests, and the bulk create and update messages for throughput. Usually the right answer for files and for the state and closure pass.
Where it runs out: You own everything, including retry on service protection limits, batching, logging, and restartability. Written casually it becomes the slowest and least observable part of the project.
Configuration Migration Tool
Good at: The supported way to move configuration and reference data between Dataverse environments with GUIDs preserved. Useful for moving your own mapping and lookup tables through dev, test, and production.
Where it runs out: Not a Salesforce migration tool and not for transactional volume. It belongs in the release process, not in the data workstream.
Whatever the mix, three properties matter more than the brand on the box. The load has to be idempotent, so a re-run corrects rather than duplicates. It has to log per row, so an exception report is a query and not an investigation. And it has to survive service protection limits by backing off and retrying, because a load that treats throttling as a failure will never finish a large table. If throughput rather than mapping is what is hurting, the batching, parallelism, and limit arithmetic are covered separately in fast Dataverse bulk import for millions of rows.
How does the migration actually run, step by step?
Steps 1 to 6 are decisions and preparation, and they are where migrations are won or lost. Steps 7 to 11 move the data. Steps 12 and 13 prove it and put it live. The order matters, because several of the early decisions cannot be revisited once rows exist.
Profile the Salesforce org as it really is
Inventory every object, custom field, picklist, record type, relationship, and automation in scope, with row counts, file counts, total file volume, and the genuine maximum length of every text field. Find what is actually used: fields populated on a handful of records, objects nobody has written to in years, automations that fire on create. This profile is what turns arguments about scope into a conversation about numbers.
Write the loss register and get it signed
Go table by table and record one of three decisions for each: migrated, archived, or deliberately dropped. Include the things that cannot be preserved natively, such as last modified metadata and field level history, and say where they land instead. This document is the reason a migration finishes without an argument, because everybody agreed in advance what "without losing everything" meant on this project.
Design the Dataverse target model and the field level mapping
Map each object to a table and each field to a column and data type. Picklists become choices with an agreed value set, multi-selects become choice columns, long text areas become multiline text sized from the profiled maximum, and Salesforce relationships become Dataverse lookups or many-to-many relationships. Decide owner, business unit, and security mapping at the same time, because those affect every row you write. The mapping document is the contract the build and the migration scripts both follow.
Fix the identity strategy on the 18 character Id
Add a Salesforce Id column to every migrated table, load the 18 character form, and define an alternate key on it. Never the 15 character form: it is case sensitive while Dataverse key matching is not, so two distinct records can merge into one during upsert and there is no clean recovery. With the key in place, every relationship resolves by source Id, failed batches re-run safely, and integrations that still point at the old Ids keep working.
Build the user, team, and currency mapping before any data moves
Produce a row per Salesforce user with the Dataverse user or team they map to, and an explicit decision for every leaver. Impersonated creates need the target user to exist and be enabled at load time, so this cannot be improvised mid run. Do the same for currencies: map CurrencyIsoCode to the Dataverse transaction currency and agree the exchange rate treatment, because money columns silently interpret an amount as base currency when no currency is supplied.
Choose the tool per workstream rather than one tool for everything
Use the purpose built toolkit or your pipeline platform for the bulk relational load, and accept that files, impersonated creates, and the state pass often want custom code against the Web API. KingswaySoft gives you Dataverse aware components and upsert on alternate keys inside SSIS. Azure Data Factory suits a staged, cloud native shape and doubles as your ongoing integration platform. Dataflows are for reference data and corrective loads. Whatever you pick, the load has to be re-runnable, logged per row, and restartable from a failure.
Turn off everything that reacts to a write
Disable plugin steps, real-time workflows, Power Automate flows, business rules, and duplicate detection rules for the duration of the load, and turn auditing off so the migration does not consume log capacity recording itself. Leave them on and every row fires side effects, ownership and dates get overwritten, throughput collapses, and the result is untrustworthy. Write down what was disabled so re-enabling it is a checklist rather than a memory test.
Load in dependency order, upserting on the alternate key
Users, business units, and teams first, then currencies and reference data, then parents before children, then activities and files last. Resolve every lookup through the source Id alternate key rather than a name or an email. Batch the writes, use the bulk operations where the tool supports them, and handle service protection limits properly: back off and retry on throttling rather than treating it as a failure, because a load that cannot survive being throttled cannot survive a large table.
Preserve created date and ownership in the same create request
Set overriddencreatedon and ownerid on create, and impersonate the original creator where created by matters, having granted the override and act on behalf of another user privileges to the migration account beforehand. There is no second chance here: overriddencreatedon cannot be applied on update, so a table that goes in without it has to be deleted and reloaded. Verify on the first batch, not on the last.
Migrate notes, attachments, and files as their own workstream
Extract Salesforce Attachment, Note, and ContentVersion content along with the ContentDocumentLink rows that say what they belong to, then load into the destination you chose in the mapping: Dataverse notes, file columns, or a SharePoint document location, associated to parents through the source Id. Run it in batches with per file logging, plan for encoding overhead and file storage capacity, and expect this to be the longest running part of the migration by a distance.
Run the second pass for state, closures, and everything create could not carry
With the data in and related, set statecode and statuscode, close opportunities and cases through the proper close operations so the closure is genuine rather than a status value, apply any values that depend on records loaded later, and load the history archive tables. Keep this pass as a separate, re-runnable job, because it is the one you will run again after every rehearsal.
Reconcile until the numbers agree, then rehearse the whole thing
Compare row counts per table, control totals on every money and numeric column broken down by currency, referential integrity checks for children with an empty parent lookup, file counts and total bytes, and a field by field sample of records the business chose rather than records you chose. Produce an exception report of everything that did not load and why. Then run the entire sequence end to end in a sandbox against full volume, because the rehearsal is what tells you how long cutover actually takes.
Cut over with a delta load, then re-enable automation deliberately
Freeze Salesforce, run the delta for everything changed since the bulk extract using LastModifiedDate and the same upsert on the alternate key, reconcile again, and only then re-enable plugins, flows, workflows, duplicate detection, and auditing, in that order and with a check after each. Turning audit on after the load rather than before it means the log starts clean on day one. Keep the source system readable for an agreed period, because the questions arrive in the first week and they are much cheaper to answer than to argue about.
What validation proves nothing was lost?
Row counts alone prove almost nothing. A table can have exactly the right number of rows while every created date is wrong, every owner is the same person, and half the amounts landed in the wrong currency. Each check below is written as the question it answers, so the reconciliation pack reads as evidence rather than as output.
| Check | Question it answers | What passing looks like |
|---|---|---|
| Row count per table, source against target | Did everything arrive? | Counts match, or every difference is explained by a line in the loss register and is present in the exception report. |
| Control totals on money and numeric columns, split by currency | Did the values survive intact? | Sums agree per currency. A count that matches while a total does not usually means currency or precision, not missing rows. |
| Referential integrity sweep | Did anything lose its parent? | Zero child records with an empty parent lookup where the source had one, and zero rows whose source Id did not resolve during the load. |
| Created date distribution | Did the history survive? | Created dates spread across the years the source covers. A spike on the load date means overriddencreatedon did not apply and that table needs reloading. |
| Ownership distribution | Did the records land with the right people? | Owners spread across the mapped users and teams. A large share sitting on the migration account means the user mapping failed quietly. |
| File count and total bytes | Did the attachments really come across? | Counts and byte totals agree per parent table, and a sample of files opens and renders correctly rather than merely existing as rows. |
| State and status breakdown | Is closed work still closed? | The open and closed split matches the source per table, and closed opportunities and resolved cases behave as closed in the user interface. |
| Field by field sample chosen by the business | Would the people who use this data accept it? | The business picks the records, not the migration team, and signs off each sampled record against the source screen. |
| Business process rehearsal on migrated data | Does the new system work with this data in it? | Key reports, views, and processes run against migrated records and produce numbers the business recognises. |
Two of these rows are the ones people skip and later regret. The created date distribution catches a failed overriddencreatedon while there is still time to reload, and the ownership distribution catches a broken user mapping before the sales team finds it. Both are a single grouped query and both should run after every batch, not once at the end.
How do you prove any migration worked before the old system is switched off?
The table above is written around a Salesforce source, but the gate is the same for any CRM, Access database, spreadsheet estate or line of business system. Business users will not do manual checks at scale, so the validation set has to be scripted and run for them: row counts per entity, financial and quantity totals per period, referential integrity on every relationship, a field by field comparison of sampled high value records, and activity and attachment counts per parent. Each entity owner then signs off the result. The old system is switched off only when a signed reconciliation report exists and a read only copy of the source has been retained.
- 1
Row counts per entity
For every entity in scope, count rows in the frozen source extract and in the target, filtered the same way. Every difference is either a line in the agreed exclusion or merge register or a failure.
- 2
Financial and quantity totals per period
Sum every money and quantity column per month or financial period, per currency, in source and target. A matching count with a different total points at currency, precision or a wrong mapping, not at missing rows.
- 3
Referential integrity on every relationship
For every lookup and parent relationship, count child records whose parent is empty or unresolved in the target where the source had one. The expected answer is zero, relationship by relationship.
- 4
Field by field comparison of sampled high value records
Take a sample weighted towards the records that matter most, the largest customers, open contracts, recent and high value transactions, plus records the business picks itself, and compare every mapped field against the source.
- 5
Activity and attachment counts per parent
Count notes, emails, tasks and files per parent record in source and target, and compare file bytes where you can. A correct total can hide activities that attached to the wrong parent, so compare per parent, not overall.
- 6
Business sign-off per entity
The owner of each entity, not the migration team, reviews the reconciliation results and the sampled records for that entity and signs them off. No signature, no decommission.
Script it so two people can run it and get the same numbers
A validation that only the migration engineer can run is an opinion, not evidence. Write every check as a query or script held in version control, parameterised by entity and period, and run it against a frozen, dated extract of the source rather than the live system, so the numbers cannot move between runs. On the Dataverse side that means the Web API or FetchXML, or the read only SQL endpoint where it is enabled in your environment; check current Microsoft documentation for what your tenant allows. The output is a dated report per run, not a screen somebody looked at.
Then have a second person, ideally from the client side, run the same scripts from a short runbook without help. If both runs produce identical figures, the process is repeatable and the report can be trusted by people who were not in the room. If they do not, fix the scripts before trusting any result, because the difference is usually a filter, a time zone or a period boundary that would otherwise have been argued about after decommission.
What is an acceptable variance, and what is a stop?
Agree the tolerances in writing before the first run. A tolerance chosen after seeing the result is always wide enough to pass.
| Check | Acceptable variance | Stop |
|---|---|---|
| Row counts | Differences fully explained by the exclusion and merge registers agreed before the run, with the record identifiers listed. | Any unexplained difference, however small, or a difference explained only after the run by someone looking for a reason. |
| Financial and quantity totals | Exact agreement per period and currency, or a rounding difference traceable to a documented precision change that was agreed in writing beforehand. | Any total that differs without a traceable cause, or a period where the count matches and the total does not. |
| Referential integrity | Zero orphans, or orphans that were already orphaned in the source and are listed as such. | Any child that had a parent in the source and has none in the target. |
| Sampled high value records | Formatting differences only, such as a phone number normalised, listed in the mapping document. | A wrong value in any field somebody makes a decision with: an amount, a date, a status, an owner, a contractual term. |
| Activities and attachments | Per parent counts match, apart from items deliberately archived and listed. | Items missing or attached to the wrong parent, or files that exist as rows but do not open. |
The decommission gate
Two artefacts, both required. First, a reconciliation report covering every entity in scope, with the results of each check, every explained variance and its register entry, the sampled records, and a signature from each entity owner and from whoever is accountable for the data overall. Second, a retained read only copy of the source: the old system switched to read only, and before its licence or hosting ends, a complete export in an open format stored with the reconciliation report beside it.
The old system stays read only at least until the first full reporting or financial period has closed on the new system, and in practice until the audit or regulatory cycle that relies on migrated data has passed. The retained export is then kept for as long as your retention policy, auditors or regulator require. Put the switch-off date and its owner in writing at cutover, so the decision is not made for you by a renewal invoice. If a migration has already been switched off without this gate, our guide to recovering a failed Dynamics 365 migration covers what can still be reconstructed, and where the target is not Microsoft at all, the same gate applies to a custom CRM migration.
How do you migrate several legacy CRMs into Dynamics 365 at once?
When several legacy CRMs move into Dynamics 365 at once, the load order is what keeps relationships intact: reference data first, then accounts and contacts upserted on alternate keys, then the records parented to them, then activities, then attachments, with each pass reconciled before the next one starts. Before any of that, every source is transformed into one canonical staging layer, so three different formats become one shape outside Dataverse. Every staged row carries its legacy identifier, prefixed with the source system, which makes any pass safe to re-run as an upsert. Cleanup and deduplication happen in staging, never in Dataverse, and closed historical records nobody works on are archived rather than loaded.
Everything in the procedure above still applies: preserved created dates, the user mapping, logic switched off during the load, the state pass, and the validation checks. What changes with several sources is that they overlap. The same customer usually exists in two or three of them under different spellings and different keys, one system is a database you can query, another is a set of exports, and the third only speaks through an API. Building a separate pipeline from each source straight into Dataverse triples the mapping work and pushes deduplication into the target, which is the one place it cannot be repeated on the next rehearsal.
One canonical staging layer for three source formats
The staging layer is a relational database outside Dataverse, organised in layers so every decision between a raw extract and a Dataverse write is visible and repeatable. Each source gets its own mapping into one shared model, and from that point the migration no longer cares how many systems it started with.
| Layer | What it holds | The rule that keeps it trustworthy |
|---|---|---|
| Raw landing, one area per source | Each system exactly as it was extracted: a database copy from one, CSV or spreadsheet exports from another, JSON pages from an API on the third. Nothing is renamed, typed, or fixed here. | Read only. A fresh extract replaces the whole area, so there is always a copy of what each source actually said on the day it was taken. |
| Canonical model | One table per target entity (accounts, contacts, the parented records, activities, and a file manifest) in a shape close to the Dataverse tables, with one mapping per source feeding the same columns. Choice values are translated from three vocabularies into the target option set here. | Every source maps into the same columns with the same types. A value that does not map is rejected into an exception table, never allowed to create a new option later. |
| Match and survivorship | The cross-source match keys, the cluster each duplicate belongs to, the surviving record per cluster, the column level rule for which source wins, and a cross-reference from every legacy identifier to its survivor. | Decisions are stored as data, not taken by hand, so the next rehearsal reproduces exactly the same merges and the business can review them before cutover. |
| Load-ready views, one per pass | Exactly what each pass sends to Dataverse, with every lookup expressed as the alternate key value of the parent rather than as a GUID or a display name. | Generated from the layers above and never edited. If a load-ready row is wrong, the fix goes into the mapping or the survivorship rule and the view is regenerated. |
| Reconciliation log | Per pass and per source: rows expected, loaded, rejected, and deliberately excluded, with a reason for every rejected row and a pointer to the register entry for every excluded one. | A pass is accepted when the numbers balance, not when the job reports success. The next pass does not start until this one is signed. |
A legacy identifier on every row, so every pass can be re-run
Integer ids from three systems collide, so the key is built at staging time from the source, the entity, and the original id, for example CRMA-ACC-1043 and CRMB-ACC-1043 as two distinct values. It goes into a text column on every target table, defined as an alternate key before the first load, and every pass writes as an upsert on it. Create the keys early and confirm each key index is active: Dataverse builds the index as a system job, and that job fails if duplicate values are already in the column.
Merged records need one more step. When a customer from the second system merges into the survivor from the first, the survivor keeps a single key, and the staging cross-reference maps the other legacy ids onto it. Child lookups are resolved through that cross-reference, so a contact or case from the second system lands under the account that survived from the first. Where users will still search by an old reference, carry the merged ids in a plain text column on the record as well.
The load order that keeps relationships intact
Each pass has a reconciliation gate, checked per source as well as in total, because a total that balances can hide one system that under-delivered and another that over-delivered.
Reference data, users, and the cross-source user map
Currencies, business units, teams, choice values, and any product catalogue the later passes need, then the user map. With three sources the same person usually has three accounts under three spellings, so the map resolves every legacy owner id from every system to one enabled Dataverse user or to a named fallback team for leavers. Gate: every owner value in every staged table resolves, and the count of rows heading to a fallback team is known and accepted before the next pass.
Accounts and contacts, upserted on the legacy key
Load surviving accounts only, upserting on the legacy key column defined as an alternate key, then contacts with their parent account resolved through that same key via the cross-reference. Self-referencing lookups such as a parent account are set in a follow-up update inside the same pass, because the parent may not exist yet when its child is written. Gate: loaded survivors equal staged survivors per source, every merged record is explained by the cross-reference, and no contact has an empty parent where its source had one.
Parented records: opportunities, cases, quotes, and custom tables
Everything that hangs off an account or a contact, loaded with lookups expressed as alternate key values so a case from the second system attaches to the account that survived from the first. Load them open and leave closure to the state pass described in the main procedure. Gate: counts per table and per source, control totals on money columns per currency, and zero unresolved parent lookups that are not already in the exception table.
Activities, with regarding and participants resolved through keys
Tasks, phone calls, appointments, and emails within the history depth agreed in the register, each with its regarding record resolved through the parent key and its participants resolved to users, contacts, or accounts. Where an email participant does not resolve, keep the address rather than dropping the party. This is usually the largest pass, and it is where throughput starts to matter. Gate: counts per activity type, per source, and per year, because gaps in a multi-source load tend to cluster by date and by system.
Attachments and documents from the staged file manifest
Files load last because they need their parent rows to exist. The staging layer holds a manifest per file: source, legacy parent key, file name, size, and a content hash, so the pass knows what should arrive before anything is sent. Load into notes, file columns, or a SharePoint document location as the mapping decided. Gate: file count and total bytes per source and per parent table match the manifest, and a sample chosen by the business opens correctly.
State pass, delta, and the sign-off across all sources
Set final state and close records through the proper operations, run the delta from each source that stayed live after its extract, upserting on the same legacy key so the delta is a re-run rather than a special case, and reconcile once more per source. Only then re-enable the logic that was switched off for the load. Gate: the reconciliation log balances for every pass and every source, and the business signs a sample drawn from each system rather than from the easiest one.
The order decides correctness. How fast each pass runs, meaning bulk messages, parallelism, and throttling, is a separate question covered in fast Dataverse bulk import for millions of rows, and it is worth reading before the activities pass rather than after it.
Cleanup happens in staging, never in Dataverse
A correction made in Dataverse after a load is undone by the next re-run, fires whatever logic is switched back on, and leaves no record of why it was made. The same correction made in staging is a rule that runs on every rehearsal and on cutover. This is the cleanup that belongs there.
- Deduplicate across systems on normalised match keys, such as a company registration or tax number where it exists, email domain plus normalised name, and a single phone format, then apply column level survivorship: which source wins for the address, which for the phone, which for the owner.
- Translate three choice vocabularies into one target option set, and reject anything unmapped into the exception table instead of letting a loader invent options on the way in.
- Decide what happens to orphans in each source before the load: a child whose parent is missing in its own system is attached to a named holding record, archived, or dropped, and the decision is written in the register.
- Convert every date to UTC per source, because three systems rarely store time the same way, and profile the real maximum length of every text field so Dataverse columns are sized from evidence.
- Remove test records, placeholder accounts, and empty activities, which every home-built CRM accumulates and which otherwise cost load time and user trust on day one.
What happens to closed historical records
Three old systems hold a lot of closed work, and loading all of it makes every rehearsal slower and every view noisier. The decision is made per kind of record, by who reads it and how often, and written into the loss register before the first pass.
| Record | Decision | Why |
|---|---|---|
| Open work and anything a user acts on this year | Load live | It belongs on the record and in views, with the created date preserved on create and the owner resolved through the user map. |
| Closed records inside the agreed history window | Load live, closed | Service history, renewals, and account reviews read these. Load them open, then close them in the state pass so the closure is real rather than a status value. |
| Closed records older than the window, still asked about occasionally | Archive, searchable | A read only archive linked by the legacy key, either a flattened table in Dataverse or an external store with a simple report, keeps them findable without paying for them on every rehearsal and in live capacity. |
| Closed history for customers that no longer exist in any source | Archive or drop, with sign-off | Loading these creates accounts nobody owns and duplicates nobody can resolve. Keep an archive if audit or regulation needs it, otherwise record the drop in the register. |
| Field change logs and legacy audit tables | Archive | The Dataverse audit log cannot be back-filled from any source, so these live in the archive whichever system they came from. |
If the project is still at the pricing stage, the discovery outputs and the grid for comparing quotes on a legacy CRM migration are on migrating to Dynamics 365 from a legacy CRM. And if the sources are home-built because the process genuinely does not fit a packaged CRM, a custom-built CRM is a legitimate destination, and the staging layer and load order carry over unchanged.
How do you migrate during a partner transition or project rescue?
Not every migration is the first attempt. A large share of the ones we are called into arrive with a partner relationship already ended, or an implementation that stalled somewhere between design and go live, and the data still has to move while the business keeps trading. That changes the plan in two specific ways. The source model can no longer be taken on trust, because the documentation describes what was intended rather than what was built. And the migration cannot be one weekend, because the people who would have to live through that weekend have already been asked to wait once.
The order is what makes this work. A migration is not the first move in a partner transition, it is roughly the third. First the environment is stabilized so it stops changing underneath you, then the existing data model is audited forensically, and only then does data move, in slices that each prove themselves. Running it the other way round, migrating first and investigating when something looks wrong, is how a rescue becomes the second failed project on the same system. The full intervention, including the hour by hour stabilization sequence and the order control changes hands in, is set out on our Dynamics 365 project rescue and takeover service page. What follows is the part of that method which decides a migration.
The forensic audit of the existing data model
On a first attempt the profiling step reads the source system. On a takeover it has to read two systems, and it has to read the target with suspicion, because something has already been built there and nobody can tell you reliably what. We read the environment itself rather than the documentation about it. These are the questions the audit answers before a mapping document is opened, in the order they change the plan.
| What the audit asks | Why it decides the plan | What the answer produces |
|---|---|---|
| Which environment is actually the source of truth? | On a stalled project there is rarely one. Production, a sandbox somebody kept working in after the freeze, and a developer environment that was never mentioned in the handover all hold different versions of the model, and the last real changes are often in the one that was left off the list. | A named environment per purpose with the rest marked read only, so the extract and the target model are both taken from a system somebody has agreed is authoritative. |
| What is in the unmanaged layer, and what was edited straight into production? | Solution layering tells you what the previous team did rather than what the design document says. Columns added directly in the live environment, unmanaged layers sitting over managed components, and default solution edits are invisible in a requirements pack and all of them decide what the mapping has to carry. | A component level list of what is managed, what is unmanaged, and what exists only in production. That list is the difference between the documented model and the real one. |
| Which tables and columns carry real data, and which were abandoned? | Half finished builds leave fields created for a phase that never shipped, tables written to for two months and then forgotten, and duplicate columns where the same problem was solved twice by two people. Carrying all of it forward is how a new model inherits the old confusion on day one. | Population counts and last written dates per column, so the decision to migrate, archive, or drop each one is a number rather than an opinion in a workshop. |
| Where does business logic write to data, and in what order? | In a troubled project the same record is usually touched by a plug-in, a real time flow, and a classic workflow at once, with nothing controlling the sequence. Every one of them fires during a load unless it is switched off, and any of them can overwrite the values you have just migrated. | An inventory of plug-in steps, flows, workflows, business rules, and duplicate detection rules with what each one writes, which becomes the disable and re-enable checklist for the load. |
| What writes into this system from outside, and what does it authenticate as? | Interfaces built during the previous engagement keep running after the relationship ends. Each authenticates as something: an application user, a service principal, or, more often than anyone expects, a named person who has already left. An interface nobody documented will keep inserting rows straight through your migration window. | A list of every inbound and outbound interface with its identity, its schedule, its secret or certificate expiry, and a decision on whether it is paused, re-pointed, or retired. |
| Has data already been loaded, and what did that load do to it? | Where a previous partner migrated something already, the two failures are almost always the same: created dates stamped at load time because overriddencreatedon was never used, and relationships resolved on a name or an email instead of a source id, so records sit under the wrong parents in a way spot checking does not surface. | A verdict per table on whether it can be corrected in place, has to be deleted and reloaded, or was never loaded at all. This is the finding that most often changes the shape of the entire plan. |
| Is the source system still live, still licensed, and still readable? | A partner transition often arrives next to a renewal date on the system being left behind, and that source is what every reconciliation and every question in the first weeks after go live gets checked against. | A dated answer on how long the source stays readable, which sets the outside edge of the migration timeline before anything else is scheduled around it. |
| What restore points exist, and until when? | If something in the target is actively damaging data, the remedy has an expiry date set by backup retention rather than by your project plan, and that window closes whether or not anybody is watching it. | A written record of the restore points available and the date each stops being available, so a recovery decision is made inside the window instead of discovered outside it. |
The row that changes budgets is the sixth one. Where a previous partner already loaded data, the created date question has no soft answer: overriddencreatedon works only in the create request, so a table that went in without it cannot be repaired by an update and has to be deleted and reloaded with every child relationship rebuilt behind it. Finding that in the audit is a scheduling problem. Finding it after the business has been working in those records for three months is a much more expensive one.
The risks that belong to a partner transition specifically
These are not general migration risks. They exist because the people who built the system are no longer in the room, and every one of them has an ordinary control that contains it once somebody has named it out loud.
| Risk | What it does to the migration | How it is contained |
|---|---|---|
| Undocumented customizations | A rule living in a plug-in nobody knew about, a value produced by a workflow rather than a formula, a requirement enforced in form script rather than on the column. None of it appears in a mapping document, and all of it either blocks the load or silently changes what gets loaded. | Take the component inventory from solution layers in the environment rather than from documentation, and disable everything that reacts to a write for the duration of the load. Where a customization turns out to be load bearing, it is re-expressed deliberately in the target instead of being inherited by accident. |
| Orphaned integrations | An interface still running on a schedule under the identity of somebody who left, writing into the tables you are mid-migration on, or pointed at an endpoint that was decommissioned so it fails quietly and nobody connects the missing records back to it. | Inventory every interface with the identity it runs as and its secret expiry, pause the inbound ones for the migration window with the owning team told in advance, and re-point or retire them one at a time after cutover with a check after each. |
| A half finished migration already sitting in the target | Some of the data is already loaded, with no alternate key on the source id, so the load is not re-runnable, duplicates arrive on every retry, and there is no reliable way to separate rows that came from the source from rows users typed in afterwards. | Add the source id column and the alternate key before another row moves, back-fill it wherever the origin can be established, and flag the rows users created during the gap so the delta load does not overwrite work the business has already done in the new system. |
| Solution source that exists nowhere outside the environment | Where the previous team never exported to a repository, the only copy of the customizations is the live environment, so there is nothing to review before the load, nothing to compare against afterwards, and nothing to restore to when a release wave breaks something. | Export the unpacked solution into a repository you own as the first item of work, before the migration rather than after. Until it exists there is no version of the system that survives the environment it runs in. |
| Ownership and access held by the outgoing party | Tenant administration, environment ownership, the app registrations the integrations authenticate through, and the repositories holding the source often sit with the previous partner, and clients regularly read that as the keys being gone. | They are not gone. The tenant belongs to the organization paying for it, and administration can be recovered through the tenant owner regardless of how the relationship ended. That transfer is an administrative act that starts before any technical work, and the order it happens in is set out on the rescue and takeover service page. |
| The knowledge that left with the team | No audit recovers which customization was a deliberate design decision and which was a workaround for something else, what was promised verbally and to whom, or which integration nobody dares touch and why. | Ask for one short, prepared handover conversation with the outgoing team even where the relationship has soured, with the questions written down in advance. It is the cheapest hour in the whole exercise and the one most often skipped. |
| A business that cannot stop while this gets fixed | A rescue arrives mid flight. Orders are still being taken, cases are still being raised, and the team has already been told once to wait for a plan. A single big bang cutover asks them to wait again, on a system that has already lost their confidence. | Migrate incrementally, in slices the business recognises, each with its own load, delta, and reconciliation. The sequence below is the one we use, and it is slower on paper and faster in practice. |
Incremental migration, so the business keeps running
The thirteen steps above assume a first attempt with a rehearsal window and a business willing to freeze for a weekend. A rescue has neither. The plan has to deliver something visible early, prove itself at every step, and never require the whole company to hold its breath at once. This is the sequence we use when the migration is part of a takeover.
Stabilize before anything moves
Nothing is migrated while the environment is still changing under you. Deployments into production are frozen, administration rights are cut back to the people who genuinely need them, direct edits in the live system stop, and production is copied into a sandbox so the current state is preserved. A migration planned against a moving environment is a plan that gets rewritten in week two. This is the same first gate as any takeover, and the full stabilization sequence, with the hours against each step, lives on the rescue and takeover service page rather than being repeated here.
Slice by business process, not by table size
The first slice should be something the business names for itself: the service pipeline, the field crews, the renewals book. That slice carries every table it needs, including the awkward ones, rather than all the accounts first and all the contacts second. Migrating table by table means nobody can use anything until the last table lands, and that wait is exactly what the previous attempt already asked of them.
Fix the identity strategy retrospectively, before the second slice
Where rows are already in the target from an earlier attempt, the source id column and the alternate key go on before anything else moves, and existing rows are back-filled wherever their origin can be established. Without that key every later load duplicates instead of upserting and no slice can be reconciled against the source. It is the least interesting change in this list and the one that makes everything after it re-runnable.
Run each slice as load, delta, reconcile, and only then release
A slice is not finished when the rows arrive. It is finished when row counts, control totals per currency, the referential integrity sweep, the created date distribution, and the ownership distribution all agree, and when the business has signed off a sample of records it chose rather than records you chose. The validation table above applies per slice instead of once at the end, and that is what makes an incremental plan safer than a single cutover rather than merely longer.
Decide which system owns which record while both are live
Between the first slice and the last, two systems are running and the failure mode is a record edited in both. Write down, per table and per slice, which system is authoritative, which direction any interim sync runs, and what happens to a record edited on the wrong side. A one way flow everybody understands beats a two way sync nobody can reason about at the moment the numbers disagree.
Move people a team at a time, and keep the old system readable
Users follow their slice. A team stops working in the old system on the day its slice is signed off, not on a date chosen six weeks earlier, and the old system stays readable, and paid for, for an agreed period afterwards. On a rescue this matters more than usual, because the team has already been let down once and what rebuilds confidence is seeing their own records, correct, in a system that works, before anybody asks them to trust the next slice.
Retire the previous build slice by slice, with dates and owners
As each slice goes live, the customizations, flows, and interfaces the old build used for it are turned off deliberately rather than left running, each with a date and a named owner against it. Half retired builds are where orphaned integrations come from in the first place, and a rescue that creates its own orphans has only moved the problem somewhere quieter.
What makes this safe is not a migration technique, it is the sequencing: stabilize, audit, then move data in slices that each reconcile before the next one starts. Our write up on taking over a Dynamics 365 project that went wrong describes the same methodology from the delivery side: reading a half built environment before touching it, keeping what earns its place, fixing what is hurting users today, and moving forward on real development, test, and production discipline instead of typing changes into the live system. A migration run on that foundation is a controlled sequence of small, reconciled steps. A migration run without it is the previous project repeating itself with newer data.
How do you migrate from Excel, Access, or legacy systems with zero budget?
Everything above assumes a project with a budget: a licensed toolkit, a staging database, somebody whose job it is to run the load. A large share of the people who need this guide have none of that. The source is a set of spreadsheets, an Access database somebody built years ago, or an export from a system nobody supports any more, and the person doing the migration is the operations manager or the one technical person in a ten person company, working evenings, with no money for tools and no developer to call. That migration is entirely doable, and this section is the sequence for it.
The tools are free and there are only three of them. Power Query, which you already have in Excel, does the cleaning and the shaping. The Data Import Wizard inside the model-driven app does the loading and gives you a per row failure log and a one click undo. XrmToolBox, the free community application, covers the rest: reading the target columns properly, querying to reconcile, and correcting or removing rows when an import goes wrong. Nothing below requires a licence, a developer environment, or code beyond a few SQL statements you can copy and adapt.
Power Query, in Excel or in a Dataverse dataflow
What it does here: The cleaning and shaping engine, and the only part of this you should do more than once. It profiles the real column contents, forces explicit data types, trims and normalises text, splits one wide sheet into the separate tables it should always have been, builds the source id column every later step depends on, and deduplicates on a normalised key. Every step is recorded, so when a load fails you fix the query and regenerate the file instead of hand editing a CSV nobody can reproduce.
Where it runs out: It is a preparation tool here, not the loader. Doing the cleaning in the worksheet instead of in the query is the single most common reason a small migration cannot be re-run, because the corrections exist only in one person memory and in one saved copy of one file.
The Data Import Wizard in the model-driven app
What it does here: The loader, free, in the box, and drivable by somebody who is not a developer. It takes a CSV or Excel file, maps columns to columns and choice values to choice values, saves that mapping as a reusable data map, runs server side, and writes an import job you can open afterwards to see exactly which rows failed and why. The two features that make it usable for a solo migration are the saved data map and the ability to delete everything a single import created.
Where it runs out: Upload size is capped, at 8 MB for a single file and 32 MB inside a zip, so a large export is split into chunks. It processes rows through the standard pipeline, so plug-ins, flows, business rules, and duplicate detection rules all fire per row. And it gives you no way to supply overriddencreatedon, so records loaded this way carry the load date as their created date.
XrmToolBox
What it does here: The free community desktop application that fills the gaps around the wizard, connecting to your environment with the same sign in you already use. Four plugins carry most of a small migration: Metadata Browser or Attribute Manager to read the true type, maximum length, required level, and choice values of every target column before you map anything; FetchXML Builder to write the counting and reconciliation queries; SQL 4 CDS to query, correct, and delete with familiar SQL statements; and Bulk Data Updater to fix one column across a filtered set of rows without reloading the table.
Where it runs out: It is community software rather than a Microsoft product, it runs with whatever privileges your account has, and nothing in it asks twice before writing. Connect it to a sandbox or a trial environment first, get used to running a select before you run a delete, and keep the statements you run in a text file so a bad one can be explained afterwards.
The step-by-step, with free tools only
This is the same spine as the thirteen step programme above, compressed to what one person can execute alone over a few evenings. The order is not a preference. Steps one to five are the decisions that cannot be revisited once rows exist in Dataverse, which is why they come before anything is loaded.
Decide which file is the truth
Before anything is cleaned, list every spreadsheet, Access database, and legacy export in circulation and mark each one authoritative, a copy, or a dead end. Two versions of the customer list with different row counts is the normal starting position, and picking between them is a business decision rather than a technical one. Write down the row count of each authoritative file today, because on a spreadsheet migration that number is the only reconciliation baseline you will ever have.
Take one clean export and leave it alone
Export each Access table separately to CSV with UTF-8 encoding rather than exporting a query that has already joined things together, because you want the underlying tables and their key columns, not a flattened report. Do the same with a legacy system: one file per entity, straight out. Keep that raw export read only in its own folder and never edit it, so there is always a copy of what the source actually said.
Read the target columns before you map anything
Open Metadata Browser or Attribute Manager in XrmToolBox and write down, for every Dataverse column you intend to fill, its real type, its maximum length, whether it is required, and for a choice column the exact option labels. Ten minutes here removes most of the mapping failures further down, because almost all of them are a text value being pushed at a column that is a choice, a lookup, a number, or a date.
Clean in Power Query, never in the sheet
Load the raw file into Power Query and do the work there: trim and clean whitespace, set an explicit type on every column, force identifiers, phone numbers, and postcodes to text so leading zeros survive and long numbers do not turn into scientific notation, standardise the date columns to one format, and split full name into first and last. The point is not tidiness. It is that the query is a recipe you can re-run against a fresh export, which is what makes a failed import recoverable instead of terminal.
Give every row a source id
Add a column in Power Query that gives each row a stable identifier of your own, such as ACC-0001 for companies and CON-0001 for people, and keep it in the file. Create a matching single line of text column in Dataverse, mark it as an alternate key, and map it in every import. This one column is what turns a re-run into an update rather than a second copy of everything, and it is what lets you find and delete exactly the rows one bad import created.
Deduplicate while the source is still in front of you
Build a normalised match key in Power Query, typically lowercased company name with punctuation and legal suffixes stripped, or the email address for people, then group on it to see the collisions. Decide which row survives and what gets merged into it while you can still read the original file, because the same decision taken after the load means editing live records in the CRM. Keep the list of merges: it is the answer when somebody asks later why there are fewer accounts than there were rows.
Split the sheet into the tables it should have been
One spreadsheet with company, contact, and deal columns on the same line becomes at least three files: accounts, contacts, and whatever the deal rows are. In the contacts file, refer to the parent by the account source id rather than by the company name, because the name is exactly what a lookup will fail or mismatch on. Do this in Power Query as three queries over the same source so a re-export regenerates all three consistently.
Load parents first, one file at a time
Accounts before contacts, contacts before anything that references them, and never two files in the same run. It feels slower and it is the reason recovery stays simple: when an import goes wrong you know which table it touched, and the delete is one job rather than an untangling exercise. Between each file, open the import job and confirm the counts before starting the next.
Map every column by hand and save the data map
Do not accept the automatic mapping. Walk the list, map each column deliberately, and pay attention to the columns the wizard has quietly left unmapped, because those are silently dropped rather than reported as an error. Map every choice value explicitly and decide on purpose whether unmapped values are allowed to create new options. Then save the mapping as a data map and re-use it, so the second attempt is identical to the first in everything except the data.
Import twenty rows before you import twenty thousand
Cut the first twenty rows into their own file, run the whole import with the real data map, then open the created records in the app and read them as a user would. Check the choice fields show labels rather than blanks, the dates are the dates you expected, the parent lookups point at the right companies, and the long text is not cut off. Almost every problem in the pitfalls table below is visible in twenty rows and invisible in a summary that says the job succeeded.
Reconcile with the same evidence a funded project uses
The validation section above does not stop applying because the tooling was free. Count rows per table against the baseline you wrote in step one, sum the money columns and compare the totals, run a FetchXML query for children with no parent, group by owner to prove the ownership is not all one account, and open a handful of records the business picked rather than ones you picked. Save those numbers, because they are what you show when somebody asks whether the migration was complete.
The pitfalls that catch people doing this without a developer
These are not exotic. They are the ordinary failures of a spreadsheet load, and what makes them dangerous is that most of them report themselves as a success. Each row below is written as the thing you will actually see, because a job that says it completed while quietly dropping a column is much harder to notice than one that fails outright.
| Pitfall | What you see | What prevents it |
|---|---|---|
| Duplicate detection rejects rows you meant to load | The import job finishes with a large failure count and a message about a duplicate record, usually on the second file or on a re-run after a partial failure. | Decide the rule before the load rather than during it. Deduplicate in Power Query on your own normalised key, turn the detection rules off for the import window, and turn them back on afterwards so they protect ongoing data entry instead of fighting the migration. |
| Duplicate detection is off and the same file goes in twice | Row counts are exactly double, or a table has two of everything created a few minutes apart, usually after an import that appeared to fail and was run again. | The source id column and the alternate key are the real protection here. With them in place a re-run updates the rows it already created, so a repeated import is harmless rather than a cleanup job. |
| A column is unmapped and disappears without an error | The import succeeds, the record count is right, and a field the business cares about is empty on every row. Nothing in the job log mentions it. | Review the mapping screen column by column and treat an unmapped column as a decision to be made rather than a default to accept. After the twenty row test, open a record and check the fields you care about are populated rather than trusting the success count. |
| Choice values arrive as new options nobody wanted | The status column now contains Active, active, and ACTIVE as three separate options, or a set of values that came straight from the spreadsheet with its typos intact. | Normalise the values in Power Query first so the file contains one spelling per value, then map each one to an existing option explicitly and only allow new options to be created where you genuinely intend it. |
| Lookups resolve to the wrong parent, or to nothing | Contacts sit under a company with a similar name, or the parent field is empty on rows that definitely had a company in the spreadsheet. | Never resolve a relationship on a display name. Load the parent table first, carry the parent source id on the child file, and map the child lookup against the alternate key so an ambiguous name cannot silently pick a winner. |
| Excel rewrites the data before it ever reaches Dataverse | Leading zeros are gone from reference numbers and postcodes, long identifiers show as scientific notation, and phone numbers have lost their formatting or their country prefix. | Never open the raw CSV by double clicking it. Import it into Power Query, set those columns to text explicitly, and generate the file for import from the query rather than from a saved worksheet. |
| Dates land a day out, or refuse to import at all | Rows fail with a message about the value not being valid for the field, or the dates load and are consistently one day off for part of the file. | Standardise every date to one unambiguous format in Power Query, check the personal options of the account running the import, since the wizard reads dates through those format settings, and verify a known record in the twenty row test before loading the rest. |
| Long text is cut off at the end | Notes and description fields look fine on most rows and are truncated mid sentence on the long ones, or those rows fail with a length error. | Measure the real maximum length of each text column in Power Query rather than trusting the declared width, and where the data needs it, provision a multiline text column in Dataverse before the load rather than after. |
| Everything ends up owned by whoever ran the import | Every migrated record is assigned to the one person who did the migration, and nobody else sees anything in their own views. | Include an owner column in the file, mapped to real enabled users, with a deliberate decision for rows whose original owner has left. If you cannot map owners at all, assign to a team rather than a person so the work is visibly unassigned instead of hidden. |
| Every record is dated the day you loaded it | A customer relationship that started six years ago shows a created date of last Tuesday, and any report based on record age is meaningless. | The wizard cannot set overriddencreatedon, so this is a real limit of the free path rather than a mistake. Keep the original date in a column of your own, so the history is at least on the record, and decide consciously whether the true created date matters enough to load those tables another way. |
The two rows worth reading twice are the duplicate detection pair, because they pull in opposite directions. Leave the rules on and a legitimate historical record gets rejected for looking like one you already loaded. Turn them off and a repeated import silently doubles the table. The way out of that trade off is not the rules at all, it is the source id column and the alternate key from step five, which make a re-run an update instead of a second copy.
When an import fails: the recovery plan
Assume at least one import will go wrong, because on a first migration one always does. A failed import is only expensive if it cannot be undone, and with free tooling it can be, provided the rows carry an identifier of yours and you loaded one file at a time. This is the order to work in.
Stop, and do not re-run the file
The instinct after a partial failure is to run the import again and hope it fills the gaps. Without an alternate key that produces a second copy of every row that succeeded the first time, which is a much harder problem than the one you started with. Nothing is lost by pausing: the rows that landed are still there and the source file has not changed.
Read the job before touching the data
Open the Imports view in the model-driven app, find the job, and look at the successes, failures, and partial failures per file. Download the failure log, which arrives as a file you can open in Excel with the failing rows and the reason for each one. Almost always there are two or three distinct causes rather than hundreds, and the fix is to the query rather than to the records.
Delete what that one job created
From the same Imports view you can delete the records that a specific import created for a table, which is the cleanest possible undo and the reason step eight said one file at a time. Do this before anyone starts working in the new records, because the option gets much less attractive once real edits are sitting on top of migrated rows.
When the job level delete is not usable, delete on the source id
If the import cannot be rolled back that way, use SQL 4 CDS in XrmToolBox and delete on the source id column, for example every row whose source id starts with the prefix from the batch that failed, after running the same statement as a select first and checking the count looks like what you expect. Never delete on created today: that also catches records real users created while you were working.
If the rows are right and one column is wrong, correct rather than reload
A reload is the expensive answer to a problem that is usually one field. Build a FetchXML filter for the affected rows and use Bulk Data Updater to set the correct value across them, or run an update statement in SQL 4 CDS. This is also how you fix a choice column that came in as the wrong option or an owner column that fell back to the migration account.
Fix the cause in the query and re-run the same data map
Go back to Power Query, correct the transformation that produced the bad values, and regenerate the file. Re-run the import with the saved data map so nothing about the mapping changes between attempts. With the alternate key in place the re-run updates the rows that are already there and creates the ones that are not, which is exactly the idempotent behaviour a funded migration pays a licensed tool for.
Write down what happened before the next attempt
One line per attempt: which file, how many rows, what failed, what was changed. On a solo migration that runs over several evenings this log is the entire project record, and it is what makes the difference between the third attempt being better than the first and it being the first attempt repeated.
Where the free path stops
Being honest about the ceiling is more useful than pretending there is not one. The free path is the right answer for tens of thousands of rows across a handful of tables, where the file is the whole source, the shape is simple, and one person can run it over a few evenings. It stops at three specific points. Volume, because the wizard caps upload size and processes rows through the standard pipeline, so a load in the hundreds of thousands or millions becomes a throughput problem with its own arithmetic, which is the subject of fast Dataverse bulk import for millions of rows. Created dates, because the wizard cannot supply overriddencreatedon, so if the true age of a record matters those tables need a different loading route. And files, because attachments and documents are their own workstream at any budget and there is no free shortcut through them.
The other thing that ends a zero budget migration is not the data at all. It is when the work stops being a load and starts being a build: the process needs automation that holds whatever route a record arrives by, the model needs relationships nobody can express in a spreadsheet, or an integration has to keep two systems agreeing. At that point the useful comparison is a few paid days against several unpaid weeks, and what that looks like is described under hire a Dynamics 365 developer or CRM developer in Armenia. If your migration fits in this section, run it yourself. That is a genuine answer, not a preamble to a sales pitch, and the reconciliation in step eleven is how you prove to yourself that it worked.
How do you migrate from Bitrix24 and other entry level CRMs?
Salesforce is not the only system people leave. A large share of the migrations we are asked about start on an entry level CRM, and in Armenia and the wider region that system is usually Bitrix24, bought by the sales team as a first CRM and grown into for several years. Everything above this point still applies without modification: the alternate key, overriddencreatedon, the dependency order, the state pass, the reconciliation. What changes is the shape of the source, and there are four differences that decide the plan. One question comes before all of them for a small team: whether Dynamics 365 is the right target at all. Where per user Microsoft licensing is the sticking point, a custom-built CRM is a legitimate destination too, and the same extraction work carries over.
Identity is the first. Bitrix24 record ids are integers that are only unique inside one entity type, so deal 1043 and contact 1043 both exist and there is no globally unique value to carry across the way the Salesforce 18 character Id is carried. The key you put in Dataverse has to be composite, such as DEAL-1043, built at extract time and defined as the alternate key everything resolves through.
Extraction is the second, and it is the first thing to establish on any engagement. A cloud portal comes out through the REST API, which means batched calls, per second rate limits, and paging on every list endpoint, so the extract is a program with retry and restart rather than a download. A self hosted portal is a PHP and MySQL application, so the CRM tables, the matching user field value tables, and the enumeration dictionaries can be read directly from the database, which is faster and hands you the true maximum column lengths that the profiling step needs anyway.
The third is that the dictionaries live outside the records. User fields are internal UF_CRM_ names whose human labels sit in the field metadata, list values come back as option ids, and deal stage labels come from the status dictionary rather than from the deal. Extract all of that before the data or you migrate integers. The fourth is that the automation layer has no export at all, which is covered in its own row below because it is the line most often missing from an estimate.
| In Bitrix24 | In Dataverse | What decides whether it works |
|---|---|---|
| Lead, Deal, Contact, Company | Lead, Opportunity, Contact, Account | A Bitrix24 portal runs in one of two CRM modes, with leads or without them. On a portal that never used leads, the whole first contact history sits on deals, and a lead table in Dataverse would be empty and misleading. Decide which entities should exist in the target before any mapping is written. |
| Record ids | A source key column with an alternate key on it | Bitrix24 ids are integers that are only unique inside one entity type, so deal 1043 and contact 1043 both exist. Unlike the Salesforce 18 character Id there is nothing globally unique to carry, so build a composite key at extract time, such as DEAL-1043, and put the alternate key on that. Load the raw integer as well and you will resolve a lookup to the wrong record eventually. |
| Deal categories and stages | Business process flow, statuscode, and a pipeline column | Stage ids are scoped to a category, so the same stage label means different things in two pipelines, and the labels themselves live in the status dictionary rather than on the deal. Resolve them at extract, then rebuild the pipeline as a designed process instead of copying twenty stages that accumulated over five years. |
| User fields (UF_CRM_ columns) | Dataverse columns with real names | The field name is an internal id, the human label lives in the field metadata, and list fields come back as option ids rather than text. Extract the field and enumeration dictionaries before the data, or you will migrate integers nobody can read. This is also the moment to drop the fields nothing has been written to in years. |
| Phone, email, and website multifields | telephone1 to telephone3, emailaddress1 to emailaddress3, websiteurl | Bitrix24 holds an unbounded list per record, each value tagged with a type such as work or mobile. Dataverse has three fixed slots. Agree the precedence rule in the mapping and put the overflow in a child table rather than silently dropping it. Records here commonly carry several mobile numbers per contact. |
| Requisites and bank details | Custom columns, or a related table where there can be more than one | Legal name, tax number, legal address, and bank details sit in a separate requisite object rather than on the company record, and they are precisely what invoicing and accounting use. A migration that only reads the company table loses the details the finance team cares about most. |
| Products, quotes, and invoices | Opportunity, quote, and order product lines | Dataverse will not accept a product line without a unit group, a product, a price list, and a currency already in place, so the catalogue is a prerequisite load rather than part of the same pass. Bitrix24 invoices moved to a smart process based model, so check which generation the portal is on before mapping them. |
| Activities, timeline, and call history | Phone call, email, appointment, and task activities, plus notes | Timeline comments are a different object from activities, and call recordings are drive files referenced by the activity rather than data on it. This is usually the largest volume in a Bitrix24 migration by a wide margin, so it gets its own extract, its own throughput budget, and its own reconciliation. |
| Tasks | Task activities, or a custom table where tasks are a real process | Tasks are a separate module that links to CRM records through a user field, so the link has to be extracted from the task side rather than from the deal. Decide early whether tasks are CRM work or project work, because migrating a project management module into a CRM is how a clean data model gets ruined in week one. |
| Drive files and attachments | Notes, file columns, or a SharePoint document location | File bytes are not in the CRM extract. Each file needs its own call against the drive API, or, on a self hosted portal, a read of the file storage path. Same separate workstream, same encoding overhead, same count and byte level reconciliation as the Salesforce file graph described above. |
| Robots, triggers, and business processes | Power Automate flows, business rules, plug-ins, business process flows | There is no export that imports anywhere else. Document each rule while the portal is still live, decide which ones still earn their place, and rebuild the survivors. This is build work priced as build work, not a line in the data mapping, and it is the part of a Bitrix24 migration that is most often missed at estimate time. |
| Open channels, chat, and messenger threads | A read only archive, or nothing | Conversations that arrived through Telegram, WhatsApp, or Viber have no equivalent object graph in Dataverse. Either flatten each thread into an archive record attached to the customer, or leave them in the source and keep the source readable. Say which in the loss register rather than discovering it after cutover. |
| Users, departments, and permissions | Entra ID users, business units, teams, and security roles | Bitrix24 identities are portal accounts, matched to your organization by email rather than by Entra ID, and the department tree is not a business unit hierarchy. Map users by email, decide the fallback owner for leavers before the load, and design the security model rather than translating the old permission list row by row. |
The row people underestimate is the automation one. Robots, triggers, and business processes are the reason the portal feels like it fits the business, and none of them export. They are documented while the source is still live, filtered down to the ones that still earn their place, and rebuilt as Power Automate flows, business rules, and plug-ins. That is build effort with names and days against it, and pricing a Bitrix24 migration as data movement alone is how these projects end up needing a rescue and takeover six months later.
The cutover, sized for a small team
The thirteen steps above assume a programme with a rehearsal window and people to run it. A Bitrix24 portal is usually run by the sales team with no dedicated CRM administrator, so the plan has to be short, reversible, and finishable over a weekend. This is the sequence we use for that case, and it is written for an Armenian business because that is where most of these land for us.
Decide how you are getting the data out
Cloud portals come out through the REST API, which means batched calls, per second rate limits, and paging on every list endpoint, so the extract is a program rather than a download. A self hosted portal is a PHP and MySQL application, so the CRM tables, the user field value tables, and the enumeration dictionaries can be read straight from the database, which is faster and gives you the true column lengths for profiling. Establish which one you have on day one, because it changes the tooling, the timeline, and who needs to be in the room.
Profile the portal before anyone talks about scope
Count records per entity, count activities, count drive files and total bytes, and list every user field with how many records actually have a value in it. Then list the deal categories and stages in real use, the automation rules that fire, and the integrations pointing at the portal, most often telephony, a website form, and messenger channels. Small portals hide a surprising amount: fields created for a campaign that ended, a second pipeline nobody works in, and one integration that quietly writes records nightly.
Write the loss register, including the automation
Same document as for any migration, with two Bitrix24 specific rows that always appear. Chat and messenger history, which has no target object and becomes an archive or stays behind. And the robots, triggers, and business processes, which are rebuilt rather than migrated. Put the rebuild in the plan as build effort with names against it, because this is the line that turns a cheap looking migration into a real project once somebody notices it.
Set up the environment before you shape the data
Two decisions here cannot be changed later. The Dataverse region is fixed when the environment is created, and there is no Microsoft datacentre in Armenia, so organizations here normally run in a European region, which also keeps a GDPR compliant data processing agreement straightforward for anyone trading into the EU. The base currency is also fixed at creation. Add every transaction currency the portal uses, Armenian dram alongside dollars, euros, and anything else in the data, and agree the exchange rate treatment before a single amount is loaded.
Clean and deduplicate at extract, not after the load
Regional data arrives in three languages and two alphabets, so the same company is often recorded twice, once in Armenian script and once in a Latin transliteration, with phone numbers written five different ways around the same +374 prefix. Normalise phone numbers to a single format, resolve the duplicates while the source is still there to check against, and configure Dataverse duplicate detection rules before go live rather than after the sales team has created the next hundred records.
Load in dependency order with dates converted properly
Users and teams, then currencies and the product catalogue, then companies and contacts, then deals, then activities, then files. Set overriddencreatedon and ownerid on create exactly as described above, and convert every timestamp to UTC at extract. Armenia is GMT+4 all year with no daylight saving, so the offset is a single fixed number rather than a seasonal one, but what the portal returns still depends on its own timezone setting and on the user the API call runs as, so pin that once and prove it on the first batch.
Rebuild the process, then train on real data
Stand up the pipeline as a business process flow, rebuild the automation that survived the loss register, and connect the telephony and web form integrations to the new system rather than the old one. Then let the team work a week in a sandbox loaded with their own migrated records, not with sample data. On a small team, adoption is the risk that actually sinks the project, and the fix is people recognising their own deals on the first screen they see.
Cut over in one weekend, with a delta and a read only source
Freeze the portal on Friday, run the delta for everything modified since the bulk extract, reconcile, switch the telephony and messenger channels last because they are the ones customers touch, and open the new system on Monday. Keep Bitrix24 readable, and paid for, for an agreed period afterwards. A small team cannot sustain a long parallel run, so the answer is a short cutover with an escape hatch rather than months of double entry that quietly stops happening in week two.
The validation section above is not optional because the source was smaller. Row counts per entity, control totals per currency, the created date distribution, the ownership distribution, and file counts with total bytes all still apply, and on a Bitrix24 migration the created date check is the one that catches the most, because activity volume dwarfs the CRM tables and a failed overriddencreatedon there is invisible until somebody opens a customer timeline.
What does moving from Bitrix24 to Dynamics 365 actually buy?
Before the mechanics matter, the decision has to be right. Bitrix24 genuinely wins the first two rows of this table for a small team, and pretending otherwise would make the rest of the page less useful. The comparison below is about where each one runs out.
| Dimension | Bitrix24 | Dynamics 365 Customer Engagement |
|---|---|---|
| What you are buying | A bundled suite on one subscription: CRM, tasks, drive, chat, telephony, and a site builder for a block of users, so the cost per head falls as the company grows and one tool covers several jobs. | CRM depth licensed per user per month at Microsoft published list prices, on the Dataverse platform, inside the Microsoft 365 tenant most organizations already pay for. |
| Time to first value | An afternoon. A sales team can switch a portal on, invite people, and have a pipeline running without involving anyone technical. This is a real advantage and it is why so many portals exist. | Weeks, because it is implemented rather than switched on. A single module rollout for a small business typically runs a couple of months from kickoff to go live. |
| Data model | A fixed entity set with user fields added on top, plus smart processes for custom entities. Fine until the business needs a relationship the model does not have. | Arbitrary tables, real relationships, alternate keys, calculated and rollup columns, and row level security, all in one Dataverse model shared with every other Power Platform app you build. |
| Automation | Robots and triggers per stage, plus business processes, all running inside the portal. Anything beyond that goes through the REST API and webhooks into code you host somewhere else. | Power Automate for process automation across the connectors your other systems already expose, business rules for form logic, and plug-ins or custom API running server side in the Dataverse pipeline so a rule holds whether the record arrives from a form, an integration, or a bulk import. |
| Change control over years | Configuration is changed in the live portal. There is no development environment, no staged release path, and no version history of the configuration to roll back to. | Managed solutions promoted through development, test, and production, configuration and code in source control, and two Microsoft release waves a year that can be tested in a sandbox before they land. This is where the return actually shows up, in years two and three rather than in month one. |
| Security and identity | Role and department based permissions inside the portal, with its own user accounts. | Business units, teams, hierarchy security, and column level security in Dataverse, with sign in through Entra ID, so multi factor authentication and conditional access are the same ones that already govern email and files. |
| Reporting | Built in CRM reports and dashboards, adequate for pipeline and activity questions and awkward beyond them. | Power BI reading Dataverse directly, and a supported link into Fabric for analytics, so the CRM numbers sit in the same reporting stack as the rest of the business rather than in a second one. |
| Service and field operations | Ticketing exists, but there is no entitlement and SLA driven case management and no field scheduling engine. | Customer Service with entitlements, SLAs, and omnichannel, and Field Service with a scheduling board and a mobile app for crews. If either of those is your actual business, this is the whole argument on its own. |
| Fit with the rest of the estate | Marketplace applications and the REST API. Integration is possible and it is work you own. | Outlook, Teams, Excel, and SharePoint integration in the box, and connectors for the accounting system you already run. We integrate with your finance system rather than trying to replace it. |
The honest return on investment
Start from the uncomfortable part. Moving from Bitrix24 to Dynamics 365 usually raises the licence line, because Bitrix24 prices a bundle for a block of users while Dynamics 365 is priced per user per month, so the return cannot come from the subscription. It has to come from somewhere else, and there are only six places it ever does.
- Work that drifted out of the CRM comes back into it. When the real pipeline lives in a spreadsheet and the real customer conversation lives in a messenger thread, the CRM is already being paid for twice.
- Rules start holding everywhere. A pricing, discount, or approval rule written as a plug-in or custom API runs whether the record arrives from a form, an integration, or a bulk import, which portal side automation cannot promise.
- Reporting stops being argued about. Power BI reads Dataverse directly, so the CRM numbers sit in the same stack as the rest of the business rather than in a second one that finance does not trust.
- A second product stops being needed. Entitlement and SLA driven case management, or a scheduling board and a mobile app for field crews, are things Bitrix24 does not have, so today they are either bought separately or done manually.
- Customization stops being rebuilt. Managed solutions moving through development, test, and production, with configuration in source control, means what you build in year one is still there in year three rather than having been redone live twice.
- Due diligence stops being a problem. Sign in through Entra ID with the same multi factor and conditional access policies as email, and a Dataverse region you chose deliberately, are answers that hold up when an EU or US customer audits your suppliers.
If none of those six describe your situation, the right advice is to stay on Bitrix24, and we will give it. A team of five that needs a shared pipeline and a task list is already being served, and a migration would buy them a larger bill and a training programme. The point at which the argument flips is not a headcount. It is the first time a requirement gets answered with a workaround that somebody has to remember to do.
What is different about doing this in Armenia
Most of a migration plan is the same anywhere. These are the parts that are not, and each one has cost somebody a week when it was noticed late rather than early.
The portal was bought by the sales team, not by IT
That is the usual history in the region, and it has consequences for the migration rather than for the sales pitch. Nobody owns the administrator account full time, the field list grew without review, and the integrations were set up by whoever needed them. Budget the profiling step properly, because on these portals the discovery genuinely is the estimate.
Three languages and two alphabets in the same table
Records here are written in Armenian, Russian, and English, often in the same column, and the same company frequently exists twice under an Armenian and a transliterated Latin name. Deduplicate while the source is still available to check against, normalise the +374 phone formats to one shape, and set up Dataverse duplicate detection before go live rather than after.
Currency is a one way decision
The Dataverse base currency is fixed when the environment is created and cannot be changed afterwards. Add Armenian dram as a transaction currency along with every other currency in the portal, decide the exchange rate treatment, and reconcile control totals per currency, because a money column loaded without a currency is quietly interpreted as base currency.
Where the data sits, and who asks about it
There is no Microsoft datacentre in Armenia, so the environment normally runs in a European region, which is chosen at creation and is a support led migration to change later. For companies selling into the EU or the US this is worth settling deliberately, because vendor and data residency questions now turn up in customer due diligence long before anyone asks about your CRM features.
A fixed +4 offset, but check what the portal returns
Armenia keeps GMT+4 all year with no daylight saving, so converting timestamps to UTC is a single constant rather than a seasonal calculation, which removes an entire class of migration bug that European sources suffer from. What still needs pinning is the portal timezone setting and the user the extract runs as, because both change the value the API hands back.
The accounting system stays where it is
Local finance teams run their own accounting software and it is not part of this conversation. Our scope is Dynamics 365 Customer Engagement and the Power Platform, so we integrate the CRM with the finance system you already have rather than proposing to replace it, and the integration is scoped as its own piece of work with its own testing.
Solzet is registered in Yerevan and the consultants live and work there, so for an organization in Armenia this is a domestic supplier rather than an import: the same working day, workshops in the room, and English, Armenian, or Russian. What that looks like as an engagement is set out on our CRM consulting in Armenia page, and the people who would do the build are described under hire a Dynamics 365 developer or CRM developer in Armenia. If the migration is the second attempt rather than the first, the Dynamics 365 project rescue and takeover service is the shape that work takes, and our write up on the migration practices that actually hold up covers why rebuilding the process on Dataverse beats copying the old one across, whichever CRM you are leaving.
How does Solzet run these migrations?
Solzet is a Microsoft Dynamics 365 Customer Engagement and Power Platform consultancy in Yerevan, Armenia. Cross-platform data migration sits inside our implementation and project rescue work, so we see these from both ends: the ones we plan from the start, and the ones we are called into after the load has already happened. The second kind is almost always the same two problems. Created dates were not preserved, so every record looks like it was born on load day and the only fix is a delete and reload. Or the relationships were resolved on a display value instead of the source Id, so the parents are wrong in a way that no amount of spot checking finds.
The same two problems show up on a Bitrix24 migration in a slightly different costume: created dates lost on the activity history, which is the largest table and the last one anybody checks, and lookups resolved on a raw integer id that means something different in another entity. Our own sequencing is the one above: profile first, write the loss register and get it signed, and treat the identity and timestamp decisions as one way doors rather than configuration. We build loads that are idempotent and logged per row, because the value of a re-runnable load only becomes obvious the first time a batch fails at three in the morning. And we reconcile per batch rather than at the end, so a broken assumption costs an hour instead of a weekend.
On the Microsoft side we work in Dynamics 365 Customer Engagement and the Power Platform: Sales, Customer Service, Field Service, model driven and canvas apps, Power Automate cloud and desktop flows, Power Pages, Dataverse, plug-ins and custom APIs, and PowerApps Component Framework controls. For organizations where Microsoft licensing is not the right fit, we also build custom CRM on React, Node.js, PostgreSQL, and .NET, and the migration discipline on this page applies to that target unchanged. We do not take on finance, accounting, or supply chain systems, so if that is where your migration is headed we will say so at the first conversation rather than after a scoping exercise. Where you already run one, we integrate the CRM with it rather than proposing to replace it.
The companion piece to this guide is our Salesforce to Dataverse migration best practices page, which covers the same move from the design and pitfalls angle rather than the execution one. The wider delivery is described under Dynamics 365 consulting. Where the migration is one symptom of an implementation that has already gone wrong, the rescue and takeover guide describes how we pick that up, and the health check and technical audit is how we assess an environment before committing to a plan.
What do teams ask about Salesforce to Dynamics 365 data migration?
How do people migrate from Salesforce to Dynamics 365 without losing anything?
They decide what "anything" means before they start. A migration plan that survives contact with reality names, table by table, what is migrated, what is archived somewhere else, and what is deliberately dropped, and it acknowledges the handful of things Dataverse genuinely will not carry, such as last modified metadata and field level history. After that it is mechanics: keep the Salesforce 18 character Id as an alternate key so relationships and re-runs are reliable, set overriddencreatedon on create so records keep their real age, turn off every plugin and flow during the load, treat files as their own workstream, and reconcile with counts, control totals, and referential integrity before anybody signs anything.
What is overriddencreatedon and why does it matter?
It is the Dataverse column that lets a data migration state the original creation date of a record. Supply it as a UTC value in the create request and Dataverse writes it through to createdon, keeping overriddencreatedon as the record of the override. It matters because createdon is otherwise stamped at insert, so without it every migrated record looks like it was created on load day, which breaks age based reporting, service history, and anything a salesperson uses to judge how long a relationship has existed. Two constraints decide your plan: the migration account needs the privilege that allows overriding created on and created by during import, and the value cannot be set on an update, so a table loaded without it has to be deleted and reloaded rather than corrected.
Should we use KingswaySoft or Azure Data Factory for the migration?
Both work, and on a large migration the honest answer is usually a combination. KingswaySoft inside SSIS is the default for Dynamics 365 Customer Engagement because its Dataverse components handle the platform specific parts natively: upsert on alternate keys, lookup resolution, impersonation, notes and attachments, and per row error output. Azure Data Factory fits when the shape is already cloud native, with large extracts staged into storage or SQL and transformed at scale, and it doubles as the platform for integration after go live. Dataflows are for reference data and corrective loads. Custom code against the Web API usually still ends up owning files, impersonated creates, and the state and closure pass, whichever main tool you choose.
Can Salesforce audit history and field history be migrated into Dynamics 365?
No. The Dataverse audit log is system managed and cannot be back-filled with historical rows, whatever a tool appears to offer. The workable approach has two parts: preserve the original created date on the live records with overriddencreatedon so they keep their true age, and load the Salesforce history objects into a read only Dataverse table or an external store as an archive. Keep auditing switched off during the migration itself, because otherwise the load fills your log capacity recording its own writes, and turn it on after cutover so the audit trail starts clean on the first real day.
How are Salesforce attachments and files moved into Dataverse?
As a separate workstream with its own plan, because they are a separate object graph in Salesforce and the slowest part of the move. Extract the classic Attachment and Note objects along with ContentVersion and the ContentDocumentLink rows that say what each file belongs to, then load into whichever destination the mapping chose: Dataverse notes, file columns, or a SharePoint document location, associated to the parent record through the source Id. Budget for encoding overhead, file storage capacity, and API throughput, run it in logged batches so it can restart, and reconcile file counts and total bytes per parent table rather than assuming success.
Why did our migrated records all end up owned by one account?
Because the user mapping was incomplete and the load fell back to whoever ran it. Salesforce owners include people who have left, and an inactive or missing user in Dataverse gives the loader nothing valid to assign to. The fix is to build the mapping as a real deliverable before data moves, with a row per Salesforce user and an explicit decision for every leaver, usually a named team rather than one service account so unassigned work is visible. Impersonated creates have the same requirement: the target user has to exist and be enabled at the moment the row is written.
How long does a Salesforce to Dynamics 365 migration take?
It depends on volume, file size, the number of objects in scope, and how much of the source is worth keeping, so any number quoted without seeing the org profile is guesswork. What is predictable is where the time goes: the mapping and the loss register take longer than people expect, the bulk relational load is usually the fastest part, files are usually the slowest, and the rehearsal is what actually tells you how long cutover weekend will be. That is the point of running the full sequence end to end against full volume in a sandbox: the rehearsal converts an estimate into a measured duration you can plan the business around.
How do you migrate from Bitrix24 to Dynamics 365?
The same way as any CRM migration, with four differences that come from how Bitrix24 stores things. First, identity: Bitrix24 ids are integers that repeat across entity types, so the alternate key has to be a composite such as DEAL-1043 rather than the raw number. Second, dictionaries: user fields are internal UF_CRM_ names and list values come back as option ids, so the field and enumeration metadata has to be extracted before the data or you migrate integers. Third, extraction shape: a cloud portal comes out through the rate limited REST API while a self hosted portal can be read from its own database, and that decision changes the tooling and the timeline. Fourth, automation: robots, triggers, and business processes have no export, so they are documented and rebuilt as Power Automate flows, business rules, and plug-ins, which is build work rather than data work. Everything else, meaning overriddencreatedon on create, dependency ordering, the state pass, files as their own workstream, and reconciliation by counts and control totals, is identical to the procedure on this page.
Is Dynamics 365 worth it compared to Bitrix24?
Only for specific reasons, and if none of them apply then staying on Bitrix24 is the right answer and we will say so. Moving usually raises the licence line, because Bitrix24 prices a bundle for a block of users while Dynamics 365 is per user per month, so the return has to come from somewhere else. The reasons that hold up are these: work that has drifted out of the CRM into spreadsheets and messenger threads coming back inside it; automation that has to hold whether a record arrives from a form, an integration, or an import, which needs server side logic rather than portal robots; reporting finance actually trusts, through Power BI reading Dataverse directly; entitlement and SLA driven service or field scheduling, which Bitrix24 does not have; a change control path with development, test, and production environments so customization survives years rather than being redone in the live system; and identity and data residency answers that stand up to customer due diligence. A five person team that needs a pipeline and shared tasks does not need any of that.
Can Bitrix24 deal history, call recordings, and chat be migrated to Dynamics 365?
Partly, and the split is worth agreeing up front. Deal, contact, and company history migrates, including activities and timeline entries, with original created dates preserved through overriddencreatedon so the records keep their real age. Call recordings migrate as files, because in Bitrix24 they are drive objects referenced by the activity rather than data on it, so they need their own extract and their own byte level reconciliation and they are usually the largest volume in the whole move. Chat and messenger threads from open channels have no equivalent object graph in Dataverse, so the realistic options are to flatten each thread into a read only archive record attached to the customer or to leave them in the source and keep the source readable for an agreed period. Whichever you choose belongs in the loss register in writing before the load starts.
What about migrating from amoCRM, Zoho, HubSpot, or spreadsheets?
The shape is the same as Bitrix24 and the section above applies with the entity names changed. Every entry level CRM has the same four properties: a fixed entity set with custom fields added on top, per entity integer or short string ids that need a composite alternate key, dictionaries that live outside the records, and an automation layer that has to be rebuilt rather than exported. Spreadsheets are the easiest source technically and the hardest to reconcile, because there is no authoritative record count to check against and duplicates are the norm rather than the exception, so the profiling and deduplication work moves to the front. In every case the decisions that matter are the same ones on this page: what is migrated, what is archived, what is deliberately dropped, and how you prove afterwards that the answer was honoured. If the source is spreadsheets and there is no budget for licensed tools or a developer, the zero budget section on this page is the whole sequence for that case, using Power Query, the Data Import Wizard, and XrmToolBox.
How do I migrate from Excel or Access to Dynamics 365 with no budget for tools or a developer?
With three free tools and a strict order of work. Power Query, already in Excel, does the cleaning: explicit data types, identifiers forced to text so leading zeros survive, one standard date format, deduplication on a normalised key, and the split of one wide sheet into the separate account, contact, and activity files it should always have been. Before loading, add a source id column of your own such as ACC-0001, create a matching text column in Dataverse, and set it as an alternate key, because that single column is what makes a re-run an update rather than a duplicate and what makes a failed import removable. Then load with the Data Import Wizard inside the app: one file at a time, parents before children, every column mapped by hand rather than automatically, choice values mapped explicitly, the mapping saved as a data map, and a twenty row test before the full file. XrmToolBox, the free community application, covers the rest: reading the true column types and lengths before you map, writing the reconciliation queries, and correcting or deleting rows afterwards. The zero budget section on this page sets out the full sequence, the pitfalls, and the recovery plan.
Can the Data Import Wizard preserve the original created dates from my spreadsheet?
No. Setting the real creation date requires overriddencreatedon in the create request, and the Data Import Wizard gives you no way to supply it, so records loaded through it carry the day you loaded them as their created date. That is a genuine limit of the free path rather than something you configured wrongly. There are three honest options. Keep the original date in a column of your own so the information is at least on the record and reportable, which is usually enough for a small business. Load the tables where record age genuinely matters through a route that can carry the column, which needs an account holding the privilege to override created on and created by during import. Or accept the loss deliberately and write it down before the load, which is what a funded project does in its loss register. What you cannot do is fix it afterwards, because the value only works on create: a table loaded without it has to be deleted and reloaded.
What do I do when a Dynamics 365 data import fails halfway through?
Stop, and do not re-run the file, because without an alternate key a second run duplicates every row that already succeeded. Open the Imports view in the app, read the successes and failures for that job, and download the failure log, which opens in Excel and gives the reason per row. There are usually two or three distinct causes rather than hundreds. From the same view you can delete the records that specific import created for the table, which is the cleanest undo and the reason for loading one file at a time. If that is not available, use SQL 4 CDS in XrmToolBox to delete on your own source id column, running the statement as a select first to check the count, and never deleting on records created today because that also catches work real users did. If the rows are correct and only one column is wrong, do not reload at all: filter with FetchXML and correct the column with Bulk Data Updater. Then fix the cause in the Power Query step rather than by hand in the CSV, regenerate the file, and re-run with the same saved data map.
Can you migrate a company in Armenia from Bitrix24 to Dynamics 365?
Yes, and it is a common request for us because Solzet is based in Yerevan and Bitrix24 is a common first CRM in the region. In practice a local project is shaped by a few specifics. The team is small and has no dedicated CRM administrator, so the plan is a short weekend cutover with a delta load rather than a long parallel run. The data is in Armenian, Russian, and English, so deduplication and phone normalisation happen at extract while the source is still there to check against. Armenian dram is added as a transaction currency and the base currency is settled before the environment is created, because it cannot be changed afterwards. There is no Microsoft datacentre in Armenia, so the environment normally runs in a European region, chosen deliberately at creation. And the accounting system stays where it is: we integrate with it rather than replacing it. Workshops and training can happen in the room in Yerevan, in English, Armenian, or Russian. It is also worth asking first whether Dynamics 365 is the right target: for a small team that wants to leave Bitrix24 without taking on per user Microsoft licensing, Solzet also builds custom CRM on React, Node.js, PostgreSQL, and .NET, and the same migration discipline applies.
Can you take over a Dynamics 365 data migration that a previous partner started?
Yes, and it is a common way these engagements begin. The first job is establishing what the earlier load actually did, because two failures account for most of what we find. Created dates stamped at load time, because overriddencreatedon was never used, which cannot be corrected by an update and means those tables are deleted and reloaded. And relationships resolved on a name or an email rather than a source id, so records sit under the wrong parents in a way spot checking does not surface. Before anything else moves we add the source id column and an alternate key so the load becomes re-runnable, and we flag the rows users created in the target during the gap so a delta load does not overwrite work the business has already done. The order matters: the environment is stabilized first, the existing data model is audited forensically, and only then does data move.
Can a migration be done in phases instead of one cutover weekend?
Yes, and during a partner transition or a project rescue it is usually the right answer, because the business is already live and the team has already been asked to wait once. Slice by business process rather than by table, so the first slice is something the business names for itself and carries every table that process needs. Each slice then runs the whole sequence on its own: bulk load, delta, and reconciliation against row counts, control totals per currency, referential integrity, created date distribution, ownership distribution, and a sample of records the business chose. The price of working this way is coexistence, so write down per table and per slice which system is authoritative while both are live, which direction any interim sync runs, and what happens to a record edited on the wrong side. A one way flow everybody understands beats a two way sync nobody can reason about when the numbers disagree.
What are the risks of migrating data after a partner breakup?
Two dominate. Undocumented customizations: a rule living in a plug-in nobody knew about, a value produced by a workflow rather than a formula, a requirement enforced in form script rather than on the column. None of it appears in a mapping document and all of it either blocks the load or silently changes what gets loaded, which is why the component inventory is taken from solution layers in the environment rather than from documentation, and why everything that reacts to a write is disabled for the duration of the load. Orphaned integrations: an interface still running on a schedule under the identity of somebody who left, writing into the tables you are mid-migration on, or pointed at a decommissioned endpoint so it fails quietly. Every interface is inventoried with the identity it authenticates as and its secret expiry, inbound ones are paused for the migration window, and they are re-pointed or retired one at a time afterwards. Behind both sits a third risk that is administrative rather than technical: tenant administration, environment ownership, and the app registrations the integrations run as often sit with the outgoing partner. They are recoverable, because the tenant belongs to the organization paying for it.
How do you migrate several old CRM systems into Dynamics 365 with relationships, attachments, and history intact?
Transform every source into one canonical staging layer first, so three formats become one shape, and give every staged row a legacy identifier prefixed with its source system. Deduplicate across systems in staging and record which legacy ids merged into which survivor. Then load in order: reference data and users, accounts and contacts upserted on the legacy key as an alternate key, the records parented to them, activities, and finally attachments from a staged file manifest. Reconcile each pass per source before the next begins, and archive closed history outside the agreed window rather than loading it.
How do you make a multi-source Dynamics 365 migration safe to re-run?
Carry a legacy identifier on every row, built as source plus entity plus original id so values from different systems can never collide, store it in a Dataverse column defined as an alternate key, and write every pass as an upsert on that key. Resolve every lookup through the parent key rather than a name or a GUID. Keep merge decisions as data in the staging layer so a rehearsal reproduces them exactly. A failed or repeated pass then updates what already exists instead of creating a second copy.
Should closed historical records from old CRMs be loaded into Dynamics 365?
Only the ones people still work with. Open work and closed records inside an agreed history window load live, created open and then closed in a state pass. Older closed records that are still asked about occasionally go to a read only archive linked by the legacy key, in a flattened Dataverse table or an external store. Closed history for customers that exist in no source is archived or dropped with sign-off, and legacy audit tables are always archived because the Dataverse audit log cannot be back-filled.
How do you validate that a data migration worked before turning off the old system?
Run a fixed validation set against a frozen copy of the source and the target, whatever the source system was: row counts per entity, financial and quantity totals per period and currency, referential integrity on every relationship, a field by field comparison of sampled high value records, and activity and attachment counts per parent. Script it so two people can run it independently and get the same numbers, agree before the run what variance is acceptable and what is a stop, and have each entity owner sign off. The old system is switched off only when the signed reconciliation report exists and a read only copy of the source has been retained.
What variance is acceptable in a migration reconciliation?
Only variance you can explain from documents written before the run. Record count differences must match the agreed exclusion and merge registers, money and quantity totals must agree per period and currency apart from any documented precision change, and there should be no child record that lost a parent it had in the source. A wrong amount, date, status or owner on a sampled high value record is a stop, not a variance. The tolerances are agreed in advance, because a tolerance chosen after seeing the result will always be wide enough to pass.
How long should the old system stay read only after cutover?
At least until the first full reporting or financial period has closed on the new system and the questions that surface in the first weeks have been answered from it, and in practice until the audit or regulatory cycle that relies on migrated data has passed. Before licences or hosting end, export a complete read only copy with the reconciliation report alongside it, and keep that for as long as your retention policy, auditors or regulator require. Agree the date and the owner of that decision in writing at cutover, not when the renewal invoice arrives.
Can Solzet run a Salesforce to Dynamics 365 migration for us?
Yes. Solzet is a Microsoft Dynamics 365 Customer Engagement and Power Platform consultancy based in Yerevan, Armenia, and cross-platform data migration is part of our implementation and project rescue work. We profile the source org, design the Dataverse target model and the field level mapping, build the identity, user, and currency strategies, run the loads in dependency order with created dates and ownership preserved, move notes and files, and reconcile before anybody signs off. We also pick up migrations that have already gone wrong, which usually means reloading the tables that lost their created dates and rebuilding the relationships that resolved on the wrong key. We work directly with end clients and white-label for Microsoft partners across Europe and the US.
Related services: Dynamics 365 consulting, Power Platform consulting, white-label Dynamics 365 subcontracting and custom CRM development.
Years of Salesforce data to move?
Send us the shape of your Salesforce org or your Bitrix24 portal, the objects or entities in scope, the row counts, and the file volume, and we will come back with the target model, the loss register, and a load plan that keeps the created dates, the relationships, and the ownership intact. If the honest answer is that you should stay where you are, that is what you will get instead. We recommend the right solution - whether that's Microsoft Dynamics 365, Power Platform, or a custom-built CRM. Some businesses need the Microsoft ecosystem. Others need full control without licensing. We deliver both.