Dynamics 365 Sales to Salesforce FSC Migration: Technical Guide
Technical guide to migrating Dynamics 365 Sales to Salesforce Financial Services Cloud. Covers data model mapping, Person Accounts, API constraints, and FSC edge cases.
Planning a migration?
Get a free 30-min call with our engineers. We'll review your setup and map out a custom migration plan — no obligation.
Schedule a free call- 1,500+ migrations completed
- Zero downtime guaranteed
- Transparent, fixed pricing
- Project success responsibility
- Post-migration support included
Dynamics 365 Sales to Salesforce FSC Migration: Technical Guide
Migrating from Microsoft Dynamics 365 Sales to Salesforce Financial Services Cloud (FSC) is a schema-translation problem, not a data-copy job. Dynamics 365 Sales uses a flexible, entity-relationship model on Microsoft Dataverse. FSC layers financial-services-specific objects — Person Accounts, Households, FinancialAccount, AccountContactRelation — on top of the Salesforce platform. Getting this mapping wrong orphans client records, breaks household hierarchies, and destroys the relationship context that financial advisors depend on.
This guide covers the end-to-end migration: data model mapping, API constraints on both sides, migration approaches with honest trade-offs, and the edge cases that derail projects mid-flight.
Core Architecture Differences: Dynamics 365 Sales vs Salesforce FSC
Before touching a single record, understand what you're translating between.
Microsoft Dynamics 365 Sales runs on Microsoft Dataverse and uses the standard extensibility model for model-driven apps. You interact with tables (formerly entities) and actions via the Dataverse Web API, which implements OData v4. (learn.microsoft.com) The core sales entities — accounts, contacts, leads, opportunities, phonecalls, emails, tasks, appointments — follow a traditional CRM relational model where Accounts and Contacts can be linked via polymorphic lookups and custom join tables.
Salesforce Financial Services Cloud is an industry-specific layer built on top of the Salesforce platform. It introduces objects purpose-built for financial institutions: FinancialAccount, FinancialAccountTransaction, FinancialHolding, FinancialGoal, AccountAccountRelation, and ContactContactRelation. FSC enforces a managed package schema designed for wealth management, insurance, and retail banking.
Key reasons companies migrate:
- Industry-specific data models: FSC natively supports Householding, Financial Goals, and Asset/Liability tracking without heavy custom engineering.
- Ecosystem integration: Deep integration with Marketing Cloud, Experience Cloud, and advisor portals.
- Regulatory compliance: FSC's built-in action plans and interaction summaries simplify SEC and FINRA compliance tracking.
- Stack simplification: Consolidating downstream automations, reporting, and advisor workflows onto a single platform.
The Person Account Decision
FSC represents individuals in one of two ways: Person Accounts or the Individual model. Person Accounts combine certain Account and Contact fields into a single object experience. Salesforce recommends Person Accounts for new FSC installations — the Individual model is supported only for existing implementations. (help.salesforce.com)
This has direct migration implications:
- Person Accounts are irreversible — once enabled on an org, they cannot be disabled
- Dynamics 365 Contacts must be transformed into Person Accounts, not standard Salesforce Contacts
- Person Accounts are linked to Household Accounts via the Account-Contact Relationship (ACR) junction object, not a direct lookup
- A Person Account can belong to multiple households but can only have one primary group
Person Account vs. Individual Model Decision Criteria
The blog-post-level advice of "Salesforce recommends Person Accounts" is insufficient for real-world decisions. Use these criteria:
| Factor | Choose Person Accounts | Choose Individual Model |
|---|---|---|
| New FSC org (greenfield) | ✅ Always — Salesforce's recommended path | Only if migrating from an existing Salesforce org that already uses standard Contacts |
| Existing Salesforce org with standard Contacts | Only if you can accept re-implementing all Contact-dependent automations, reports, and integrations | ✅ Avoids breaking existing Contact-based logic |
| B2C-dominant client base | ✅ Cleaner UX — individuals are first-class Account records | Workable but requires more navigation clicks |
| B2B and B2C mixed | ✅ Person Accounts for individuals, standard Accounts for businesses | Works, but relationship modeling is less intuitive |
| Third-party integrations expecting standard Contacts | Requires integration refactoring — Person Account Contacts behave differently in SOQL and triggers | ✅ Less integration disruption |
| Org already has Person Accounts enabled | ✅ No choice needed | Not available — Person Accounts can't be disabled |
Critical technical consideration: Person Accounts create both an Account record and a Contact record simultaneously. Any Apex triggers or Flows on the Contact object will fire during Person Account creation. If your FSC org has existing Contact triggers (including FSC managed package triggers), test thoroughly in sandbox before committing.
Decide the person model early. This belongs in sandbox design, not cutover week.
Household Model
Dynamics 365 Sales has no native household concept. Where Dynamics 365 treats B2B and B2C relatively similarly (using Accounts and Contacts), FSC relies on Households — a specific Account Record Type — to group individuals. (help.salesforce.com)
If your Dynamics instance uses custom entities or fields to group contacts into households, that structure must be mapped to FSC's Household Account record type and the ACR junction object. If households aren't modeled in Dynamics at all, you'll need to construct them during migration — a data enrichment step that requires explicit grouping rules.
Household Construction Logic
When Dynamics 365 has no explicit household data, you need heuristics to group contacts. Here's a decision tree that covers the most common patterns:
For each Contact in Dynamics 365:
│
├─ Does an explicit "Household" or "Family" custom entity/field exist?
│ └─ YES → Use that grouping directly. Map to Household Account.
│
├─ NO → Apply heuristic grouping:
│ │
│ ├─ Step 1: Match on shared mailing address (normalized) + shared last name
│ │ └─ Match found → Group into candidate Household
│ │
│ ├─ Step 2: Check for explicit relationship fields
│ │ (e.g., "Spouse", "Child Of" in Connections entity)
│ │ └─ Relationship found → Merge into same Household
│ │
│ ├─ Step 3: Check for shared Account associations
│ │ (Contacts linked to same Account with family-type roles)
│ │ └─ Association found → Merge into same Household
│ │
│ └─ Step 4: Remaining unmatched Contacts
│ └─ Create single-member Households (or flag for manual review)
│
└─ Business validation: Export candidate Households for advisor review
before loading into FSC
Important: Address normalization is critical — "123 Main St" and "123 Main Street, Apt 2" must resolve correctly. Use USPS standardization or a fuzzy matching library. False positives (grouping unrelated people) are worse than false negatives (creating too many single-member households), because merging households in FSC is simpler than splitting them.
Always export candidate household groupings for business stakeholder review before loading. Automated heuristics will produce errors that only advisors can catch.
Data Model & Object Mapping
This section determines whether your migration succeeds or silently corrupts your data. Do not map fields until you answer two questions: Is this person a Person Account or a Contact under a Business Account? and Is this relationship household membership, business affiliation, or historical context?
Core Object Mapping
| Dynamics 365 Sales Entity | Salesforce FSC Target | Mapping Notes |
|---|---|---|
account (Organization) |
Account (Business Account record type) | Map name, address1_*, telephone1, industrycode. Direct mapping for B2B accounts. |
contact |
Person Account | Not a standard Contact. Create Person Account records using the Person Account record type. Map firstname, lastname, emailaddress1, birthdate. |
lead |
Lead or Person Account + Opportunity | FSC supports Leads. For qualified leads at migration time, convert to Person Account + Opportunity. Depends on pipeline stage. |
opportunity |
Opportunity or Financial Goal | Map name, estimatedvalue → Amount, estimatedclosedate → CloseDate, statuscode → StageName. Long-term client objectives may map to Financial Goals depending on your FSC implementation. |
task |
Task | Direct mapping. Map subject, description, scheduledstart, scheduledend. Link via regardingobjectid → WhatId/WhoId. |
email |
Task (Email type) or EmailMessage | If Email-to-Case is enabled, use EmailMessage. Otherwise, map to Task with TaskSubtype = 'Email'. |
phonecall |
Task (Call type) | Map to Task with TaskSubtype = 'Call'. Map phonenumber, subject, description. |
appointment |
Event | Map subject, scheduledstart → StartDateTime, scheduledend → EndDateTime. |
annotation (Notes/Attachments) |
Note, ContentNote, or ContentVersion | annotation.notetext → Note.Body. For rich text, use ContentNote. Attachments in annotation.documentbody require separate handling via ContentVersion + ContentDocumentLink. |
| Custom entities (e.g., Investment Portfolios) | FinServ__FinancialAccount__c or custom object |
Map to FSC-native objects where possible rather than building custom objects from scratch. Record types control account type (Checking, Savings, Loan). |
| N/A (construct during migration) | Household Account + ACR | Build Household Accounts and Account-Contact Relationships to establish household membership. |
| Account-Contact relationships | AccountContactRelation |
FSC uses ACR as a junction object. Set Roles and IsPrimary fields. |
| Connections/Relationships | AccountAccountRelation / Reciprocal Roles |
Map inter-account relationships (subsidiary, parent company) and inter-person relationships. |
Field-Level Mapping Considerations
- Option Sets → Picklists: Dynamics 365 stores option sets as integer values with labels. Export both the value and label, then map to Salesforce picklist values by label. Create missing picklist values in FSC before import.
- Lookups → Lookup/Master-Detail: Dynamics 365 lookup fields reference entity GUIDs. You must maintain an ID crosswalk table (old GUID → new Salesforce ID) and remap all lookups post-migration.
- Currency fields: Dynamics 365 supports multi-currency with
transactioncurrencyid. FSC supports multi-currency but requires org-level enablement. Mapmoneyfields and their currency codes. - DateTime: Dynamics 365 stores all dates in UTC. Salesforce stores dates in UTC but displays in user timezone. No conversion needed for API imports, but verify display behavior. Ensure ISO 8601 format for all date fields.
- Owner fields: Map
owneridto SalesforceOwnerId. Pre-provision all users in FSC and build a user ID crosswalk. - State and Status Codes: Dynamics uses
statecode(Active/Inactive) andstatuscode(Reason). Salesforce uses picklists. Map these explicitly to avoid validation rule failures.
Migration Approaches
1. CSV Export/Import
How it works: Export Dynamics 365 data to CSV via Advanced Find, Excel Online, or the Data Management Framework. Transform in Excel or Python. Import into FSC via Data Import Wizard or Data Loader.
When to use it: Small datasets (under 10,000 records), simple schemas with few custom objects, proof-of-concept migrations. The Data Import Wizard is capped at 50,000 records. Person Account imports need first and last name fields, not a single business-account Name column.
Pros: No coding required. Familiar tooling. Good for initial data exploration.
Cons: Flattens relationships — you lose lookup references and must manually rebuild them. No automation. Doesn't handle attachments. Cannot handle multi-object chains (Account → Contact → Financial Account → Opportunity) without manual VLOOKUPs.
Complexity: Low | Scalability: Small datasets only
2. API-Based Migration
How it works: Extract data from Dynamics 365 using the Dataverse Web API (OData v4 endpoints). Transform in a custom script. Load into FSC using the Salesforce Bulk API 2.0 for large volumes or the Composite API for small dependent chains (up to 25 subrequests per call). (developer.salesforce.com)
Key technical constraints:
- Dynamics 365 Web API returns a maximum of 5,000 records per page. Use
@odata.nextLinkfor pagination —$skipis not supported. (learn.microsoft.com) - Salesforce Bulk API 2.0 processes up to 100 million records per 24-hour period, with internal batches of 10,000 records and a 150 MB per-job data limit.
- Authentication: Dynamics 365 uses OAuth 2.0 via Microsoft Entra ID. Salesforce uses OAuth 2.0 with Connected App credentials.
Pros: Full control over transformation logic. Handles relationships programmatically. Scriptable, repeatable, auditable.
Cons: Requires significant engineering effort. Must handle rate limits, pagination, error retries, and reconciliation on both sides.
Complexity: High | Scalability: Enterprise-grade
3. Third-Party Migration Tools
How it works: Tools like Jitterbit, Informatica, Talend, or MuleSoft provide pre-built connectors for both Dynamics 365 and Salesforce. You configure mapping rules in a GUI and execute the migration.
When to use it: Organizations with existing ETL tool licenses, teams that prefer visual mapping over code.
Pros: Visual mapping interface. Pre-built connectors. Logging and error handling included.
Cons: License costs (often $50K+ annually). FSC-specific objects (FinancialAccount, ACR, Household) may not have pre-built templates. Tools often assume cleaner 1:1 object parity than FSC actually provides.
Complexity: Medium | Scalability: Enterprise-grade with proper licensing
4. Middleware/iPaaS (Zapier, Make, Workato)
How it works: Configure triggers and actions in an iPaaS platform to move records between systems.
When to use it: Ongoing sync scenarios, small record volumes, supplementary data flows post-cutover. Not for historical migrations.
Pros: Quick setup for simple cases. Good for ongoing sync after the main migration.
Cons: Not designed for bulk historical migration. Per-record execution models become expensive at scale. Limited transformation capabilities. Cannot handle household construction or complex relationship rebuilding.
Complexity: Low | Scalability: Low-volume only
5. Custom ETL Pipeline
How it works: Build a dedicated extract-transform-load pipeline using Python, Node.js, or similar. Extract into a staging database, normalize to a canonical model, then load Salesforce in dependency order.
When to use it: Enterprise volumes, compliance-heavy environments, phased cutovers, zero-downtime requirements.
Pros: Maximum flexibility and control. Full audit trail. Handles any edge case. Fully testable.
Cons: Highest engineering cost. Easy to overbuild for a one-time migration.
Complexity: High | Scalability: Enterprise-grade
Approach Comparison
| Approach | Complexity | Max Scale | Relationships | Attachments | Best For |
|---|---|---|---|---|---|
| CSV Export/Import | Low | ~50K records | Manual rebuild | No | POC, small orgs |
| API-Based | High | Millions | Programmatic | Yes | Full control, serious migrations |
| Third-Party Tools | Medium | Millions | Configurable | Yes | Existing ETL tooling |
| Middleware/iPaaS | Low | Thousands | Limited | Limited | Ongoing sync only |
| Custom ETL | High | Millions | Programmatic | Yes | Complex enterprise transforms |
Which Approach Fits Your Scenario
- Small business (<10K records, simple schema): CSV export/import with Data Loader. Budget 2–3 days.
- Mid-market (10K–500K records, custom objects): API-based migration or third-party tool. Budget 2–6 weeks.
- Enterprise (500K+ records, complex relationships, regulatory requirements): Custom ETL pipeline or managed migration service. Budget 4–12 weeks.
- Ongoing sync requirement: API-based pipeline with scheduled runs, or iPaaS for low-volume scenarios.
When to Consider a Managed Migration Service
Build in-house when you have a dedicated engineering team with experience on both platforms, a simple schema with few custom objects, and a timeline that accommodates iteration.
Conditions that favor external help:
- Household construction from flat data — building household hierarchies from Dynamics 365 contact data requires business logic and heuristic matching that's hard to codify without FSC experience.
- Person Account migration is new to your team — the irreversibility of Person Accounts means mistakes in a production org are permanent.
- Regulatory constraints apply — financial services migrations often require audit trails, data lineage, and compliance documentation that ad-hoc scripts don't produce.
- Your timeline is under 8 weeks for a complex schema — FSC migrations commonly take 3–9 months for enterprise orgs; compressed timelines increase the risk of skipping validation phases.
- The migration sits next to quarter-end reporting or advisor onboarding — the team responsible for retries, reconciliation, and weekend support likely can't also be shipping product.
Common failure modes in DIY migrations:
- Orphaned records: Loading Opportunities before the parent Accounts exist, producing records with null lookups that pass API validation but break reports.
- Broken Householding: Failing to properly link Person Accounts to Household Accounts via ACR, resulting in advisors seeing incomplete client groups.
- API Throttling: Dataverse returns 429s with
Retry-After, and Salesforce tracks org-wide API usage over rolling 24-hour windows. (learn.microsoft.com) - Silent data corruption: Records that import successfully but are operationally wrong — detached activities, duplicate people, households with missing members.
- FSC managed package trigger collisions: Loading large volumes into Person Accounts or ACR fires FSC's managed package Apex triggers, which can hit Apex CPU time limits (10,000ms synchronous, 60,000ms asynchronous) and cause batch failures that don't surface obvious error messages (see Edge Cases below).
The hidden costs aren't just script writing. They're owner mapping, external-ID policy, sandbox rehearsals, limit tuning, error queues, UAT support, and rollback planning.
Pre-Migration Planning
Data Audit
Before extracting anything, audit what's in your Dynamics 365 org:
- Accounts: Count active vs. inactive. Identify parent-child hierarchies.
- Contacts: Count total. Identify contacts without associated accounts (orphans). Flag contacts that should become Person Accounts vs. business contacts.
- Leads: Determine which are still active vs. historical. Decide: migrate as Leads or convert to Person Account + Opportunity?
- Opportunities: Map pipeline stages to FSC Opportunity stages. Count by status (Open, Won, Lost).
- Activities: Total Tasks, Emails, Phone Calls, Appointments. Decide historical cutoff (e.g., last 2 years only).
- Custom Entities: List all custom entities. Determine FSC target: FSC standard object, custom Salesforce object, or exclude.
- Attachments/Notes: Calculate total size. Salesforce has file storage limits — confirm your attachment volume fits within your licensed storage.
- Users: List active users for owner mapping. Provision users in FSC before migration.
- Custom Fields per Object: Count custom fields on each Dynamics 365 entity. FSC's managed package pre-consumes custom field slots — the
Accountobject is most constrained, with the FSC managed package consuming approximately 100–150 of the 500 available custom field slots (varies by FSC version and enabled features). RunSELECT count() FROM FieldDefinition WHERE EntityDefinition.QualifiedApiName = 'Account' AND IsCustom = truein your FSC sandbox to see actual availability.
Define Migration Scope
Not everything needs to move. Common exclusions:
- System-generated audit records
- Duplicate records (deduplicate before migration, not after)
- Activities older than a business-defined cutoff
- Deprecated custom entities no longer in use
- Empty custom fields or leads that have been dead for years
- Test/sandbox data accidentally promoted to production
Migration Strategy
| Strategy | Description | Risk | Best For |
|---|---|---|---|
| Big Bang | Migrate everything in a single cutover window | High — no fallback if issues arise | Small datasets, tight timelines |
| Phased | Migrate by entity group or department | Medium — parallel systems temporarily | Mid-to-large orgs |
| Incremental | Migrate historical data first, then sync delta changes | Low — production stays live | Enterprise with zero-downtime requirement |
For financial services organizations, phased + incremental is almost always the right call: migrate historical data to FSC in phases, validate each phase, then do a final delta sync and cutover. This avoids split-brain data scenarios while giving you validation checkpoints. The FSC-specific load order — Person Accounts first, then relationship objects, then Financial Accounts — aligns naturally with phased execution.
Migration Architecture
The safe architecture is extract → stage → transform → load → reconcile. Both platforms have specific API constraints that shape how you build this pipeline.
Data Flow
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────────┐
│ Dynamics 365 │────▶│ Transform & │────▶│ Salesforce FSC │
│ Sales │ │ Staging Layer │ │ │
│ │ │ │ │ │
│ Dataverse │ │ - ID Crosswalk │ │ - Bulk API 2.0 │
│ Web API │ │ - Field Mapping │ │ - Composite API │
│ (OData v4) │ │ - Deduplication │ │ - Data Loader │
│ │ │ - Household │ │ │
│ 5K records/ │ │ Construction │ │ 100M records/ │
│ page │ │ │ │ 24hr (Bulk API) │
└─────────────────┘ └──────────────────┘ └─────────────────────┘
API Constraints
Dynamics 365 (Source):
- Page size: 5,000 records maximum per API call. Use
@odata.nextLinkfor pagination —$skipis not supported. - Service protection limits: Evaluated per user over 5-minute sliding windows based on three factors — number of requests (6,000 per 5 minutes), combined execution time (20 minutes per 5-minute window), and concurrent requests (52 simultaneous). (learn.microsoft.com)
- Throttling response: HTTP 429 with a
Retry-Afterheader. - For datasets over 100K records: Consider Azure Synapse Link for Dataverse (formerly Export to Data Lake) to bypass API rate limits entirely by reading from a replicated data store.
Salesforce FSC (Target):
- Bulk API 2.0: 100 million records per 24-hour rolling period. Internal batches of 10,000 records. 150 MB per job. 25 concurrent ingest jobs maximum. Note: Bulk API 2.0 manages batching internally — unlike Bulk API v1, you do not create batches explicitly.
- Bulk API v1 (if used): 15,000 batches per rolling 24-hour period. This limit is not shared with Bulk API 2.0, which uses a different internal batching mechanism. If you're using Data Loader, it uses Bulk API v1 by default — switch to Bulk API 2.0 in Data Loader settings for large migrations.
- Composite API: Up to 25 subrequests per call, counts as a single API call. Useful for small dependent chains that must succeed together.
- REST API daily limit: Starts at 100,000 requests per 24-hour period for Enterprise Edition, increases based on licenses (1,000 per Salesforce license, 200 per platform license).
If your Salesforce org has other active integrations using Bulk API, coordinate before running large migration jobs. Bulk API 2.0 concurrent job limits (25 max) are org-wide. Running a marketing automation sync alongside a migration can cause job queue failures on both sides.
Authentication: Both systems require OAuth 2.0. Ensure service accounts have "Modify All Data" permissions in Salesforce. To preserve historical CreatedDate fields, enable the "Set Audit Fields upon Record Creation" permission — this only works on insert, not update. If you mess up the initial load, you must delete and re-insert to keep historical dates.
External IDs
Create a source-ID external field (e.g., D365_Source_ID__c) on every target object before the first load. This turns reruns, upserts, and relationship rebinding from guesswork into standard operations. Both Dataverse and Salesforce support key-based upsert patterns. (learn.microsoft.com)
Persist the crosswalk in a database, not a spreadsheet or in-memory structure. The ID mapping between Dynamics 365 GUIDs and Salesforce IDs is the backbone of your migration.
Step-by-Step Migration Process
Data must be loaded in strict hierarchical order to maintain referential integrity. Each step depends on IDs created in previous steps.
Step 1: Extract Data from Dynamics 365
Use the Dataverse Web API with deterministic ordering for reliable paging. The safest pattern uses modifiedon plus the primary key so reruns are stable.
import requests
import time
def extract_dynamics_entity(base_url, entity_name, access_token, fields=None):
"""Extract all records for a given Dynamics 365 entity with paging."""
headers = {
"Authorization": f"Bearer {access_token}",
"OData-MaxVersion": "4.0",
"OData-Version": "4.0",
"Accept": "application/json",
"Prefer": "odata.maxpagesize=5000"
}
url = f"{base_url}/api/data/v9.2/{entity_name}"
params = []
if fields:
params.append(f"$select={','.join(fields)}")
# Deterministic ordering for stable pagination
params.append("$orderby=modifiedon asc")
if params:
url += "?" + "&".join(params)
all_records = []
while url:
response = requests.get(url, headers=headers)
if response.status_code == 429:
retry_after = int(response.headers.get("Retry-After", 30))
time.sleep(retry_after)
continue
response.raise_for_status()
data = response.json()
all_records.extend(data.get("value", []))
url = data.get("@odata.nextLink")
return all_records
# Extract contacts
contacts = extract_dynamics_entity(
base_url="https://yourorg.crm.dynamics.com",
entity_name="contacts",
access_token=token,
fields=["contactid", "firstname", "lastname", "emailaddress1",
"birthdate", "telephone1", "address1_line1", "address1_city",
"modifiedon"]
)Step 2: Transform Data for FSC
The transformation layer handles three critical tasks: field mapping, ID crosswalk construction, and household construction.
PERSON_ACCOUNT_RECORD_TYPE_ID = "YOUR_RECORD_TYPE_ID"
def transform_contact_to_person_account(d365_contact):
"""Transform a Dynamics 365 Contact to an FSC Person Account."""
return {
"RecordTypeId": PERSON_ACCOUNT_RECORD_TYPE_ID,
"FirstName": d365_contact.get("firstname"),
"LastName": d365_contact.get("lastname"),
"PersonEmail": d365_contact.get("emailaddress1"),
"PersonBirthdate": d365_contact.get("birthdate"),
"Phone": d365_contact.get("telephone1"),
"PersonMailingStreet": d365_contact.get("address1_line1"),
"PersonMailingCity": d365_contact.get("address1_city"),
"PersonMailingState": d365_contact.get("address1_stateorprovince"),
"PersonMailingPostalCode": d365_contact.get("address1_postalcode"),
"D365_Source_ID__c": d365_contact.get("contactid")
}Step 3: Load into Salesforce FSC in Dependency Order
Follow this load order:
- Users — provision and map owners
- Business Accounts — organizations, firms
- Household Accounts — create Household record type Accounts
- Person Accounts — individuals (creates both Account and Contact records)
- Account-Contact Relationships (ACR) — link Person Accounts to Households
- Account-Account Relationships (AAR) — inter-account relationships
- Financial Accounts (
FinServ__FinancialAccount__c) — banking/investment products - Financial Account Roles — must include at least one Primary Owner per Financial Account
- Opportunities — link to Person Accounts/Business Accounts
- Activities (Tasks, Events) — link via
WhatId/WhoId - Attachments/Notes — link to parent records via ContentDocumentLink
Violating this order produces specific errors:
- Loading Opportunities before Accounts →
REQUIRED_FIELD_MISSINGonAccountId - Loading ACR before Person Accounts →
INVALID_CROSS_REFERENCE_KEYonContactId - Loading Financial Accounts without Financial Account Roles → record saves but FSC UI shows validation errors and rollup calculations fail
For large-volume loads, use Bulk API 2.0:
POST /services/data/v67.0/jobs/ingest
{ "object": "Account", "operation": "upsert", "externalIdFieldName": "D365_Source_ID__c" }
PUT /services/data/v67.0/jobs/ingest/{jobId}/batches
<CSV payload>
PATCH /services/data/v67.0/jobs/ingest/{jobId}
{ "state": "UploadComplete" }Build your crosswalk table incrementally — after loading Accounts, capture the new Salesforce IDs before loading records that reference them.
Step 4: Rebuild Relationships
After loading Person Accounts, create ACR records to establish household membership:
def create_acr(person_account_contact_id, household_account_id, role, is_primary):
"""Create Account-Contact Relationship for FSC household."""
return {
"AccountId": household_account_id,
"ContactId": person_account_contact_id,
"Roles": role, # e.g., "Primary Member", "Spouse"
"FinServ__PrimaryGroup__c": is_primary
}Note: ContactId here refers to the Contact ID that was auto-created when the Person Account was inserted. Retrieve this from the Person Account record's PersonContactId field and store it in your crosswalk.
Step 5: Validate
Run validation checks against both source and target after each entity load — don't wait until the end.
- Record counts: Total records per entity in Dynamics 365 vs. FSC
- Field-level spot checks: Sample at least 5% of records and compare field values
- Relationship integrity: Verify all Person Accounts have at least one ACR record
- Opportunity pipeline: Confirm stage distribution matches source
- Activity linkage: Verify activities are linked to correct parent records
- Financial Account ownership: Every FinancialAccount must have a Primary Owner role record
Disable Salesforce triggers, workflow rules, process builders, and FSC-specific validation rules before running migration loads. FSC's managed package includes triggers on Account, Contact, and ACR objects that will fire on every insert. At scale, this causes Apex CPU time limit exceptions and generates thousands of automated notifications. Re-enable and test after validation.
Delta Sync and Incremental Migration
For phased and incremental migrations, you need a reliable mechanism to capture records that changed in Dynamics 365 after your initial extraction.
Dataverse Change Tracking
Dataverse supports two approaches for delta extraction:
1. modifiedon Watermark (Simple):
Store the timestamp of your last extraction. On subsequent runs, filter with $filter=modifiedon gt {last_extraction_timestamp}.
def extract_delta(base_url, entity_name, access_token, watermark, fields=None):
"""Extract records modified after the watermark timestamp."""
headers = {
"Authorization": f"Bearer {access_token}",
"OData-MaxVersion": "4.0",
"OData-Version": "4.0",
"Accept": "application/json",
"Prefer": "odata.maxpagesize=5000"
}
filter_clause = f"$filter=modifiedon gt {watermark}"
url = f"{base_url}/api/data/v9.2/{entity_name}?{filter_clause}&$orderby=modifiedon asc"
if fields:
url += f"&$select={','.join(fields)}"
all_records = []
while url:
response = requests.get(url, headers=headers)
if response.status_code == 429:
time.sleep(int(response.headers.get("Retry-After", 30)))
continue
response.raise_for_status()
data = response.json()
all_records.extend(data.get("value", []))
url = data.get("@odata.nextLink")
return all_recordsLimitation: This approach doesn't capture deleted records. If records are deleted in Dynamics 365 during the migration window, they'll remain as orphans in FSC.
2. Dataverse Change Data Capture (Robust):
Enable change tracking on the entity in Dynamics 365 (Settings > Customizations > Entity > Enable Change Tracking). Then use the RetrieveEntityChanges function via the Dataverse SDK or Web API, which returns created, updated, and deleted records since a given version token. (learn.microsoft.com)
This is the preferred approach for enterprise migrations because it captures deletions, provides a version token for exactly-once processing, and is more efficient than timestamp-based queries on large tables.
Final cutover delta sync: During the cutover window, set Dynamics 365 to read-only, run one final delta extraction, load into FSC, validate, then switch users to FSC. The cutover window length depends on your delta volume — for most organizations, this is 2–8 hours.
Edge Cases & Challenges
Polymorphic Lookups
Dynamics 365 uses regardingobjectid which can point to an Account, Contact, or Opportunity. Salesforce requires explicit lookup fields (WhatId for Accounts/Opportunities, WhoId for Contacts/Leads). Your transformation script must inspect the regardingobjectid_*@odata.type annotation to determine the source entity type and route to the correct Salesforce field.
def resolve_regarding_lookup(activity, crosswalk):
"""Map Dynamics 365 regardingobjectid to Salesforce WhatId/WhoId."""
entity_type = activity.get("regardingobjectid_*@Microsoft.Dynamics.CRM.lookuplogicalname")
source_id = activity.get("_regardingobjectid_value")
if not source_id:
return {"WhatId": None, "WhoId": None}
sf_id = crosswalk.get(source_id)
if entity_type == "contact":
return {"WhoId": sf_id, "WhatId": None}
elif entity_type in ("account", "opportunity"):
return {"WhatId": sf_id, "WhoId": None}
else:
# Custom entity — may need WhatId if the custom object supports it
return {"WhatId": sf_id, "WhoId": None}Duplicate Records
Dynamics 365 orgs often contain duplicate contacts. Deduplicate before migration — not after. Importing duplicates into FSC creates duplicate Person Accounts, which are harder to merge because Person Account merging has constraints compared to standard Account merging. FSC documentation notes that duplicate detection is supported, but merge is not fully supported in all FSC scenarios. (help.salesforce.com)
Attachments and Notes
Dynamics 365 stores attachments in the annotation entity with Base64-encoded content in documentbody. These payloads are suited to smaller files; larger files may require chunked handling. (learn.microsoft.com)
Salesforce stores files as ContentDocument/ContentVersion records. The migration requires at least two steps:
- Create ContentVersion records in Salesforce with the file data
- Create ContentDocumentLink records to associate files with parent records
Implementation guidance:
- Stream files individually rather than loading all Base64 strings into memory — this prevents memory bloat on large attachment sets
- Salesforce ContentVersion uploads support up to 2 GB per file, but Base64 encoding increases payload size by ~33%. A 1.5 GB source file in Dynamics 365 produces a ~2 GB Base64 payload
- For orgs with very large attachment volumes (>50 GB total), consider extracting files to Azure Blob Storage first, then using Salesforce's REST API to stream uploads directly rather than passing through your ETL pipeline
- File storage counts against your Salesforce org limits (10 GB base + per-license allocation). Calculate total attachment size before migration and confirm org capacity
FinancialAccount Primary Owner Requirement
Each FSC Financial Account must have at least one active Financial Account Role of type Primary Owner, or the insert will fail. If your Dynamics data has orphaned financial records without clear ownership, clean this up before loading. (help.salesforce.com)
ACR Deactivation Constraint
If you deactivate an Account Contact Relationship between a business and an individual in FSC, Salesforce requires you to reactivate the original relationship rather than create a new one. Plan your ACR loading strategy accordingly — if you need to modify ACRs after initial load, update the existing records rather than deleting and recreating them. (help.salesforce.com)
FSC Managed Package Governor Limits
This is a non-obvious production failure mode. FSC's managed package includes Apex triggers on Account, Contact, AccountContactRelation, and FinServ__FinancialAccount__c objects. These triggers fire on every DML operation, including Bulk API loads.
What happens at scale:
- Loading 500,000 Person Accounts via Bulk API 2.0 fires FSC's managed
AccountandContacttriggers on every internal batch of 10,000 records - These triggers consume Apex CPU time (10,000ms synchronous limit per transaction) and can invoke SOQL queries that count against the 100 SOQL queries per transaction limit
- If FSC triggers push a batch past governor limits, you get
System.LimitException: Apex CPU time limit exceeded— the entire batch of 10,000 records fails, with no partial success - The error messages reference managed package code you can't inspect or modify
Mitigations:
- Disable FSC managed package triggers during migration if possible — this requires Salesforce support to temporarily unlock the managed package, which is not always granted
- Reduce Bulk API 2.0 batch size by splitting CSV uploads into smaller files (e.g., 2,000 records per job instead of relying on the default 10,000-record internal batch)
- Avoid loading Person Accounts alongside any custom triggers you've built on Account or Contact — the cumulative trigger execution time adds up
- Test with realistic data volumes in a Full Sandbox — Developer sandboxes have different governor limit behavior and won't surface these issues
API Failures and Throttling
Both platforms throttle aggressively under load. Build retry logic with exponential backoff:
- Dynamics 365: Watch for HTTP 429 responses. The
Retry-Afterheader tells you how long to wait (typically 5–60 seconds). - Salesforce: Bulk API 2.0 jobs can fail if internal batches time out (10-minute limit per 10,000-record batch). Monitor job status via
GET /jobs/ingest/{jobId}and resubmit failed records from thefailedResultsendpoint.
Log at minimum: source object, source ID, target object, target ID, batch ID, status code, error message, retry count, and timestamp.
Custom Fields Without FSC Equivalents
Dynamics 365 custom fields that have no FSC equivalent require creating custom fields in Salesforce. FSC orgs have the same custom field limits as any Salesforce org (800 custom fields per object for Enterprise Edition), but the FSC managed package already consumes a significant portion of this allocation on key objects. Check remaining capacity on Account, Contact, Opportunity, and FinServ__FinancialAccount__c before creating migration-specific custom fields.
Limitations & Constraints
FSC-specific limitations:
- Person Accounts cannot be converted back to standard Accounts
- The Individual model is legacy — Salesforce only supports it for existing implementations
- FSC relationship objects (AAR, CCR) are managed package objects with limited customization
- FinancialAccount record types are predefined; adding custom types requires careful configuration
- FSC managed package triggers fire on all DML, including bulk loads — this is not configurable
Salesforce platform constraints:
- Bulk API 2.0 doesn't transfer compound fields (e.g.,
MailingAddress). Map individual address components (PersonMailingStreet,PersonMailingCity, etc.). - 25 concurrent Bulk API 2.0 ingest jobs maximum across the entire org
- ContentVersion uploads are limited to 2 GB per file
- Governor limits apply per-transaction: 100 SOQL queries, 150 DML statements, 10,000ms Apex CPU time (synchronous)
Dynamics 365 extraction constraints:
- 5,000 records per page — large entities require hundreds of paginated calls
- Service protection limits: 6,000 requests, 20 minutes execution time, 52 concurrent requests per 5-minute window per user
- Annotation exports can be slow for large file counts due to Base64 encoding overhead
$skipis not supported for pagination — use@odata.nextLinkonly- Change tracking must be explicitly enabled per entity before first use
Validation & Testing
Do not trust API success responses blindly.
Record Count Comparison
Run count queries on both sides for every migrated entity. Mismatches indicate dropped records during transformation or failed API calls that weren't retried.
Field-Level Validation
Export a random sample (minimum 5%) from both Dynamics 365 and FSC. Compare field-by-field using the source GUID stored in the external ID field. Automate this comparison — manual spot checks miss systematic mapping errors.
-- Salesforce: Count Person Accounts with source IDs
SELECT COUNT(Id) FROM Account WHERE RecordType.Name = 'Person Account' AND D365_Source_ID__c != null
-- Salesforce: Find orphaned Person Accounts (no Household ACR)
SELECT Id, Name FROM Account
WHERE RecordType.Name = 'Person Account'
AND Id NOT IN (SELECT AccountId FROM AccountContactRelation WHERE Account.RecordType.Name = 'Household')UAT Process
- Have 2–3 financial advisors or relationship managers verify their client data in FSC
- Check household structures — are family members correctly grouped?
- Verify Financial Account associations — do account balances and types match?
- Test pipeline views — do Opportunity stages and amounts look correct?
- Confirm activity history — can advisors see the complete interaction timeline?
Always run UAT in a Full Sandbox before production cutover.
Rollback Planning
FSC rollback is destructive — you can't "undo" Person Account creation. Your rollback plan should include:
- Maintaining the Dynamics 365 org in read-only mode during migration
- If migration fails, reverting users to Dynamics 365 and deleting migrated records from FSC using cascading deletes keyed on external IDs
- Never migrating directly to a production FSC org without a full dry run in sandbox
- A pre-built script to execute cascading deletes in reverse dependency order (Attachments → Activities → Opportunities → Financial Accounts → ACR → Person Accounts → Household Accounts → Business Accounts)
Post-Migration Tasks
Re-enable automations: Turn Salesforce Flows, Apex triggers, validation rules, and FSC managed package triggers back on. Run a small batch of test records through each automation to verify they fire correctly against the migrated data.
Rebuild integrations: Dynamics 365 Power Automate flows and business rules don't transfer to Salesforce. Rebuild them as Salesforce Flows. Any systems currently integrated with Dynamics 365 — marketing automation, document management, telephony, ERP — need to be repointed to FSC endpoints. Map these dependencies before cutover.
User training: FSC's Person Account model and Household navigation are significantly different from Dynamics 365's Account/Contact structure. Budget dedicated training time — advisors need to learn the new navigation patterns, relationship views, and Financial Account components.
Monitor data quality: Run weekly data quality reports for the first month post-migration. Watch for:
- Orphaned Person Accounts (no Household association)
- Opportunities without owners
- Activities without parent records
- Duplicate Person Accounts created by integrations that weren't updated
- Financial Accounts without Primary Owner roles
Best Practices
- Back up everything — Generate a full backup of Dynamics 365 before starting. Store backups independently of both platforms.
- External IDs are mandatory — Create
D365_Source_ID__c(or equivalent) as an External ID on every target object. Use this for all upserts to prevent duplicates and enable relationship rebinding. - Run full rehearsal migrations in sandbox — Execute at least one complete dry run with production-scale data in a Full Sandbox. Developer Pro sandboxes have limited storage and different governor limit profiles.
- Validate incrementally — Check record counts and sample data after each entity load. Don't batch all validation to the end.
- Coordinate with active integrations — Any systems currently integrated with Dynamics 365 need to be repointed to FSC. Map these dependencies before cutover.
- Plan for FSC's data model learning curve — Even experienced Salesforce admins need time to understand ACR, AAR, Household rollups, and Financial Account relationships. FSC is conceptually different from standard Salesforce.
- Disable all automations during load — Turn off Salesforce triggers, workflow rules, process builders, and validation rules during data import. This includes requesting temporary deactivation of FSC managed package triggers if Salesforce support will grant it.
Sample Field Mapping Reference
| Dynamics 365 Field | API Name | Salesforce FSC Field | API Name | Type Notes |
|---|---|---|---|---|
| Account Name | name |
Account Name | Name |
Direct |
| Phone | telephone1 |
Phone | Phone |
Direct |
| Website | websiteurl |
Website | Website |
Direct |
| Industry | industrycode |
Industry | Industry |
Option Set → Picklist |
| Annual Revenue | revenue |
Annual Revenue | AnnualRevenue |
Money → Currency |
| Contact First Name | firstname |
First Name | FirstName |
Person Account field |
| Contact Last Name | lastname |
Last Name | LastName |
Person Account field |
| Contact Email | emailaddress1 |
PersonEmail |
Person Account field | |
| Contact Birthdate | birthdate |
Birthdate | PersonBirthdate |
Date → Date |
| Contact ID | contactid |
Source ID | D365_Source_ID__c |
Text (External ID) |
| Opportunity Name | name |
Opportunity Name | Name |
Direct |
| Est. Revenue | estimatedvalue |
Amount | Amount |
Money → Currency |
| Est. Close Date | estimatedclosedate |
Close Date | CloseDate |
DateTime → Date |
| Pipeline Phase | salesstagecode |
Stage | StageName |
Option Set → Picklist |
| Owner | ownerid |
Owner | OwnerId |
GUID → Salesforce ID (via crosswalk) |
| Status | statecode |
Active | IsActive |
Transform 0/1 to True/False |
Making the Transition Work
Migrating from Dynamics 365 Sales to Salesforce Financial Services Cloud is not a weekend project. The fundamental data model difference — flat CRM entities vs. FSC's Person Account/Household/FinancialAccount hierarchy — means every record must be transformed, not just copied. The irreversibility of Person Accounts raises the stakes for every decision made during planning.
Organizations that succeed invest in thorough data auditing, build the crosswalk infrastructure before touching production, run full-scale rehearsals in sandbox, and validate obsessively at each phase. Organizations that struggle treat it like a standard CRM-to-CRM copy and discover FSC's complexity — particularly managed package governor limits and household construction — too late.
If your move is small and flat, CSV plus Data Loader may be enough. If it includes Person Accounts, households, custom tables, historical activities, or advisor workflows, treat it like a schema-translation project with staging, replay, and reconciliation from day one.
Frequently Asked Questions
- How do you map Dynamics 365 Contacts to Salesforce Financial Services Cloud?
- Dynamics 365 Contacts map to Person Accounts in FSC, not standard Salesforce Contacts. Person Accounts combine Account and Contact fields into a single record. You must also create Account-Contact Relationship (ACR) records to link Person Accounts to Household Accounts, which is how FSC models family and client group structures.
- What are the API rate limits for Dynamics 365 and Salesforce FSC during migration?
- Dynamics 365 returns a maximum of 5,000 records per API page and enforces service protection limits over 5-minute sliding windows. Salesforce Enterprise Edition starts at 100,000 REST API requests per 24 hours, and Bulk API 2.0 supports up to 100 million records per 24-hour period with 10,000 records per internal batch and a 150 MB per-job limit.
- What is the correct load order for a Salesforce FSC migration?
- The recommended load order is: 1) Users, 2) Business Accounts, 3) Household Accounts, 4) Person Accounts, 5) Account-Contact Relationships (ACR), 6) Account-Account Relationships (AAR), 7) Financial Accounts, 8) Opportunities, 9) Activities, 10) Attachments/Notes. Each step depends on IDs created in previous steps.
- Can you undo Person Account migration in Salesforce FSC?
- No. Enabling Person Accounts in a Salesforce org is irreversible — you cannot convert Person Accounts back to standard Accounts. This makes thorough testing in a sandbox org critical before any production migration. Your rollback plan should involve reverting users to Dynamics 365 and deleting records from the FSC sandbox.
- How do you handle Dynamics 365 attachments during a Salesforce FSC migration?
- Dynamics 365 stores attachments as Base64-encoded content in the annotation entity. You must extract the binary content, create ContentVersion records in Salesforce, then create ContentDocumentLink records to associate files with parent records. Stream files individually to avoid memory bloat, and verify total attachment size against your Salesforce file storage limits.

