PL-200 common mistakes
PL-200 · Understand the traps and how to avoid them.
Using a business rule with an 'Entity' or 'All Forms' scope to guarantee that a field is always populated, then discovering that records created through an integration or a data import bypass the check.
Business rules are primarily a form-layer validation experience. Candidates treat them as equivalent to a required column at the table level and do not test the requirement through a non-UI channel.
What to do instead
Row-level enforcement belongs on the column: mark it Business Required on the table, and add a plug-in (or a synchronous validation step) when the rule must also hold for API, import, and bulk-edit scenarios. Reserve business rules for show/hide, lock/unlock, and cross-column UI logic.
Building a calculated column to sum or count child records, or building a rollup column and expecting the value to update the instant a child record is created.
The names sound interchangeable and both are read-only, so candidates pick whichever is listed first. They miss that calculated columns work only from values on the same record (plus its parent), while rollups aggregate related rows on a refresh schedule.
What to do instead
Use calculated columns for same-record formulas and parent-value derivation; use rollup columns for SUM/COUNT/MIN/MAX over related rows. Remember rollups recalculate on a schedule (default roughly 12 hours) and can be forced with Recalculate, so design the app or flow to tolerate or trigger that lag.
Deleting an option from a local choice column and assuming existing records lose that value or that reports will still display the old label.
Candidates think of a choice as a lookup-style reference. In Dataverse the integer value is stored on the record, so removing the option leaves orphaned values that render blank in forms and views.
What to do instead
Audit rows using the option before removing it, migrate them to a valid value, and prefer a global choice when the same list is reused so the definition is maintained in one place. Removing an option never rewrites existing data.
Configuring a one-to-many relationship with Cascade All as the delete behavior when the business rule is that child records must survive, or choosing Restrict Delete and then being surprised when the parent cannot be deleted at all.
The behaviors are read quickly as 'how related records behave' without mapping each option to an actual scenario, and Cascade All is the option candidates assume is needed for rollups or views to work.
What to do instead
Match each behavior to intent: Cascade All deletes children with the parent, Remove Link nulls the lookup, Restrict Delete blocks the parent delete while children exist, No Cascade does nothing. Only cascade-delete configurations are needed for that purpose; rollups work with any behavior.
Writing Filter() with a non-delegable function (for example searching with a complex expression on a large SharePoint or SQL table) and reporting that the gallery is 'missing records' in production but complete in the maker's small test environment.
The maker's dev data is under the delegation limit, so the warning triangle is invisible in behavior. The app is silently returning only the first page of rows.
What to do instead
Design filters around delegable operators and indexed columns, keep the delegable predicate first and the non-delegable refinement second, and verify against a dataset larger than the delegation limit (typically 500 or 2000 rows) before publishing.
Sharing a model-driven app and having users report the table or form is missing, even though the table, form, and views were all created correctly.
In a model-driven app the components must be explicitly added to that app. Creating a form or view in the table designer does not place it in the app; candidates stop after the table work is done.
What to do instead
Open the app in the modern app designer and add the table, then confirm the specific forms, views, charts, and dashboards are selected for that table, and that the table appears in the app navigation (site map) so it is reachable.
Using Set() for a value that should be scoped to one screen, or expecting a context variable created with UpdateContext() to be readable on a different screen.
Both syntaxes look like 'assign a variable'. Candidates do not distinguish global variables from screen context variables and only discover the problem when navigation lands on a blank screen.
What to do instead
Use Set() for global variables that must persist across screens, UpdateContext() for screen-local state, and Navigate(..., {name: value}) to seed a context variable on the target screen. Use Collect()/ClearCollect() when the value is a table that must persist for the session.
Creating an automated cloud flow on the 'row added, modified or deleted' trigger without setting trigger conditions or filtering columns, and then getting a runaway loop or thousands of unnecessary runs when the flow updates the same table.
The trigger fires on any change to the row by default, and the flow's own update action counts as a change. Candidates test with a single manual edit and never see the recursion or volume problem.
What to do instead
Narrow the trigger with Select columns (filtering attributes) and add trigger conditions in the trigger settings (for example an expression that checks the changed field or a status value), and make the flow's update action idempotent so a re-trigger cannot repeat the change.
Assuming a business process flow's stages enforce required data - that a record cannot move to the next stage until required columns are filled in.
Stages look like wizard steps with a Next button, so candidates infer validation. Stage gating only enforces the condition you configure, and the required flags come from the table or form, not from the BPF.
What to do instead
Implement enforcement with required columns on the table (or a business rule/plug-in) and, if stage progression must be controlled, configure the stage's condition step. Also remember the BPF must be added to the app and that it is a separate entity from the table it drives.
Customizing a managed solution's components directly in the test or production environment, then finding that the next managed import does not carry those changes forward.
The customization succeeds and looks correct, so the active unmanaged layer created on top of the managed solution is invisible to the maker until an upgrade overwrites expectations.
What to do instead
Make all changes in the unmanaged development solution in the dev environment, export as managed, and import to downstream environments. If an unmanaged layer already exists, remove it there and move the change into the source solution.