DataVerse

Migrating Customer Insights Data Between Dataverse Environments: What We Learned

We had a working PowerShell script that migrates 27 Sales tables between Dataverse environments via the Web API. The question came up: what happens when the source is a Dynamics 365 Customer Insights instance? What extra tables need migrating, and what traps are waiting?

This post covers what we found, what we built, and the quirks that would have bitten us in production.

The Starting Point

Our migration script (Migrate-Dataverse-1.ps1) already handles the hard parts of Dataverse data migration:

  • Dependency ordering — topological sort ensures lookup targets exist before referencing records
  • Circular dependencies — two-pass strategy creates stubs first, patches lookups second
  • State management — products, quotes, and activities are created in Draft state, then set to their correct state after all references resolve
  • Lookup remapping — owners by name, currencies by ISO code, UoMs by name
  • Activity parties — can’t be created standalone, so they’re patched onto parent activities via navigation properties

All of this worked well for the core Sales tables. But Customer Insights brings its own world of complexity.

Scanning the Source Instance

We started by scanning the source environment’s schema. The results were revealing:

  • 68 CI-related tables in the schema (prefixed msdynmkt_ and msfp_)
  • Only 8 had data — all consent/configuration tables
  • The core CI content tables (journeys, segments, emails, interactions) had no data — Customer Insights – Journeys wasn’t fully provisioned

This immediately told us something important: you can have CI tables in your schema without having CI fully deployed. The migration script needs to handle both cases gracefully.

The Four Tiers of CI Tables

Not all CI tables are created equal. We categorised them into tiers based on criticality and migration complexity.

Tier 1: Consent & Compliance — The Non-Negotiable Ones

These are legally binding records. Get them wrong and you’re sending marketing emails to people who opted out.

Table What It Does
msdynmkt_purpose Consent purposes (“Marketing”, “Transactional”)
msdynmkt_topic Topics under purposes
msdynmkt_compliancesettings4 Compliance configuration
msdynmkt_consentprovider Consent provider config
msdynmkt_preferencecenter Preference centre definitions
msdynmkt_contactpointconsent4 Per-contact opt-in/out records
msdynmkt_consent General consent records

The migration order matters here: purposes before topics, config before consent records, and all of it after contacts and leads (since consent records reference them).

Tier 2: Journeys & Segments

Table What It Does
msdynmkt_journey Journey definitions
msdynmkt_journeyinstance Journey participation records
msdynmkt_segment Segment definitions
msdynmkt_segmentdefinition Segment query logic

These come with significant caveats (more on that below).

Tier 3: Marketing Content

Email templates, marketing emails, forms, form submissions, SMS messages, push notifications. Standard parent-child relationships — templates before emails, forms before submissions.

Tier 4: Customer Voice (Forms Pro)

Survey tables follow a strict chain: msfp_project > msfp_survey > msfp_question > msfp_surveyinvite > msfp_surveyresponse > msfp_questionresponse. The dependency graph handles this automatically, but you need to know the chain exists.

The Quirks That Would Have Burned Us

1. Consent Table Versioning

This was the most surprising finding. Customer Insights has gone through four versions of its consent table:

  • msdynmkt_contactpointconsent (v1) — deprecated
  • msdynmkt_contactpointconsent2 (v2) — deprecated
  • msdynmkt_contactpointconsent3 (v3) — deprecated
  • msdynmkt_contactpointconsent4 (v4) — current

All four can exist in the schema simultaneously. If you migrate v2 data thinking it’s current, you’ll have stale consent records in your target — and CI will be reading v4.

Our fix: v1-v3 go into the system excludes list. Only v4 is migrated.

2. Virtual Elastic Tables Look Real But Aren’t

Tables prefixed msdynci_ (Customer Insights Data) appear in the entity metadata like any other table. But they’re virtual elastic tables backed by Azure infrastructure. They’re read-only via the API — Microsoft’s documentation explicitly states: “use Dataverse APIs only to read data (GET)”.

Attempting to write to them will fail. Worse, they sync automatically when CI is connected to the target environment, so migrating them would be pointless even if you could.

Our fix: All msdynci_* tables go into system excludes unconditionally.

3. Interaction Data Lives Somewhere Else Entirely

If you’re expecting to find email opens, click tracking, bounce data, and web interactions in Dataverse — you won’t. CI stores its primary analytics/interaction data in Azure Data Lake, not Dataverse. The msdynmkt_emailinteraction table (if it even exists in your schema) contains only a synced subset.

Migrating full interaction history requires a separate Azure Data Lake export process. It’s simply not possible via the Web API.

4. Journey State Management

