Dataverse ALM Power Platform

The Dataverse Solution Layer Trap: Why Your Managed Form Changes Disappear

The Scenario

You’re a Dataverse developer working on a CRM integration project. The system has been in production for over a year. Multiple managed solutions have been layered into target environments over time — each one carrying forms, columns, views, and workflows.

Your task: replace an old text field (crx_vendorgroupid) on the Account form with a proper lookup (crx_vendorgroup) that references a new configuration table. Clean, normalized, the way it should have been from day one.

You do everything right:

  1. In Dev, you add the new lookup column to the Account form
  2. You remove the old text field from the form
  3. You save, publish
  4. You add the form to your solution, export as managed
  5. You import the managed solution into Test

You open the Account form in Test. The new lookup is there. But the old text field? Still there. Staring right back at you.

You re-publish. You clear your browser cache. You check twice. It won’t go away.

What Went Wrong

Nothing. You did everything correctly. This is by design.

How Dataverse Merges Managed Solution Forms

When multiple managed solutions contain the same form, Dataverse doesn’t pick a winner. It merges all layers together — and the merge is additive.

Final Form XML = Layer 1 (oldest) + Layer 2 + Layer 3 + ... + Layer N (newest)

“Additive” means: if ANY layer has a control on the form, it appears in the merged result. Your newest solution removed the field from its layer, but an older solution still has it. The merge unions them all.

Here’s what was happening in the Test environment:

graph BT
    subgraph merged["  MERGED FORM  — what users see  "]
        F1["crx_vendorgroup &#40;lookup&#41;<br/>← from Solution B"]
        F2["crx_vendorgroupid &#40;text&#41;<br/>← from Solution A"]
        F3["primarycontactid<br/>← from both"]
    end

    subgraph solA["  Solution A  ·  v1.0  ·  Jun 2025  "]
        A1["✗  HAS the old text field"]
    end

    subgraph solB["  Solution B  ·  v2.0  ·  Feb 2026  "]
        B1["✓  Does NOT have it"]
    end

    subgraph solC["  Solution C  ·  Security  ·  May 2024  "]
        C1["—  Has form, no field changes"]
    end

    solA -->|"additive merge"| merged
    solB -->|"additive merge"| merged
    solC -->|"additive merge"| merged

    style merged fill:#2d1b3d,stroke:#cf6679,stroke-width:2px,color:#f5f5f5
    style solA fill:#4a1942,stroke:#cf6679,stroke-width:2px,color:#f5f5f5
    style solB fill:#1b3a4b,stroke:#4fc3f7,stroke-width:2px,color:#f5f5f5
    style solC fill:#1b3a4b,stroke:#81c784,stroke-width:2px,color:#f5f5f5
    style F1 fill:#1b3a4b,stroke:#4fc3f7,color:#f5f5f5
    style F2 fill:#4a1942,stroke:#cf6679,color:#f5f5f5
    style F3 fill:#263238,stroke:#90a4ae,color:#f5f5f5
    style A1 fill:#4a1942,stroke:#cf6679,color:#f5f5f5
    style B1 fill:#1b3a4b,stroke:#4fc3f7,color:#f5f5f5
    style C1 fill:#263238,stroke:#81c784,color:#f5f5f5

Solution B (your latest, clean export) correctly has the form without crx_vendorgroupid. But Solution A (the older, larger solution from a previous team) still has the old form layout — including the text field. Dataverse merges both layers, and the old field reappears.

The Investigation

The tricky part is that none of this is visible at first glance. The form in Dev works perfectly. The export looks clean. The import succeeds without errors.

To diagnose, you need to look at two things:

1. Merged XML vs Managed XML

The systemform record in Dataverse has two XML fields:

FieldWhat it contains
formxmlThe merged form XML — the final result after all solution layers are combined. This is what users see.
formxmlmanagedThe base managed layer XML — just your solution’s contribution.

In the failing Test environment:

