PL-400 common mistakes
PL-400 · Understand the traps and how to avoid them.
Embedding environment-specific URLs, tenant IDs, or Dataverse record GUIDs directly in canvas apps, flows, plug-ins, or custom connectors and then promoting the same solution to test or production.
Candidates often build in a single development environment and assume the solution is portable; they treat environment values as constants rather than configuration.
What to do instead
Use environment variables for URLs and IDs, connection references for connectors, and secure/configuration data for plug-ins; never hardcode tenant-specific values in solution components.
Storing a related record's GUID in a text column instead of creating a Lookup column, then trying to use relationship navigation, views, or cascading behavior.
It feels simpler to store an ID string, especially during data import or API-first design, but Dataverse metadata does not know the text value represents a relationship.
What to do instead
Model the relationship with a Lookup, customer, or owner column and configure relationship behavior; use alternate keys for data loads instead of text GUIDs.
Relying on a business rule alone to enforce a required field or valid combination when records can be created through the Web API, import, or plug-ins.
Business rules appear in the form designer and seem like validation, but their scope and execution may be client-side or may not cover every entry point.
What to do instead
Enforce critical data integrity server-side with a synchronous pre-operation plug-in or required column plus plug-in validation; use business rules primarily for user experience.
Granting System Administrator or another broad built-in role to a user or application user to fix a missing privilege instead of identifying the exact privilege required.
Security troubleshooting is slow, and broad roles make the immediate error disappear, so candidates stop at 'it works' rather than applying least privilege.
What to do instead
Trace the failing operation, identify required create/read/write/append/append-to privileges on the relevant tables, and assign a custom role, team-based access, or application user with only the required privileges.
Using a non-delegable canvas app expression such as Search() inside Filter(), or a complex expression across a large Dataverse or SQL table, and assuming the gallery shows all matching records.
It works with small test datasets because Power Apps silently processes only the delegation limit locally; candidates do not test with more rows than the limit.
What to do instead
Use delegable predicates and functions for the data source, pre-filter with delegable conditions, keep small sets in collections, or move querying server-side; verify behavior with more than 500 or 2,000 rows.
Writing model-driven form scripts against the global Xrm.Page object and not returning or handling the promise from an asynchronous OnLoad handler.
Older tutorials and snippets still show Xrm.Page, and async handlers appear to work until form save or navigation races occur.
What to do instead
Use executionContext.getFormContext(), register the handler with 'Pass execution context as first parameter', return the promise from asynchronous work, and avoid deprecated Xrm.Page.
In a PCF control, manipulating the DOM in init() or changing a bound output value without calling notifyOutputChanged().
The component lifecycle differs from a normal page script: init runs before the container is reliably available, and the host only learns about changes through the notification method.
What to do instead
Render in updateView(), use init only for setup, call notifyOutputChanged() whenever a bound output changes, and implement destroy() to clean up listeners and timers.
Registering a plug-in step on Update without filtering attributes and without checking the target or images, causing it to fire on every update and potentially recurse.
The registration form makes 'all attributes' the easy default, and the plug-in may update the same record, creating a loop or severe performance issue.
What to do instead
Set filtering attributes to only the columns the logic depends on, configure pre-images or post-images deliberately, guard against recursion with depth or a flag column, and update only when values actually change.
Putting a long-running external HTTP call, large file operation, or heavy loop in a synchronous plug-in registered on Create or Update.
Synchronous plug-ins feel like normal code and are easy to test in isolation, but they run inside the Dataverse sandbox transaction and are subject to timeouts and isolation limits.
What to do instead
Keep synchronous plug-ins short and deterministic; move outbound calls and heavy work to an asynchronous plug-in, Power Automate, Azure Function, or queue, and use ITracingService for diagnostics.
Calling the Dataverse Web API from JavaScript or an integration and consuming only the first page of results, ignoring @odata.nextLink, or not using $batch for multi-record operations.
Small development datasets return all rows in one response; the paging token only appears when the page size is exceeded, so the bug stays invisible until production volume.
What to do instead
Follow @odata.nextLink until it is absent, set Prefer: odata.maxpagesize deliberately, use $batch or appropriate bulk patterns for multi-record operations, and handle 429 responses with Retry-After.