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. 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 rather than a generic checklist.
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.
The most common 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.
Step-by-step: refactor and stabilize the platform
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.
Deprecated APIs and Xrm.Page Migration
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.
Frequently Asked Questions
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.
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.
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.