formxml (merged):        73,884 chars — HAS crx_vendorgroupid
formxmlmanaged (layer):  29,428 chars — does NOT have crx_vendorgroupid

Your solution is clean. Something else is putting the field back.

2. Solution Component Inventory

Query which solutions contain the old column as a component:

Solutions containing crx_vendorgroupid attribute:
  → CorePlatform_MVP (MANAGED, v1.0, Jun 2025)    ← HERE

Solutions containing crx_vendorgroup lookup:
  → PlatformEnhancements (MANAGED, v2.0, Feb 2026) ← Your solution

The old solution still owns the old column — and its form layer still has it on the form.

Why This Happens

This situation is almost inevitable in any Dataverse project that:

  1. Evolves over time — early solutions carry form layouts that later solutions want to change
  2. Has multiple solution publishers — security roles, core platform, feature releases, each touching the same forms
  3. Uses managed solutions in target environments — which is the recommended practice

The longer a project runs, the more managed solution layers accumulate on key forms like Account, Contact, and Opportunity. Each layer contributes controls, and the merge only grows.

The Progression

graph TB
    subgraph row1[" "]
        direction LR
        subgraph m1["  Month 1  "]
            M1["Solution A deploys Account form<br/>✅ 10 fields on form"]
        end
        subgraph m4["  Month 4  "]
            M4["Solution B adds 3 fields<br/>✅ 13 fields on form"]
        end
        subgraph m8["  Month 8  "]
            M8["Solution C adds security fields<br/>✅ 15 fields on form"]
        end
        m1 --> m4 --> m8
    end

    subgraph row2[" "]
        direction LR
        subgraph m12["  Month 12 — The Problem  "]
            M12A["Solution D replaces 2 fields with lookups"]
            M12B["Removes old fields from its layer"]
            M12C["But Solutions A, B, C still have them"]
            M12D["Merged form: 17 fields — not 15!"]
            M12A --> M12B --> M12C --> M12D
        end
    end

    m8 --> m12

    style row1 fill:none,stroke:none
    style row2 fill:none,stroke:none
    style m1 fill:#1b3a4b,stroke:#4fc3f7,stroke-width:2px,color:#f5f5f5
    style m4 fill:#1b3a4b,stroke:#4fc3f7,stroke-width:2px,color:#f5f5f5
    style m8 fill:#1b3a4b,stroke:#81c784,stroke-width:2px,color:#f5f5f5
    style m12 fill:#4a1942,stroke:#cf6679,stroke-width:2px,color:#f5f5f5
    style M1 fill:#263238,stroke:#4fc3f7,color:#f5f5f5
    style M4 fill:#263238,stroke:#4fc3f7,color:#f5f5f5
    style M8 fill:#263238,stroke:#81c784,color:#f5f5f5
    style M12A fill:#2d1b3d,stroke:#cf6679,color:#f5f5f5
    style M12B fill:#2d1b3d,stroke:#cf6679,color:#f5f5f5
    style M12C fill:#4a1942,stroke:#cf6679,color:#f5f5f5
    style M12D fill:#4a1942,stroke:#e94560,color:#fff,stroke-width:3px

Every refactoring that replaces or removes a field hits this wall.

The Fix

You have three options, each with different trade-offs:

Option 1: Manual Override (Quick, Per-Environment)

Open the form in the target environment (Test, UAT, Prod), manually remove the field, save, and publish. This creates an Active (unmanaged) customization layer that takes precedence over all managed layers.

ProCon
30-second fixMust repeat per environment
Zero risk to solutionsCreates unmanaged customizations (some orgs discourage this)
ImmediateDoesn’t travel with solution exports

Instead of removing the field from your solution’s form, add it back but set it to hidden (visible = false). When the same control exists in multiple layers, the highest-priority layer’s properties win.