Journeys have lifecycle states: Draft, Live, Stopped, Completed. Like products in our Sales migration, they need to be created in a safe state first, then transitioned after all references are in place.

Journey definitions also contain embedded JSON with GUIDs referencing segments, emails, and other records. If your migration changes GUIDs, those internal references break silently — the journey looks fine until you try to run it and it references records that don’t exist.

Our script preserves source GUIDs via upsert (PATCH with the same ID), so this isn’t a problem for us. But if you’re using a tool that generates new GUIDs, you’d need a JSON rewriting step.

Our fix: msdynmkt_journey added to the deferred state set alongside products and quotes.

5. Segment Membership is Computed, Not Stored

msdynmkt_segment contains the segment definition (the query logic), not the membership. Actual membership lives in msdynci_segmentmembership — which is one of those virtual read-only tables.

After migrating a segment definition, the CI backend in the target environment must re-evaluate the query against target data. If the underlying contact/lead data hasn’t been migrated yet, the segment will evaluate to empty.

Implication: Migrate contacts and leads before segments, and expect a delay before segment membership populates.

6. Platform-Managed Tables That Look Like Data

Several msdynmkt_ tables look like they contain useful configuration, but they’re actually managed by the CI platform and get recreated during provisioning:

  • msdynmkt_featureconfiguration — feature flags
  • msdynmkt_contactpointsettings — contact point config
  • msdynmkt_configuration — system config
  • Channel instance tables (*channelinstance*) — per-environment channel provider setup

Migrating these would either fail (the platform overwrites them) or cause conflicts. They’re excluded unconditionally.

What We Built

The implementation was deliberately minimal — a single switch parameter that extends the existing script’s behaviour:

# Without CI (current behaviour - sales only):
.\Migrate-Dataverse-1.ps1 -SourceUrl "..." -TargetUrl "..." ...

# With CI (adds consent, journeys, segments, surveys):
.\Migrate-Dataverse-1.ps1 -SourceUrl "..." -TargetUrl "..." -CustomerInsights ...

The changes:

  1. -CustomerInsights switch parameter — opt-in, no impact on existing behaviour when absent
  2. 30 CI tables added to the filter set when the switch is active
  3. 45+ CI tables added to system excludes unconditionally (virtual, deprecated, platform-managed, channel config) — these are excluded whether or not the switch is used
  4. Journey state deferralmsdynmkt_journey joins products/quotes in the deferred state set
  5. Console output confirming the mode

The existing dependency graph, topological sort, and two-pass cycle handling all work without modification. CI tables have ManyToOne relationships like any other Dataverse table, so the infrastructure handles ordering automatically.

What About Plain Dataverse (Non-Dynamics)?

If you’re migrating a Dataverse environment that doesn’t have Dynamics 365 at all, there are additional considerations the CI work surfaced:

  • File and image columns store binary data that doesn’t come through in the standard JSON response. You need separate GET/PATCH /{entityset}({id})/{column}/$value calls.
  • Elastic tables (Cosmos DB-backed) use the same API surface but have different pagination and query behaviour. They can’t be dependency-sorted the same way.
  • Virtual tables connected to external sources (SQL, SharePoint) should always be excluded — detect via EntityDefinition.DataProviderId.
  • Business Process Flow instances track which stage a record is at. The process definition migrates via Solutions, but the stage data is per-record and needs data migration.
  • Environment variable values are deliberately environment-specific (API keys, endpoint URLs). Definitions migrate via Solutions; values should be flagged for manual review, not blindly copied.
  • Audit history is read-only via the API. If you need it, export to Data Lake.
  • Attachment size limits on the annotation table differ between environments. A source with 128MB limit and a target with the default 5MB will silently fail on large attachments.

Lessons Learned

  1. Schema presence doesn’t mean data presence. 68 CI tables in the schema, 8 with data. Always scan before assuming.
  2. Deprecated tables can coexist with current ones. The consent versioning story is a cautionary tale about assuming the first matching table is the right one.
  3. Not everything in Dataverse is Dataverse data. Virtual tables, Data Lake storage, and platform-managed config all look like tables but behave very differently.
  4. Preserve GUIDs if you possibly can. Journey JSON, segment definitions, and workflow references all embed GUIDs. Changing them creates a cascade of silent breakage.
  5. State management is a pattern, not an exception. Products need it. Activities need it. Journeys need it. Any table with a lifecycle state probably needs it. Build the pattern once and extend it.

The full script is in the repository. The CI changes are backwards-compatible — running without -CustomerInsights produces identical results to the original.

Leave a Comment

Your email address will not be published. Required fields are marked *