Common Dynamics 365 Customization Mistakes and How to Fix Them
A troubleshooting guide to the customization anti-patterns that quietly break Dynamics 365 projects, and the practical steps to refactor and stabilize the platform.
Many failed Dynamics 365 projects stem from poor customization practices. This troubleshooting guide lists common errors (over-customizing OOB features, poor plugin design, ignoring solution layering) and provides clear, actionable steps to refactor and stabilize the platform, improving performance and maintainability. The fixes follow one principle: configure first, put each piece of logic in the lightest tool that can own it, and ship every change through managed-solution ALM. Solzet is a Microsoft Dynamics 365 Customer Engagement and Power Platform consultancy headquartered in Yerevan, Armenia. Project rescue is core to what we do, so the mistakes and fixes below are the same ones we work through on real client environments.
If the specific problem you are here for is legacy client script, go straight to deprecated APIs and Xrm.Page migration, which covers how to audit an environment for deprecated Client API usage, the compatibility shims that stabilize it in the first week, and a phased remediation plan that fixes the non-UI callers first instead of forcing a rewrite. If the app has just broken after a Microsoft update, start with when every release wave breaks something, which covers keeping the business running, finding the failing layer and the release gate that makes the next wave a non-event. If every solution is unmanaged and each change breaks something else, go to solution layering in an all-unmanaged environment, which explains the cascade and the salvage path.
What are the most common Dynamics 365 customization mistakes?
Over-customizing out-of-the-box features
Rebuilding what the platform already gives you is the most expensive mistake. Reworking standard sales or service tables, hiding half the ribbon, layering scripts over native behaviour, or replacing built-in features with bespoke ones adds maintenance forever and fights every release wave. Most of the time a business rule, a view, or a light form change achieves the same outcome and survives upgrades untouched.
Poor plugin design
Plugins registered on the wrong message or stage, synchronous plugins doing slow work in the transaction, missing depth guards that cause infinite loops, and business logic buried in code that a maker could have done with a business rule or flow. Heavy synchronous plugins are a leading cause of form-save timeouts and platform slowness, and undocumented plugin assemblies make every future change risky.
Ignoring solution layering
Customizing directly in production, editing the default solution, or stacking unmanaged changes on top of managed ones creates an active layer that quietly overrides the managed layer beneath it. The result is behaviour that no solution explains, changes that will not import cleanly to the next environment, and components nobody can safely remove. Weak publisher and prefix discipline compounds it.
Too much JavaScript and unsupported form code
Form scripts that manipulate the DOM directly, rely on undocumented objects, or run heavy logic on every field change slow the form down and break without warning on updates. Only the supported Client API is safe across release waves. Client-side code that enforces data rules the server does not is also easy to bypass, so validation ends up living in the wrong place.
No environment strategy or ALM
When everyone builds in a single environment and ships straight to production, there is no clean dev-to-test-to-production path, no source control, and no way to roll a change back. Customizations drift, testing happens on live data, and a bad import takes the business down. The lack of managed-solution application lifecycle management is usually the root cause behind every other mistake on this list.
How do you refactor and stabilize a Dynamics 365 environment, step by step?
Work through these in order. The early steps map the damage and fix the foundations; the later steps move logic to the right place and lock in a safe deployment path.
Inventory the customizations and read the solution layers
Start with the solution layers on your key tables and forms. In the maker portal, use "See solution layers" on a component to see exactly what is managed, what is unmanaged, and what active layer is overriding the rest. Export a list of solutions, plugin assemblies, workflows, flows, and JavaScript web resources so you know the full surface area before changing anything.
Fix the publisher and solution structure
Adopt a single custom publisher with a proper prefix and stop customizing the default solution. Group your work into focused, well-named solutions and move to managed solutions for anything you deploy downstream. This removes the accidental active layers and gives every change a clean home that imports predictably to the next environment.
Remove customizations the platform already covers
Walk the over-customized areas and ask whether a standard feature, a business rule, or a simple view or form change does the same job. Retire bespoke code that duplicates native behaviour. Each thing you delete is one less component to maintain and one less item that can break on the next Microsoft release wave.
Refactor the plugins
Review every plugin for the right message, stage, and mode. Move slow or non-critical work to asynchronous execution, add depth checks to stop recursion, and push simple field logic down to business rules or Power Automate. Keep synchronous plugins fast and lean so form saves stop timing out, and document what each assembly does and why it fires.
Clean up client-side scripts
Replace direct DOM manipulation and undocumented calls with the supported Client API. Trim heavy on-change logic, and move any rule that protects data integrity to the server side with a plugin or a real-time flow so it cannot be bypassed. Lighter, supported form code is faster for users and safe across updates.
Put the logic in the right tool
Match each piece of logic to the lightest tool that can own it: business rules for simple field behaviour, Power Automate for orchestration and integration, and plugins only for synchronous, transactional server logic. Getting this split right is what makes the platform both faster and easier to maintain, because no single layer is carrying work it was never meant to.
Stand up environments and validate before promoting
Establish a dev-to-test-to-production path with managed-solution ALM, ideally through a pipeline so imports are repeatable and reversible. Reproduce the refactored behaviour in a sandbox, run through the affected business processes, and only then promote. From that point on, no one customizes production directly, and every change has an audit trail.
How do you migrate off deprecated APIs and Xrm.Page without a rewrite?
Xrm.Page has been deprecated since Dynamics 365 version 9.0. It still resolves in model driven apps for backward compatibility, which is exactly why it survives in production for years: nothing fails, so nothing gets fixed, until a release wave or a security review turns a quiet dependency into an incident. The same is true of the rest of the deprecated Client API surface, including Xrm.Utility.alertDialog, Xrm.Utility.confirmDialog, Xrm.Utility.openEntityForm, Xrm.Utility.openWebResource, Xrm.Utility.openQuickCreate, Xrm.Page.context, getServerUrl, and the 2011 SOAP and OData endpoints that older integrations still call. Most guidance on this topic tells you the replacement API and stops there. Replacement is the easy part. The work is finding every call site, keeping the system running while you change it, and sequencing the change so you are not rewriting a working solution.
1. Audit the environment for deprecated API usage
Export and unpack the solutions, then search the source
The maker portal will not show you what is inside a JavaScript web resource, so stop looking there. Export every solution, including the default solution, and unpack it with the Power Platform CLI so you have the web resources, the customizations file, and the ribbon definitions on disk as plain text. Search that tree for the deprecated tokens. This is the only pass that is genuinely exhaustive, and it gives you file paths you can put in a ticket.
Run Solution Checker over every solution
Solution Checker, either from the maker portal or through pac solution check, reads the same web resources and rates what it finds. Its web resource rules cover this class of problem directly: calls to the deprecated 2011 service endpoints, reaching into window.top or the parent frame, direct DOM access against the form, and calls that have a supported Navigation API replacement. Use it for severity and for the report, not for coverage: it sees what is inside solutions, so anything outside them is still on you.
Check which handlers actually pass the execution context
A function can be clean and still be stuck on Xrm.Page because the handler that calls it never passes the execution context. For every form library, list each event handler and whether the "Pass execution context as first parameter" option is set. Command bar buttons are the same problem in a different place: a command that does not pass PrimaryControl leaves its script with no form context to use, and the maker portal will not surface that. Read the ribbon definitions from the unpacked solution.
Audit the non-UI callers as well
Deprecated usage is not only on forms. Plugins and custom workflow activities built against old SDK assemblies, console applications, scheduled jobs, and middleware that still talks to the 2011 SOAP or OData endpoints or authenticates with legacy WS-Trust instead of OAuth all belong in the same inventory. These are the ones that fail hard and silently when an endpoint is retired, and they are the ones a security review asks about first.
Record blast radius, not just file names
Turn the raw hits into one row per call site: the file, the form or command that fires it, the business process behind it, whether a supported replacement exists directly or the logic needs redesigning, and rough effort. Mark each row keep, shim, migrate, or rebuild. A list of grep hits is not actionable; this inventory is, and it is what the phased plan below is sequenced from.
# Unpack every exported solution to plain text first.
pac solution unpack --zipfile ./MySolution.zip --folder ./src --packagetype Both
# Then search the unpacked tree for the deprecated surface.
grep -rnE 'Xrm\.Page|parent\.Xrm|window\.top|crmForm' ./src
grep -rnE 'Xrm\.Utility\.(alertDialog|confirmDialog|openEntityForm|openWebResource|openQuickCreate)' ./src
grep -rnE 'XRMServices/2011|OrganizationData\.svc|ClientGlobalContext\.js\.aspx' ./src
grep -rnE 'getServerUrl|document\.getElementById|getElementsByTagName' ./src
# Rate what the checker finds, and keep the report as audit evidence.
pac solution check --path ./MySolution.zip --outputDirectory ./checker-reportIf you would rather have this done as an independent, evidence backed pass with a written report at the end, it is exactly what our Dynamics 365 health check and technical audit produces: a fixed scope assessment of configuration, security, performance, and ALM, with every finding rated by impact and effort and handed over as a prioritized action plan you can run with any partner or your own team.
2. Immediate compatibility shims to stabilize the system
Before any migration work starts, make the environment safe to change. The goal of this step is not to fix the deprecated calls, it is to remove the dependency on a global that can disappear, without touching business logic and without a regression cycle.
Add one context resolver and change one line per function
Ship a single small library, loaded before every other form script, that returns a form context from the execution context when it is available and falls back to the deprecated global when it is not. Every existing function then changes by exactly one line at the top and keeps the rest of its logic untouched. Nothing is rewritten, nothing is retested beyond a smoke pass, and the moment a handler is switched to pass the execution context that function is genuinely migrated with no further code change.
Wrap the deprecated dialog and navigation calls, do not patch Xrm
Put thin wrappers around the supported Xrm.Navigation replacements and do a find and replace on the deprecated calls. This is safe, mechanical, and reversible. What you must not do is reassign or monkey patch the Xrm namespace itself to keep old code alive: that is unsupported, it is invisible to whoever debugs it next, and it is the thing most likely to break on the next release wave. Shims live in your own namespace, never inside the Microsoft one.
Freeze the debt on the way in
Stabilization only holds if the pile stops growing. Add the same searches you ran during the audit to the build as a failing check, so a pull request that introduces a deprecated call cannot merge, and make "handlers pass the execution context" part of the definition of done. Freezing first is what turns an open ended cleanup into a finite one.
Give every shim an owner and an expiry
A shim is a stated debt, not a solution. Register each one with the phase that removes it and the person who signs it off, and keep the fallback branch visible in code review rather than buried. Shims that nobody has scheduled to delete are how the original problem was created.
// solzet_compat.js, loaded before every other form library.
var Solzet = window.Solzet || (window.Solzet = {});
// Returns the supported form context, falling back only where a handler
// has not yet been switched to pass the execution context.
Solzet.getFormContext = function (executionContext) {
if (executionContext && typeof executionContext.getFormContext === 'function') {
return executionContext.getFormContext();
}
return typeof Xrm !== 'undefined' && Xrm.Page ? Xrm.Page : null;
};
// Supported replacements for the deprecated Xrm.Utility helpers.
Solzet.alert = function (text) {
return Xrm.Navigation.openAlertDialog({ text: text });
};
Solzet.openForm = function (entityName, entityId) {
return Xrm.Navigation.openForm({ entityName: entityName, entityId: entityId });
};
// Every existing function now changes by one line and keeps its logic.
function onAccountLoad(executionContext) {
var formContext = Solzet.getFormContext(executionContext);
// ... unchanged body, formContext instead of Xrm.Page ...
}3. A phased remediation plan that avoids a full rewrite
The sequencing is the whole point. Every phase below is ordered by risk removed per unit of disruption, which puts the invisible non-UI work first and the expensive user interface work last, where it stays small.
Phase 0: freeze and shim, in the first week
Land the context resolver, the navigation wrappers, and the build check. Nothing user visible changes and no business logic moves, so this can go through a normal release with a smoke test. At the end of week one the environment is stable, new deprecated usage is blocked at the gate, and the inventory from the audit is the backlog. If a release wave is already imminent, this is also the phase that buys you the room to plan rather than react.
Phase 1: non-UI callers first, because they fail hardest
Move plugins, custom workflow activities, integrations, and scheduled jobs off the 2011 SOAP and OData endpoints and onto the Web API, and off legacy WS-Trust authentication and onto OAuth with a service principal. These have no user interface, so they need no retraining and no user acceptance testing, they can be verified with integration tests against a sandbox, and they carry the real retirement and security risk. Fixing them first buys the largest risk reduction for the least disruption, which is the opposite of how most teams sequence this work.
Phase 2: shared script libraries, one deployment
Convert the common libraries that every form pulls in, keeping their public function signatures exactly as they are so no form or command definition has to change. Replace Xrm.Page.context with Xrm.Utility.getGlobalContext, getServerUrl with getClientUrl, and any remaining 2011 endpoint calls in client script with the Web API. This is a single deployment with no visible change, and it typically clears a large share of the inventory in one pass.
Phase 3: form by form and command by command, in business order
Now switch handlers to pass the execution context, ordered by traffic and revenue risk rather than by file size. Do the highest use forms and the command bar buttons behind them first. As each form lands, delete the fallback branch for the functions it owns, so the shim shrinks with every release instead of becoming permanent. Each form gets a regression pass against the process it supports, which is affordable precisely because phases 1 and 2 already removed everything that did not need one.
Phase 4: rebuild only what cannot be made supported
A small residue will not migrate, because it never used the supported API in the first place: HTML web resources that manipulate the form DOM, iframes that scrape fields out of the page, and controls built on frameworks the platform no longer hosts. That, and only that, is the rebuild bucket, and the supported destination for it is a PowerApps Component Framework control. Keeping this phase last and small is what separates a migration from a rewrite.
Phase 5: remove the shims and prove the result
Delete the fallback branches and the compatibility register, re-run Solution Checker across every solution, and keep both the before and the after report. Leave the build gate in place permanently. The final state is a supported, checker clean customization layer, with documented evidence of what was found, what was changed, and when, which is what the next audit is going to ask for.
The one place a rebuild is genuinely the right answer is phase 4, and the supported destination for it is a PowerApps Component Framework control. PCF is the Microsoft recommended successor to legacy HTML web resources, so an iframe or a script that reaches into the form DOM becomes a supported component with a proper lifecycle instead of a permanent exception in your audit report.
4. Running this as a fixed scope engagement that passes an audit
A security or compliance reviewer almost never demands zero deprecated calls. What they ask for is control: an inventory of unsupported components, a named owner, a dated remediation plan, and evidence that it is being worked. Time and materials cleanup, run in the gaps between other work, produces none of those artifacts, which is why this debt survives audit after audit. A fixed scope, vendor led engagement produces all of them as a by-product of doing the work, because each phase has a deliverable attached to it.
That is how Solzet scopes it. The audit pass above is the health check, and it delivers the written inventory and the Solution Checker report that the reviewer wants to see. Where the deprecated API problem is one symptom of a project that has already stalled or been handed back by another partner, it runs inside our Dynamics 365 project rescue and takeover service, where the first six weeks are a fixed scope reset with dated milestones: an environment audit report and technical debt inventory inside two weeks, then a fixed price and a fixed end date agreed in writing before any build work starts. The deprecated API rows sit in that same technical debt inventory, marked keep, shim, migrate, or rebuild, so the migration is scoped and priced against evidence rather than estimated against a guess.
The evidence pack at the end is what closes the audit finding: the before and after Solution Checker reports, the inventory with every row resolved or explicitly accepted, the build gate that stops the debt coming back, and a note of which shims were removed in which release. That is deliberately more than a list of replacement APIs, because a list of replacement APIs is not something you can put in front of an auditor.
What do you do when every Dynamics 365 update breaks something?
A Dynamics 365 build becomes update-fragile for a small number of reasons: form scripts that manipulate the page DOM, deprecated client APIs such as Xrm.Page, unmanaged layers sitting on top of managed solutions, PCF controls built against old framework and tooling versions, and plugins nobody has tests for. Microsoft changes the platform underneath all of them in the twice-yearly release waves and the service updates between them. The triage order is always the same: keep the business capturing data, find the failing layer, disable rather than delete, run Solution Checker, catalogue the unsupported patterns by blast radius, rewrite the highest risk ones onto supported APIs, then put a release gate in place so the next update is a non-event.
1. Keep the business running while the app is down
Check it is your build before you touch it
Look at service health and the Message Center in the Microsoft 365 admin center and at the Power Platform admin center first. If Microsoft has an active incident on your region, changing your customizations will only add a second problem. If it is only your environment, and only some forms or processes, the cause is almost always in your own layers.
Give people a structured place to capture work
If salespeople cannot log activities or agents cannot update cases, do not let the work go into personal notes, chat messages and email. Stand up one temporary capture point with fixed columns that match the target table, for example a Microsoft Form or a SharePoint list with the account, contact, activity type, date, owner and notes. Structured capture can be imported back with the standard import tools once the app works, so nothing is retyped and nothing is lost.
Look for a route that still works
A broken form script often breaks one form, not the table. A quick create form, an editable grid on a view, or the Dynamics 365 App for Outlook may still save the same record correctly. Test the alternative route yourself before you tell users about it, because a route that saves bad data is worse than the temporary list.
Stop anything that is sending wrong data downstream
When a custom module feeds payroll, finance or billing exports, the emergency is not the error on screen, it is the wrong file that goes out tonight. Pause the outbound flow, job or export and tell the receiving team before the next run. A delayed export is recoverable; a wrong payroll or invoice run is not. Solzet fixes the Customer Engagement and Power Platform side of that feed, and where the receiving system is Business Central or Finance and Operations, the same team can check the ERP side of the run.
What not to do while it is down
Do not restore an environment backup, because it discards every record entered since the backup was taken. Do not uninstall or delete a managed solution to get rid of the broken part, because removing a managed solution also removes the tables and data it owns. Do not reimport an old solution file kept on a laptop over the top, and do not start fixing things with unmanaged edits straight in production unless each one is written down, because every one of them becomes a new active layer that the next update will trip over.
2. Find the failing layer from solution history and the logs
Start from what changed, in time order
Put three timelines side by side: the date the update reached the environment, the solution history for the environment (every import, upgrade and uninstall, with its status and time), and the date users first reported the problem. If a solution import happened in the same window as the update, you have two suspects, not one, and the solution history tells you which came first.
Read the logs that belong to each layer
Form script errors show up in the browser developer console and in the Power Apps Monitor session for the model driven app. Plugin failures show up in the plug-in trace log, once trace logging is switched on for exceptions, and asynchronous failures in the system jobs list. Flow failures sit in the run history of each cloud flow. Each of these points at a specific component, which is what turns "Dynamics is broken" into a ticket someone can fix.
Check the solution layers on the failing component
Once you have a component, open its solution layers. An unmanaged active layer on top of a managed solution explains a lot of "it worked in test" failures: production is not running what test is running. A PCF control that fails to render after an update is usually a control built with an old version of the framework tooling, and the fix is a rebuild and redeploy rather than a patch.
3. Disable, do not delete
The fastest way to turn an outage into a data loss is to delete the thing that broke. Switch it off in a way that can be switched back on.
- Plugins: disable the failing step in the Plugin Registration Tool rather than unregistering the assembly, so the registration, filtering attributes and images are still there when the fix goes back in.
- Cloud flows and classic workflows: turn them off or deactivate them; do not delete them, because run history and connection references go with them.
- Business rules: deactivate the rule, which keeps its definition intact.
- Form scripts: ship a small managed patch that removes the one failing handler, or guard the handler with a switch your team controls, such as an environment variable read at load, and keep the web resource itself.
- Write every change on one page: what was disabled, when, by whom, and what business behaviour is now missing, so the missing behaviour is covered manually until it is restored.
4. Stabilize in this order once the business is working again
Run Solution Checker over every solution
Run it over every solution in the environment, not only the one that broke, and keep the dated report. It is the severity source for web resources, plugins and custom workflow activities, and the before report you will compare against later.
Catalogue the unsupported patterns by blast radius
One row per pattern: DOM manipulation, deprecated client API, unmanaged layer over managed, outdated PCF control, untested plugin, and the external callers the checker cannot see. Rank each row by the business process it can take down on the next update, not by the number of warnings.
Rewrite the highest risk rows onto supported APIs
Fix the rows that can stop revenue, service or payroll first, using the supported Client API, the Web API and current PCF tooling, and release each fix as a managed solution. The lower rows can often be replaced by configuration or deleted rather than rewritten.
Clear the active layers in production
Move every emergency edit into source, redeploy it as managed, then remove the unmanaged active customizations from production so production runs exactly what was tested.
The full version of this, with a deprecation map, every finding classified as must-fix, replace with configuration or delete, remediation in reversible slices and an estimate per slice, is our Dynamics 365 upgrade remediation service for unsupported customizations. If the update is only the latest symptom of a project that has stalled, lost its partner or lost the confidence of its users, start with the Dynamics 365 project rescue and takeover service instead.
How do you make the next release wave a non-event?
With a pre-release gate: every wave runs against a copy of your build before it runs against production, and every deployment is checked before it can add new fragility. None of this needs new tooling beyond what the platform provides, only a sandbox, a short list of processes that must not break and a pipeline.
Keep a sandbox that is a recent copy of production
Test on your real customizations and realistic data, not on a clean trial. Refresh the copy before each wave so it contains everything that went live since the last one.
Turn on early access updates in that sandbox
Release waves have an early access period before they are applied to everyone. Enable the early access updates on the sandbox as soon as the window opens, and read the release plans for the apps you use alongside it, so you see the next wave running against your build weeks before production gets it.
Run a scripted regression pass on the processes that matter
Write down the handful of processes that must not break, such as logging an activity, saving a case, booking a job and producing the export finance relies on, and run them on the early access sandbox. Automate the ones you run every wave, with unit tests for plugins and UI tests for the forms, so the pass is repeatable rather than a heroic effort.
Gate every deployment on Solution Checker
Make the checker part of the build so a solution that adds a new unsupported pattern cannot reach test or production. The pipeline mechanics are on our guide to PAC CLI and pipelines.
Sign off the wave before it lands
A named owner confirms the regression pass is green, or that the fixes for anything red are already released, before the wave reaches production. With that in place the update date is a calendar entry, not an incident.
The gate depends on being able to build and release from source. If your solutions still move between environments as manually exported ZIP files, put that right first, using our guide to moving from manual ZIP files to PAC CLI and pipelines, which also covers gating the build on Solution Checker and rehearsing a rollback.
What if the update is already on the calendar and you cannot finish in time?
Then aim to be ready rather than finished. Turn on early access in a sandbox copy of production today, so the list of what breaks is evidence rather than fear. Fix the rows that can stop revenue, service or payroll, and for everything else prepare the continuity kit above in advance: the disable switch for each fragile component, the structured capture point, the people who decide on the day, and a message ready for users. Senior consultants and full-stack developers can be added to the remediation to cover the gap, and whatever is not fixed before the wave goes into the stabilization order straight after it, with a date against each row.
Why does every change break something else in an all-unmanaged environment?
If every solution in the environment is unmanaged, several of them contain the same tables and forms, and each import fixes one thing while breaking another, the cause is the layering model rather than bad luck. Unmanaged changes do not live inside the solution you think you are editing. They all land in one shared active layer on top of everything else, the last one written wins, and every solution that references a component carries its latest state with it. The salvage path is to inventory which solution should own each component, split the work into properly scoped solutions with distinct publishers, build a managed artefact from a clean environment, and rehearse the switch on a copy of production before doing it for real.
1. Managed and unmanaged layering in plain terms
Managed layers stack in install order
At the bottom sits the system layer that ships with the platform. Each managed solution you install adds a layer above it, in the order the solutions were installed, and for most component types the highest managed layer that touches a component decides how it behaves. Forms and a few other component types merge changes from several layers rather than simply taking the top one, which is why a form can look like a mixture of two solutions.
There is one unmanaged layer, and it sits on top
Unmanaged solutions do not get a layer each. Every unmanaged change in the environment, whichever solution it was made in, lands in a single active layer above all the managed ones, and the active layer wins. Five unmanaged solutions are therefore not five separate pieces of work. They are five labelled views onto the same shared layer.
What users see is the top of the stack
The behaviour of a component is whatever the stack resolves to. The "See solution layers" view on a component in the maker portal shows the stack in order, which layer is active, and which properties each layer changed. It is the single most useful screen for this problem and most people who inherit an environment have never opened it.
2. What an unmanaged change on top of a managed component does
- An admin edits a component that belongs to a managed solution, where its managed properties allow customization, for example a form or a view. The edit is written to the active unmanaged layer above the managed layer.
- The vendor or your own team ships a new version of that managed solution. The import succeeds and the managed layer underneath is updated, but for every property the active layer overrides, users still see the old unmanaged version. This is the classic "the import worked and nothing changed" symptom.
- The fix is "Remove active customizations" from the solution layers view of that component, which deletes the unmanaged layer so the managed layer shows through. It works per component, it cannot be undone, and it discards every unmanaged change on that component, so export and record what is there first and decide which parts need rebuilding properly in development.
- Until the active layer is removed, the managed solution is not really in control of that component, which is why an environment full of these layers cannot be upgraded predictably.
3. How components end up shared across five unmanaged solutions
A solution is only a container
A solution holds references to components. The component itself exists once in the environment. Adding the Account table to a second solution does not copy it; it adds a second reference to the same table.
One component can sit in many solutions
Nothing stops the same table, form, option set or web resource from being added to all five unmanaged solutions, and adding a table with all its subcomponents makes it very easy to do by accident. Once that happens, no solution owns the component and every solution depends on it.
Exporting any of them carries the latest state
Because there is one component and one active layer, exporting solution A includes whatever was last done to a shared component, including a half finished change somebody made for solution C. Import that export elsewhere and project C arrives early, unreviewed, inside the wrong release.
Unmanaged imports into the target overwrite each other
In the target environment every unmanaged import writes into that environment active layer too. Import A, then D, and D quietly reverts the form change A just delivered because D was exported before it. The order of imports becomes the real release process, and nobody wrote it down.
Removing a component from a solution does not delete it
Taking a component out of a solution removes the reference and leaves the component in the environment. Deleting the component itself removes it from every solution that referenced it. Teams that confuse the two either leave clutter behind or break four solutions while tidying one.
One default publisher hides who made what
When all five solutions use the same default publisher and prefix, nothing in a schema name tells you which piece of work a column came from. Ownership has to be reconstructed from the solutions and the history rather than read off the component.
4. The salvage path, in order
The order matters more than speed. Every step before the cutover can be undone, and the cutover itself is only run once it has been rehearsed on a copy of production.
Freeze direct edits and record the environment as found
Agree that nobody customizes production while the salvage runs, except through a written emergency route. Export every unmanaged solution, unpack each one with the Power Platform CLI and commit the result to source control as the as found record. Take a copy of production into a sandbox. Nothing below is safe without a copy to rehearse on and a record to compare against.
Build a component ownership inventory
From the unpacked exports, list the root components of each solution and put them in one matrix of component against solution. Flag every component that appears in more than one solution, then use the solution layers view on the important ones to see what is actually active. For each shared component, decide the one solution that should own it. This matrix, not the solution names, is the real map of the environment.
Design properly scoped solutions with distinct publishers
Group components along real boundaries: a core data model, one solution per functional area that ships on its own, and one for code components such as plugins and PCF controls, with dependencies running one way only. Give new work a proper publisher and prefix instead of the default. Existing components keep the schema names and prefix they were created with, so do not plan to rename them; plan for new components to carry the new prefix from now on.
Rebuild a clean development environment from source control
Create a fresh development environment, import the newly scoped solutions into it from source, and check it against the as found record until the differences are the ones you intended. From this point all changes are made there, never in production. Build managed artefacts from that source on a pipeline rather than exporting them by hand; the commands and the pipeline are set out in our PAC CLI guide.
Rehearse the transition on a copy of production
Converting an unmanaged environment to managed solutions is not a setting you switch. Import the managed artefacts, in dependency order, into the production copy, then open the solution layers on the key components to see whether the managed layer or an old unmanaged layer is winning. Where the managed version should win, remove the active customizations after recording them. Run the business processes that matter on the copy and time every step. How a managed import behaves over components that already exist unmanaged depends on the component types involved, so repeat the rehearsal until it is uneventful.
Cut over in small waves, then retire the old containers
Run the rehearsed sequence in production one solution scope at a time, outside peak hours, with a customization freeze rather than an outage for users: solution imports and layer removal do not take the application offline, although you should test each wave against your own processes before promising that. After each wave, check the layers and run the process checks. When every component is owned by a managed solution, delete the old unmanaged solution containers, which removes the containers and leaves their components in place.
The pac commands for the export, unpack and pack steps, the deployment settings and the build and release pipeline that produces the managed artefacts are all in our guide to Power Platform ALM with the PAC CLI and pipelines, which this salvage path hands over to once the solutions are properly scoped.
5. When salvage beats a rebuild, and when it does not
Salvage is usually right when
- The data model is broadly sound and already holds live data, integrations and reports that depend on it.
- The problem is packaging, meaning overlapping solutions, shared components, default prefixes and active layers, rather than design.
- The business processes work today and the complaint is that nobody can change them safely.
- Most of the customization is in use, so rebuilding would mean recreating the same components under new names and migrating the data into them.
A rebuild is usually right when
- The data model itself is wrong: duplicated tables for the same thing, relationships that force the wrong numbers, or custom tables that replace standard ones the business actually needs.
- A large share of the customization is unused, duplicated or unsupported code that would have to be rewritten anyway.
- The inventory shows so much conflicting active layer that reconciling it component by component costs more than rebuilding the subset still in use.
- You would be carrying the same mistakes into managed solutions, which only makes them harder to change later.
A rebuild here means rebuilding the solutions in a clean environment and migrating the data into them, not replacing the platform, and it is a larger project than a salvage because of that migration. If the inventory suggests the data model itself is the problem, our guide to remediating a Dataverse data model in production covers that decision. If you want the inventory done independently first, it is part of the Dynamics 365 health check, and where the environment is one symptom of a stalled project it runs inside our project rescue and takeover service.
What do teams ask about fixing Dynamics 365 customization mistakes?
What are the most common Dynamics 365 customization mistakes?
The recurring ones are over-customizing out-of-the-box features when a standard configuration would do, poor plugin design (wrong message or stage, slow synchronous code, missing depth guards), ignoring solution layering so unmanaged changes override the managed layer, too much unsupported JavaScript on forms, and having no environment strategy or application lifecycle management. Most of these trace back to customizing directly in production without managed solutions.
Why is over-customizing Dynamics 365 a problem?
Every bespoke customization is something you have to maintain forever and re-test on each Microsoft release wave. Reworking standard tables, hiding native features, or replacing built-in behaviour with custom code adds cost and fragility for little gain, because a business rule, view, or form change usually achieves the same outcome and survives upgrades untouched. The goal is to configure first and only write code where the platform genuinely cannot do the job.
How does poor plugin design slow down Dynamics 365?
Synchronous plugins run inside the save transaction, so any slow query, external call, or heavy loop directly delays the form save and can cause timeouts. Plugins registered on a shared message without tight filtering fire more often than needed, and missing depth guards can trigger recursion. The fix is to move non-critical work to asynchronous mode, keep synchronous plugins fast and narrowly filtered, add depth checks, and push simple logic down to business rules or flows.
What does ignoring solution layering do to a Dynamics 365 environment?
Customizing in production or editing the default solution creates an unmanaged active layer that silently overrides the managed layer beneath it. That produces behaviour no single solution explains, changes that will not import cleanly to the next environment, and components nobody can safely remove. Adopting a single publisher, focused solutions, and managed-solution ALM removes those accidental layers and makes deployments predictable.
Is Xrm.Page still supported in Dynamics 365?
No. Xrm.Page has been deprecated since Dynamics 365 version 9.0. It still resolves in model driven apps for backward compatibility, so existing scripts keep working, but it is unsupported and Microsoft can remove it. The supported replacement is the form context obtained from the execution context, using executionContext.getFormContext(), with the handler configured to pass the execution context as the first parameter. The related deprecations to clear at the same time are Xrm.Page.context, replaced by Xrm.Utility.getGlobalContext, getServerUrl, replaced by getClientUrl, and the Xrm.Utility dialog and navigation helpers, replaced by the Xrm.Navigation API.
How do I audit a Dynamics 365 environment for deprecated API usage?
Export every solution, including the default solution, unpack it with the Power Platform CLI so the web resources and ribbon definitions are plain text on disk, then search that tree for Xrm.Page, parent.Xrm, window.top, the deprecated Xrm.Utility helpers, the 2011 service endpoints, and direct DOM access. Run Solution Checker over the same solutions for severity and a report you can keep as evidence. Then cover the two things a search alone misses: event handlers and command bar buttons that never pass the execution context, and non-UI callers such as plugins, integrations, and scheduled jobs still using the 2011 endpoints or legacy WS-Trust authentication.
Do we have to rewrite our JavaScript to migrate off Xrm.Page?
In almost all cases, no. A single context resolver library that returns the form context from the execution context and falls back to the deprecated global lets every existing function change by one line and keep its logic, which stabilizes the environment in the first week. From there the remediation is phased: non-UI callers first because they carry the real retirement and security risk, then shared script libraries, then form by form in business order, deleting the fallback as each one lands. Only code that never used the supported API, such as HTML web resources manipulating the form DOM, needs rebuilding, and the supported destination for that is a PCF control.
Why do our Dynamics 365 customizations break on every update?
Because some of them depend on things Microsoft does not promise to keep the same. Form scripts that manipulate the page DOM, deprecated client APIs such as Xrm.Page, unmanaged layers on top of managed solutions, PCF controls built with old framework tooling and plugins with no tests all sit on assumptions that a release wave or service update can change. The fix is to run Solution Checker, catalogue those patterns by blast radius, rewrite the highest risk ones onto supported APIs, and gate every wave on an early access sandbox.
Dynamics 365 Sales stopped working after an update and salespeople cannot log activities. What do we do first?
Check Microsoft service health to rule out a platform incident, then give sellers one structured temporary capture point with the same columns as the activity table so the records can be imported later. Test whether another route such as a quick create form or the Dynamics 365 App for Outlook still saves correctly. Then find the failing component from the solution history, browser console, plug-in trace log and flow run history, and disable it rather than deleting it. Do not restore a backup or uninstall a managed solution, because both lose data.
The next release wave is on the calendar and we cannot fix everything in time. What now?
Turn on early access updates in a sandbox copy of production now, so you know what actually breaks instead of guessing. Fix the rows that can stop revenue, service or payroll first, and for the rest prepare a disable switch, a structured temporary capture point and a named person to make the call on the day. Make sure every solution is deployable from source, so a fix can be released in hours. The service page on Dynamics 365 upgrade remediation covers the full assessment and slice plan.
Why does every change break something else in an all-unmanaged Dynamics 365 environment?
Because all unmanaged changes share one active layer. Unmanaged solutions are containers that reference components, not separate layers, so when five solutions include the same table or form, a change made for any of them is part of all of them. Exporting one solution carries the latest state of every shared component, including unfinished work for another project, and in the target each unmanaged import overwrites the previous one, so the order of imports decides what users get. Unmanaged edits on top of managed components also mask later managed updates. The way out is a component ownership inventory, properly scoped solutions with distinct publishers, and managed artefacts built from a clean development environment.
Why did importing an updated managed solution not change anything users see?
The usual cause is an unmanaged active layer on the component. Someone customized the component directly in that environment, so their change sits in the active layer above the managed layer. The import updated the managed layer underneath, but for the properties the active layer overrides, users still see the unmanaged version. Open "See solution layers" on the component to confirm it. "Remove active customizations" deletes that unmanaged layer so the managed version shows through, but it cannot be undone, so record the unmanaged changes first and rebuild any you still need in development. Version numbers that were not incremented are the other common cause.
Can an unmanaged Dynamics 365 environment be moved to managed solutions without downtime?
Usually yes, with preparation, but it is not a switch. There is no setting that turns existing unmanaged customizations into managed ones. You rebuild properly scoped solutions in a clean development environment, produce managed artefacts from source, and rehearse importing them over a copy of production, checking the solution layers on key components and removing active customizations where the managed version should win. Solution imports do not normally take the app offline, so the production cutover can run in small waves outside peak hours with a customization freeze. How the import behaves over existing unmanaged components depends on the component types, which is why the rehearsal is not optional.
Can Solzet refactor and stabilize a messy Dynamics 365 customization?
Yes. Solzet is a Dynamics 365 Customer Engagement and Power Platform consultancy based in Yerevan, Armenia, and project rescue is core to our work. We audit the solution layers, plugins, scripts, and environments, then refactor toward supported patterns and managed-solution ALM, validating every change in a sandbox before it reaches production. We deliver directly or on a B2B and white-label basis for other Microsoft partners.
We recommend the right solution: Dynamics 365 (Customer Engagement, Business Central or Finance and Operations), Power Platform, or a custom-built CRM. One team for CRM, ERP and the integration between them.
Have your Dynamics 365 customizations gone off the rails?
Solzet runs exactly these audits and refactors as part of our Dynamics 365 and Power Platform project rescue work. Tell us what your environment is doing and we will map the damage and stabilize it, directly or on a white-label basis for your team.