graph LR
    subgraph old["  Solution A ·  older  "]
        OV["crx_vendorgroupid<br/>visible = true"]
    end

    subgraph new["  Solution B ·  newer, higher priority  "]
        NV["crx_vendorgroupid<br/>visible = false"]
    end

    subgraph result["  Merged Result  "]
        RV["crx_vendorgroupid<br/>visible = false  ✓"]
    end

    old -->|"property merge"| result
    new -->|"wins"| result

    style old fill:#4a1942,stroke:#cf6679,stroke-width:2px,color:#f5f5f5
    style new fill:#1b3a4b,stroke:#4fc3f7,stroke-width:2px,color:#f5f5f5
    style result fill:#1a3a2a,stroke:#81c784,stroke-width:2px,color:#f5f5f5
    style OV fill:#4a1942,stroke:#cf6679,color:#f5f5f5
    style NV fill:#1b3a4b,stroke:#4fc3f7,color:#f5f5f5
    style RV fill:#1a3a2a,stroke:#81c784,color:#f5f5f5
ProCon
Travels with the solutionField is still technically on the form (just hidden)
Works across all environments on importRequires understanding of the layering issue
No manual steps per environmentMinor maintenance debt

This is the pragmatic fix — you can’t remove what another solution contributes, but you can override its properties.

The Cell ID Trap (Why “Just Hide It” Can Fail)

If you try this and the field is still visible in the target environment, you’ve likely hit the next layer of the problem: cell GUID mismatch.

Every control on a Dataverse form lives inside a <cell> element, and every cell has a unique GUID:

<cell id="{4cf1a254-03b6-4c33-8355-60dc2d2290d3}" visible="true">
    <control id="crx_vendorgroupid" datafieldname="crx_vendorgroupid" />
</cell>

When you add a field back to the form in Dev using the form designer, Dataverse generates a new cell GUID. Your solution’s form layer now carries the field in a different cell than the old solution’s layer:

graph TB
    subgraph problem["  The Problem — Different Cell GUIDs  "]
        direction LR
        subgraph solA2["  Solution A ·  older  "]
            CA["cell {4cf1a254...}<br/>visible = true<br/>crx_vendorgroupid"]
        end
        subgraph solB2["  Solution B ·  newer  "]
            CB["cell {42e8ce66...}<br/>visible = false<br/>crx_vendorgroupid"]
        end
        subgraph merged2["  Merged Result  "]
            MA["cell {4cf1a254...}<br/>visible = true  ✗"]
            MB["cell {42e8ce66...}<br/>visible = false"]
        end
        solA2 -->|"different GUIDs"| merged2
        solB2 -->|"both included"| merged2
    end

    subgraph fix["  The Fix — Matching Cell GUIDs  "]
        direction LR
        subgraph solA3["  Solution A ·  older  "]
            CA2["cell {4cf1a254...}<br/>visible = true<br/>crx_vendorgroupid"]
        end
        subgraph solB3["  Solution B ·  newer  "]
            CB2["cell {4cf1a254...}<br/>visible = false<br/>crx_vendorgroupid"]
        end
        subgraph merged3["  Merged Result  "]
            MC["cell {4cf1a254...}<br/>visible = false  ✓"]
        end
        solA3 -->|"same GUID"| merged3
        solB3 -->|"property wins"| merged3
    end

    style problem fill:none,stroke:#cf6679,stroke-width:2px,color:#f5f5f5
    style fix fill:none,stroke:#81c784,stroke-width:2px,color:#f5f5f5
    style solA2 fill:#4a1942,stroke:#cf6679,stroke-width:2px,color:#f5f5f5
    style solB2 fill:#1b3a4b,stroke:#4fc3f7,stroke-width:2px,color:#f5f5f5
    style merged2 fill:#2d1b3d,stroke:#cf6679,stroke-width:2px,color:#f5f5f5
    style solA3 fill:#4a1942,stroke:#cf6679,stroke-width:2px,color:#f5f5f5
    style solB3 fill:#1b3a4b,stroke:#4fc3f7,stroke-width:2px,color:#f5f5f5
    style merged3 fill:#1a3a2a,stroke:#81c784,stroke-width:2px,color:#f5f5f5
    style CA fill:#4a1942,stroke:#cf6679,color:#f5f5f5
    style CB fill:#1b3a4b,stroke:#4fc3f7,color:#f5f5f5
    style MA fill:#4a1942,stroke:#e94560,color:#f5f5f5,stroke-width:3px
    style MB fill:#263238,stroke:#90a4ae,color:#f5f5f5
    style CA2 fill:#4a1942,stroke:#cf6679,color:#f5f5f5
    style CB2 fill:#1b3a4b,stroke:#4fc3f7,color:#f5f5f5
    style MC fill:#1a3a2a,stroke:#81c784,color:#f5f5f5,stroke-width:3px

