Dynamics 365 CE gives a delivery team many ways to change system behavior. That flexibility is useful, but it also creates a recurring architecture question: where should a requirement live?
The answer is rarely “always use low code” or “put it in a plugin.” A better decision starts with the business event, the required guarantee, and the team that will operate the result.
Begin with configuration
Use native configuration when the platform already expresses the rule clearly. Tables, forms, views, business rules, security roles, and process configuration are visible to administrators and reduce custom maintenance. Native configuration is often the most supportable engineering choice.
The boundary appears when a rule needs complex validation, cross-record consistency, transaction control, reusable domain logic, or behavior that native components cannot express safely.
Use Power Automate for observable orchestration
Power Automate works well when a process is asynchronous, integration-oriented, and valuable to monitor as a sequence of actions. Notifications, approvals, document movement, and calls to external services are common examples.
Before choosing a flow, ask what happens when a step runs twice, fails halfway, or receives data in an unexpected order. Retry behavior, idempotency, connection ownership, environment variables, and support visibility are part of the design.
Use plugins for server-side guarantees
A Dataverse plugin is appropriate when logic must run close to the data operation, enforce a rule consistently across clients, or participate in a transaction. Plugins can provide strong control, but that control comes with responsibility for execution time, recursion, exception handling, registration, tracing, and deployment.
Keep plugin responsibilities narrow. A plugin that performs large external operations synchronously makes a save depend on another system’s availability. The Dataverse transaction cannot roll back a side effect in that external system. Where possible, enforce the immediate invariant in the plugin and move slower orchestration to an asynchronous boundary.
Use PCF and client scripting for interaction
PCF controls and client-side code should improve how a user works with data, not become a second hidden business layer. They are useful for specialized visualization, guided input, and interaction patterns that standard controls cannot provide.
Any business rule implemented only in the browser can be bypassed by another integration or client. If the rule protects data integrity, it also needs a server-side home.
Use external services when the boundary is real
External .NET services are justified when the work requires independent scaling, specialized libraries, long-running processing, a separate security boundary, or integration ownership outside Dataverse. The service contract, authentication, monitoring, and failure model then become first-class architecture.
Worked example: validate a review, then notify
Consider a hypothetical review table with a custom Review state column. New rows must start in Draft. Moving to Final requires a decision date and review reference, and a Final review cannot return to Draft. A notification should follow a successful finalization, but a notification failure must not undo the saved review. This is an illustrative design exercise, not a description of a client implementation.
First check whether declarative platform rules can enforce the complete policy through the required write paths. If they cannot, a synchronous validation plugin is a reasonable choice. A browser-only check would miss API writes; a flow responding after the change would be too late to reject the original save.
For rejection logic, start with synchronous PreValidation, as Microsoft’s exception-handling guidance recommends. On Update, compare the incoming values with a registered pre-image to evaluate the proposed state. An omitted column in an update is not the same as a request to clear it. On Create, validate the initial state separately: an Update-only step would leave a gap if an integration created a Final row directly. Microsoft’s plugin tutorial demonstrates comparing an update with a pre-image.
Stage selection still matters. PreValidation is usually outside the transaction for the initial operation, but a nested operation can already be inside one. PreOperation runs inside the database transaction and is the place to change incoming row values before they are written. It is not the default place to reject a request that could have been rejected earlier. See the Dataverse event pipeline.
Keep the notification in a separate asynchronous process. Define its trigger, ownership, retry behavior, and duplicate handling. Dataverse-triggered flows can run more than once for repeated updates, even when values are unchanged; a trigger is not an exactly-once delivery guarantee. Microsoft’s row-trigger documentation explains this behavior. The notification process needs to recognize an already handled finalization, rather than send again on every matching update.
What would need to be verified
- Creating a Draft row succeeds; creating a Final row directly is rejected.
- Finalizing without the required date or reference fails without persisting the invalid state, through both the application and the API.
- A valid finalization succeeds, while a later attempt to return to Draft fails.
- An update that omits the reference retains the stored value; explicitly clearing it while Final is rejected.
- Repeated trigger deliveries do not produce duplicate notifications, and a notification failure leaves the valid review saved with a visible recovery path.
These are acceptance checks for the example, not reported test results. Registration is part of the implementation: choose the relevant messages, filtering attributes, and pre-image columns deliberately. Filtering attributes match columns included in a request, not proof that their values changed. Review Microsoft’s plugin registration guidance alongside the code.
The useful separation is the guarantee: reject an invalid write at the data boundary, then handle a recoverable notification independently. This example does not establish cross-record consistency or make an external operation part of a Dataverse transaction.
A practical decision sequence
- Can native configuration express the behavior clearly?
- Is the work asynchronous orchestration that benefits from visible flow history?
- Does the rule require a server-side or transactional guarantee?
- Is the requirement primarily about user interaction?
- Does it belong to an independently operated service boundary?
The right extension point is the smallest one that satisfies the required guarantee and remains understandable to the team that will own it.
This framework is grounded in my Dynamics 365 customer platform work and Power Platform expertise.