Dataverse merges cells by their GUID. Different GUIDs = different cells = both appear. Your hidden cell is there, but the old visible one is too. The field shows up because the old cell still has visible = true.

How to fix the cell GUID

The form designer won’t let you choose a cell’s GUID. You need to edit the form XML directly:

  1. Find the old cell GUID — Query formxml in the target environment and look for the <cell> containing your field. Note its id attribute.

  2. Update Dev’s form XML — Use the Dataverse API to PATCH the systemform record in Dev, replacing the cell GUID with the one from the old solution:

PATCH /api/data/v9.2/systemforms({form-guid})
{ "formxml": "<updated XML with old cell GUID>" }
  1. Publish, re-export, reimport — Now your managed solution carries the field in a cell with the same GUID as the old solution. The merge sees one cell, two layers — your visible = false wins.

You can verify the fix by inspecting the exported customizations.xml — look for your field’s <cell> and confirm the id matches the old solution’s cell GUID.

Option 3: Update the Old Solution (Proper, Risky)

Re-export the old solution (Solution A) from Dev — where the field has already been removed from the form — as a new managed version, and reimport it into target environments.

ProCon
Fixes the root causeOld solution may have hundreds of components
Clean solution layersRisk of unintended changes if Dev has drifted
No hidden fieldsRequires access to original solution source

Option 4: Migrate and Uninstall (Long-term)

Migrate all components from the old solution into your new solution, then uninstall the old one.

ProCon
Eliminates the problem permanentlyMajor undertaking (hundreds of components)
Reduces solution debtRisk of breaking dependencies
Clean environmentNeeds thorough testing

Prevention: What You Can Do Today

1. Minimize Form Overlap

Keep forms in as few solutions as possible. If CorePlatform owns the Account form, don’t also add it to SecurityRoles, FeatureRelease, and HotfixBundle. Every solution that touches a form creates a merge layer.

2. Use a Single “Form Owner” Solution

Designate one solution as the owner of each major form. Other solutions can add columns to the table but should not include the form. Only the owner solution modifies the form layout.

3. Document Solution Component Ownership

Maintain a matrix of which solution owns which forms, views, and dashboards. When onboarding new team members, this prevents accidental duplication.

4. Plan for Field Replacement

When you know you’re replacing a field (text → lookup, single field → composite), plan the form change to include the hide pattern (Option 2) from the start. Don’t assume removal will propagate.

5. Audit Before Import

Before importing a managed solution that modifies forms, check which other managed solutions in the target environment touch the same forms:

GET /api/data/v9.2/solutioncomponents
  ?$filter=objectid eq {form-guid} and componenttype eq 60
  &$expand=solutionid($select=uniquename,friendlyname,version)

If multiple solutions own the form, expect merge behavior.

6. Mark Deprecated Columns Clearly

When you hide a deprecated field on a form, also rename its display name to something unmistakable:

crx_vendorgroupid  →  "ZZZ - Do NOT Use - Vendor Group ID"

The ZZZ prefix pushes it to the bottom of any alphabetical column picker, and the label makes it obvious to anyone browsing the form designer or column list that this field is dead. If the hide ever fails or someone accidentally re-adds it, the label screams “don’t touch.” It’s a cheap safety net that costs nothing and saves future confusion.

The Diagnostic Flow

flowchart TD
    A["Field still visible after<br/>managed solution import"] --> B{"Check formxml vs<br/>formxmlmanaged"}
    B -->|"Field in formxml<br/>but NOT in formxmlmanaged"| C["Another solution layer<br/>is contributing it"]
    B -->|"Field in BOTH"| D["Your solution still<br/>has the field — re-export"]
    B -->|"Field in NEITHER"| E["Clear browser cache<br/>and publish all"]
    C --> F{"Query solutioncomponents<br/>for the form"}
    F --> G["Identify which managed<br/>solutions contain the form"]
    G --> H{"How many solutions<br/>own this form?"}
    H -->|"Multiple"| I["Hide field in your solution<br/>with visible = false"]
    I --> L{"Still visible<br/>after reimport?"}
    L -->|"No"| M["Done ✓"]
    L -->|"Yes"| N["Cell GUID mismatch!<br/>Your cell has a different ID<br/>than the old solution's cell"]
    N --> O["Find old cell GUID in<br/>target environment's formxml"]
    O --> P["PATCH Dev form XML<br/>to use the old cell GUID"]
    P --> Q["Re-export managed<br/>and reimport"]
    Q --> M
    H -->|"Only yours"| K["Check for Active<br/>customization layer"]

    style A fill:#4a1942,stroke:#cf6679,stroke-width:3px,color:#f5f5f5
    style B fill:#263238,stroke:#90a4ae,stroke-width:2px,color:#f5f5f5
    style C fill:#2d1b3d,stroke:#cf6679,stroke-width:2px,color:#f5f5f5
    style D fill:#1b3a4b,stroke:#4fc3f7,stroke-width:2px,color:#f5f5f5
    style E fill:#1b3a4b,stroke:#4fc3f7,stroke-width:2px,color:#f5f5f5
    style F fill:#263238,stroke:#90a4ae,stroke-width:2px,color:#f5f5f5
    style G fill:#263238,stroke:#90a4ae,stroke-width:2px,color:#f5f5f5
    style H fill:#263238,stroke:#90a4ae,stroke-width:2px,color:#f5f5f5
    style I fill:#1b3a4b,stroke:#4fc3f7,stroke-width:2px,color:#f5f5f5
    style K fill:#1b3a4b,stroke:#e94560,stroke-width:2px,color:#f5f5f5
    style L fill:#263238,stroke:#90a4ae,stroke-width:2px,color:#f5f5f5
    style M fill:#1a3a2a,stroke:#81c784,stroke-width:3px,color:#f5f5f5
    style N fill:#4a1942,stroke:#e94560,stroke-width:3px,color:#f5f5f5
    style O fill:#2d1b3d,stroke:#cf6679,stroke-width:2px,color:#f5f5f5
    style P fill:#1b3a4b,stroke:#4fc3f7,stroke-width:2px,color:#f5f5f5
    style Q fill:#1b3a4b,stroke:#4fc3f7,stroke-width:2px,color:#f5f5f5

The Bigger Picture

This isn’t a bug — it’s a consequence of Dataverse’s design philosophy: managed solutions are additive, not destructive. Microsoft designed it this way to prevent one solution from accidentally breaking another. But the side effect is that removing things from managed solutions is fundamentally harder than adding them.

The longer a Dataverse project runs and the more solutions accumulate, the more this matters. It’s the kind of issue that doesn’t surface in greenfield deployments but becomes a recurring challenge in mature environments with multiple solution publishers, multiple release trains, and years of accumulated customizations.

Understanding the merge behavior — and planning for it — is the difference between a smooth deployment and a puzzling “I removed it, why is it still there?” investigation.


If you’ve hit this wall before, I’d love to hear how you solved it. Drop a comment or DM.