Learn how the Dataverse event pipeline really works — stages, modes, images, and filtering — and how to configure plug-in steps and business event handlers without writing code. Build systems with genuine transactional integrity and event-driven automation.

Imagine this scenario: your organization has a Service Request table in Dataverse. Every time a service request reaches "Escalated" status, three things need to happen — a notification record should be created, a related SLA timer should be updated, and the record's owner should shift to a senior team. A business rule can handle some of that. A cloud flow can handle more. But cloud flows are asynchronous and can fail silently, business rules have limited scope, and neither gives you server-enforced, synchronous transactional control over what happens before data is committed to the database.
That's the domain of plug-in steps and business event handlers. These are the mechanisms that let you attach server-side logic to Dataverse table operations — create, update, delete, associate, retrieve — so that your logic runs inside the Dataverse platform transaction, not as an afterthought. Historically, leveraging plug-ins meant deploying compiled C# assemblies and managing them through the Plugin Registration Tool. Business event handlers, introduced more recently, let you wire certain logic to platform events directly from the maker experience in ways that don't require code deployment at all. Understanding both, and knowing when to reach for each, is what separates a Dataverse power user from a genuine Dataverse architect.
By the end of this lesson, you will understand the plug-in execution pipeline deeply enough to make architectural decisions about where your logic belongs, how to register and configure plug-in steps without writing the plug-in itself, and how business event handlers can trigger Power Automate flows and other automation anchored to platform events rather than polling or connector-based triggers.
What you'll learn:
You should be comfortable with Dataverse as a platform before working through this lesson. Specifically, you should understand:
You do not need to be a C# developer to understand or configure plug-in steps. You do need to think carefully about execution order, transactional scope, and failure modes.
Before you configure anything, you need a genuine mental model of what the Dataverse platform does when a user saves a record. Most explanations gloss over the internals in ways that cause real pain later. Let's go deep.
When a client (a model-driven app form, an API call, a Power Automate action, a bulk import) sends an operation to Dataverse — say, an Update on a Contact record — that operation enters what Microsoft calls the event execution pipeline. The pipeline has a defined sequence of stages, and your plug-in steps slot into positions in that sequence.
Here's the sequence for a standard "Update" message:
Stage 10 — Pre-Validation: Runs outside the database transaction. This is the earliest intervention point. Logic here cannot be rolled back by the platform transaction because the transaction hasn't started yet. Use this for validation that doesn't need database access, or for operations that must survive even if the main operation is cancelled.
Stage 20 — Pre-Operation: Runs inside the database transaction, before the platform writes to the database. This is where you can modify the incoming request — change field values, add data — and those changes will be included in the committed record. Critical point: if your plug-in throws an exception here, the entire transaction rolls back, including anything other plug-ins have done.
Core Operation: The platform writes the data. You don't plug into this.
Stage 40 — Post-Operation: Runs inside the database transaction (for synchronous mode), after the platform writes but before the transaction commits. This is the most commonly used stage. You can read the newly written data, create related records, trigger side effects. In synchronous mode, exceptions still roll back everything. In asynchronous mode (Mode = 1), this runs as a background job after the transaction commits.
This distinction between synchronous and asynchronous mode is arguably the single most important decision you make when configuring a plug-in step.
Key insight
Synchronous Post-Operation is appropriate when you need side effects to be guaranteed within the same transaction — creating a related record that must exist if the parent exists. Asynchronous Post-Operation is appropriate when the side effect is advisory, can tolerate delay, or involves external systems. Choosing synchronous for everything because "it's simpler" is the most common performance mistake in Dataverse implementations.
Every plug-in step is registered against a specific message (the Dataverse API operation) and a specific entity (the table). The message name maps to the SDK/API operation: Create, Update, Delete, Retrieve, RetrieveMultiple, Associate, Disassociate, SetState, Assign — and many more. Custom API calls also generate messages.
When you register a step for Update on the Account entity, that step fires every time any account is updated — whether from a form, API, import, or another plug-in. That's often too broad. Step filtering lets you narrow it to specific column changes. If you only care when the statuscode column changes, you configure the step's Filtering Attributes to include only statuscode. The step then fires only when the update payload includes that attribute — not on every account update in the system.
This is enormously important for performance and correctness. A system with a dozen plug-ins all firing on every account update, with no attribute filtering, is a system with serious latency and debugging nightmares.
One more concept before we configure anything: images.
When a plug-in step runs, the execution context contains the current operation data — for an Update, that means only the fields being changed in this particular request, plus the entity ID. It does not contain all the other fields on the record. If you want to know what the priority field was before this update, or what all the other fields look like after it, you need images.
Images are named (you give each image a string alias like "PreImage" or "FullAccountState"), and you specify which columns to include — don't request every column unless you need them, because each image is fetched from the database and adds overhead.
Warning
Pre-images on Update steps are the most commonly misconfigured element in Dataverse plug-in registrations. If a developer's code references a pre-image alias that wasn't registered, the plug-in throws a null reference exception in production. Always verify image registrations against the actual code that consumes them.
The Plugin Registration Tool (PRT) is a standalone application distributed as a NuGet package. You don't write code with it — you use it to register and manage plug-in assemblies, steps, and images. Even if you're not the developer who wrote the plug-in, you need to understand PRT to configure and audit plug-in behavior in your environment.
The recommended approach is to download PRT via the Power Platform CLI or as part of the Power Platform Tools for Visual Studio Code. From a terminal:
pac tool prt
This command downloads and launches the Plugin Registration Tool against the environment you're authenticated to. Alternatively, download the NuGet package Microsoft.CrmSdk.XrmTooling.PluginRegistrationTool and run the extracted executable.
When PRT launches, click Create New Connection. Choose Microsoft Login and authenticate with your credentials. Select your environment from the list. Once connected, you'll see the main tree view on the left, which shows:
This hierarchy is the complete picture of all server-side logic registered in your environment. Before you change anything, take the time to read what's already there.
Click on any registered step and you'll see its properties panel. Let's walk through each field as you'd find it in a real system:
Name: Usually auto-generated as PluginTypeName: MessageName of EntityLogicalName. For example: Contoso.ServiceRequest.EscalationHandler: Update of cr7b2_servicerequest. This naming convention tells you the class, the operation, and the table at a glance.
Primary Entity: The logical name of the table this step monitors. cr7b2_servicerequest in our example.
Message: Update. You might also see Create, Delete, Associate, etc.
Stage: One of PreValidation (10), PreOperation (20), or PostOperation (40).
Execution Mode: Synchronous or Asynchronous.
Rank/Order: An integer controlling execution order when multiple steps fire on the same event. Lower numbers run first. This is only meaningful for steps at the same stage from different assemblies — within a single transaction, predictable ordering is important.
Filtering Attributes: A comma-separated list of column logical names. If empty, the step fires on all updates regardless of which fields changed. If populated, only fires when one of those listed columns is in the update payload.
Run in User's Context: Whether the plug-in executes with the permissions of the calling user (User mode) or with elevated system-level permissions (System mode, also called "Caller" vs "System" context). This has significant security implications — a plug-in running as System can bypass field-level security and record sharing restrictions.
Warning
Plug-ins running in System context bypass column-level security entirely. If your plug-in reads or writes fields protected by column-level security profiles, verify the execution context carefully. A plug-in running as System that exposes data via a notification record or log can inadvertently leak restricted field values to users who shouldn't see them.
To register a new step against an already-registered assembly (perhaps a developer deployed an assembly and you need to register the step without their involvement):
Update and the autocomplete will help.cr7b2_servicerequest).1 is fine.After registering the step, register your images by right-clicking the new step and selecting Register New Image. Set the image type (Pre-Image, Post-Image, or Both), give it an alias that matches what the plug-in code expects (ask the developer), and specify which columns to include.
Microsoft has been gradually surfacing plug-in step configuration in the Power Apps maker portal, making it accessible without the PRT. This is particularly valuable in environments where makers manage automation configuration but aren't involved in code deployment.
In the Power Apps maker portal, navigate to Solutions, open a solution, and select Add existing > More > Plugin Assembly to bring an existing assembly into your solution. Once the assembly is in a solution, you can see and manage its steps within the solution's component list.
For per-step configuration, select a registered step component within your solution. The properties panel shows the stage, mode, message, entity, and filtering attributes, and you can edit them directly — no PRT required for basic reconfiguration.
Tip
Always work within a solution when configuring plug-in steps in environments that will be migrated (from development to test to production). Plug-in steps outside a solution are environment-specific and won't travel with your deployment package. This is the most common reason automation "works in dev but disappears in prod."
This matters especially for teams that separate the developer role (who writes and deploys the assembly) from the solution architect role (who configures which steps are active, on which events, with which filtering). The maker portal lets architects configure without needing PRT credentials or developer tools.
Business event handlers represent a different paradigm. Instead of registering a compiled assembly to respond to Dataverse operations, you configure a connection between a Dataverse platform event and a Power Automate cloud flow — entirely from the maker experience, without writing or deploying any code.
A business event handler is a Dataverse-managed subscription that fires a cloud flow when a specific platform event occurs. The mechanism under the hood is similar to a plug-in step in asynchronous Post-Operation mode — the platform event fires, and a payload is dispatched to a subscriber. The key difference is that the subscriber is a cloud flow, not a compiled plug-in.
This matters architecturally because:
Use a business event handler (triggering a cloud flow) when:
Don't use a business event handler when:
Key insight
Business event handlers and plug-in steps are not competing technologies — they're complementary. A well-designed system often uses a synchronous pre-operation plug-in for validation and field manipulation, and a business event handler/cloud flow for all post-commit notification and integration work. The plug-in enforces data integrity; the flow handles the business workflow consequences.
Let's walk through a concrete scenario: you want a cloud flow to trigger whenever a Service Request record's status is changed to "Escalated," so the flow can send a Teams notification to the senior support team.
Step 1: Navigate to the table in the maker portal.
Go to make.powerapps.com, open your solution, and find the Service Request table. Select it, then look at the options in the top navigation.
Step 2: Access Business Events.
From the table's component list or the table editor, look for the Business events section. In the modern maker experience, this appears in the table's event catalog. You'll see a list of platform events available for this table — typically Create, Update, Delete, and in some cases operation-specific events like Assign.
Step 3: Select and configure the event.
Select the Update event for the Service Request table. You'll be prompted to specify filtering conditions — specifically, which columns' changes should trigger the event. Select statuscode (the Status Reason column). This mirrors the attribute filtering concept from plug-in steps but is configured through a guided UI.
Step 4: Create the handler flow.
After configuring the event, select + New handler or Create a handler. This opens Power Automate with a pre-configured trigger — specifically a When a business event occurs trigger — already connected to the event you configured. The trigger payload includes the full record data (or the data you configured to be included) from the event.
In the flow, you can now build the logic: check if the new status is "Escalated" (using the trigger body data), retrieve additional record details via Dataverse connector actions, compose a Teams message, and post it to the senior support channel.
Step 5: Activate and test.
Save and turn on the flow. Return to the maker portal and verify the handler appears as active under the business event. Test by updating a service request record's status to "Escalated" and monitoring the flow's run history.
Tip
Business event triggers pass a structured payload that includes the changed field values, the record ID, and metadata about the operation (who triggered it, when). Unlike polling-based triggers (which check for changes on a schedule), business event triggers fire in near-real-time after the commit — typically within a few seconds. For integration scenarios, this is significantly more responsive and less resource-intensive than a scheduled check.
For completeness: business event handlers use Dataverse's Service Endpoint infrastructure under the hood — the same mechanism used to push Dataverse events to Azure Service Bus, Azure Event Hubs, or webhooks. When you configure a business event handler that triggers a cloud flow, the platform registers a service endpoint of type "Webhook" pointing to the flow's trigger URL, and associates it with the table event via a registered step (in the SDK's async plugin model).
You can see these registrations in the PRT if you look at the Service Endpoints section. This means if you need to troubleshoot a business event handler that isn't firing, PRT is a useful diagnostic tool — you can verify whether the service endpoint and step registration actually exist.
Real environments don't have a single plug-in per event. They have layers — first-party platform logic, ISV solutions, and custom solutions all registering steps on the same messages. Understanding how these interact is critical for diagnosing unexpected behavior.
Within a single stage (say, all Pre-Operation steps for Update on Account), steps from different assemblies execute in ascending Rank order. Steps with the same Rank execute in an unspecified order. If you have two plug-ins that both modify the same field in Pre-Operation, the one with the higher rank (runs last) wins.
Microsoft recommends keeping rank values spread apart (10, 20, 30 rather than 1, 2, 3) to leave room for future insertions. This is the same discipline you'd apply to sort orders in views.
This is where things get genuinely complex. Consider this scenario:
If plug-in C throws an unhandled exception, the entire transaction rolls back: the service request update is not saved, the audit log record from step 3 is not saved, and the notification from step 5 is not saved. The user sees an error. This is powerful — it means you get true transactional integrity across related records. But it also means a bug in any synchronous post-operation plug-in can completely block users from saving records.
Warning
A synchronous post-operation plug-in that throws an unhandled exception is one of the most disruptive things that can happen in a production Dataverse environment. Every save to the affected table fails until the plug-in is fixed or disabled. Always implement defensive error handling in synchronous plug-in code, and always have a documented process for emergency step disablement via PRT or the maker portal.
If a plug-in step is causing all saves to fail, you need to disable it quickly without redeploying code. In PRT:
The step is immediately deactivated. The plug-in assembly remains registered, but this specific step no longer fires. You can re-enable it once the code is fixed and redeployed.
In the maker portal (within a solution), select the step component and use the Deactivate option from the command bar.
Tip
For mission-critical tables where a broken plug-in would halt operations, consider documenting the disable procedure and keeping PRT credentials accessible to an on-call administrator — not just developers. In a crisis, you don't want to be waiting for the developer with PRT access to wake up.
We touched on attribute filtering earlier. Let's be more precise, because getting this wrong is extremely common.
Attribute filtering on Update steps means the step fires only when at least one of the specified attributes appears in the incoming change payload. This is a filter on what was submitted for update, not what changed value (a field can be submitted with the same value it already had).
This creates some non-obvious behavior:
firstname in its Update action even though the value didn't change, any step filtered to firstname will fire.PATCH with only the changed fields are the "cleanest" trigger — only the actually changed fields appear.To verify what's actually in an incoming update payload, you can enable Dataverse plug-in trace logging (described below) and inspect the execution context's InputParameters > Target attributes collection.
In PRT, when registering or editing a step:
For our Service Request escalation scenario, we'd select only statuscode. The plug-in then fires only when a Service Request update includes the statuscode field — not when someone edits the description, adds a note, or changes the assigned user.
This is the difference between a system where plug-ins fire 2-3 times per relevant event and one where they fire 200 times per user session. The performance difference is not academic.
When a plug-in behaves unexpectedly — fires when it shouldn't, doesn't fire when it should, or produces wrong results — plug-in trace logging is your first tool.
Trace logging is controlled at the environment level in the Power Platform Admin Center:
admin.powerplatform.microsoft.comWarning
Setting trace logging to "All" in a production environment with high transaction volume will generate enormous log data and can itself impact performance. Use "All" only temporarily during active troubleshooting, then switch back to "Exception" or off.
Trace logs are stored as records in the PluginTraceLog table. You can view them in the maker portal via Advanced Find (the old classic interface, still accessible from model-driven apps) or through the Plug-in Trace Log view in Settings.
Each log record shows:
ITracingService.Trace() messages the developer wrote to the tracing serviceThat last point is why good plug-in developers instrument their code with trace messages. When you read a trace log and see structured diagnostic output, you can understand exactly what data the plug-in saw and what decision it made.
For diagnosing why a step isn't firing, check:
This exercise assumes you have a Dataverse environment with at least one custom table and administrative access to the Plugin Registration Tool.
Your organization has a Service Request table (cr7b2_servicerequest). A developer deployed an assembly containing escalation logic two months ago. You need to:
Launch PRT and connect to your environment
Expand the registered assembly list and locate the Contoso.ServiceRequest.Escalation assembly
Expand it to find the EscalationHandler plug-in type
Click the registered step. In the properties panel, verify:
cr7b2_servicerequestUpdatePostOperation (40)Synchronous or Asynchronous (note which it is and why it was chosen)statuscode listed? If not, this is a problem.Click the step's expand arrow to see registered images. Is there a Pre-Image registered? Does it include statuscode? If the developer's code checks the pre-image to know what the status was before the update, the pre-image must exist and include that column.
Finding 1: If filtering attributes are empty, document this as a performance risk — the plug-in fires on every service request update, not just status changes.
Finding 2: If no pre-image is registered but the code checks context.PreEntityImages["PreImage"]["statuscode"], the plug-in will throw a null reference exception when no pre-image is found. This is a latent bug.
statuscode from the attribute listPre ImagePreImage (must match what the developer's code references)PreImagestatuscode and ownerid (the two fields the escalation logic needs from the pre-state)Service Request table and select Business eventsUpdate event and configure it with statuscode as the triggering attributeTest the configuration by updating a Service Request's status to "Escalated" in a model-driven app. Monitor the plug-in trace log for the step execution, and check the flow's run history in Power Automate to confirm the Teams message was sent.
Symptom: The system becomes slow after a plug-in is deployed. Users report laggy saves on a high-activity table.
Cause: A synchronous plug-in step has no attribute filtering, so it fires on every update regardless of relevance. A table with 500 updates per hour is now triggering 500 plug-in executions, most of them unnecessary.
Fix: Add attribute filtering in PRT. Check trace logs to see execution frequency and average execution time. Eliminate unnecessary invocations first before optimizing the plug-in code itself.
Symptom: Users experience 2-5 second delays when saving records on a specific table.
Cause: A post-operation plug-in that sends email notifications or calls an external API is registered as synchronous. The external call blocks the user's save operation.
Fix: Change the step's execution mode to Asynchronous in PRT. The user's save completes immediately; the notification runs in the background. This requires accepting that the notification might be slightly delayed and that failures won't roll back the save.
Symptom: A plug-in throws KeyNotFoundException or null reference exceptions only on Update operations, not Create.
Cause: The code references context.PreEntityImages["PreImage"] but the registered image has alias "pre" or "BeforeUpdate". The alias is case-sensitive.
Fix: In PRT, check the registered image's Entity Alias value. Verify it exactly matches the string used in code. Update the image alias or ask the developer to update the code. Redeploy if the code changes.
Symptom: The cloud flow doesn't trigger even though records are being updated.
Cause (most common): The flow is turned off. Check Power Automate's flow status.
Cause (less obvious): The attribute filtering on the business event is set to a specific column, but the update operation isn't including that column in the payload. Verify by updating the record and explicitly changing the filtered column.
Cause (subtle): The service endpoint registration was deleted or corrupted. Check PRT's Service Endpoints section to verify the webhook endpoint for the flow exists and is active.
Symptom: Plug-in steps work in the development environment but don't appear in test or production after solution deployment.
Cause: The step was registered directly in the environment (not via a solution import), or was registered via PRT without being added to a solution first. Steps registered outside solutions don't travel with solution packages.
Fix: In PRT or the maker portal, verify the step exists in the correct solution. If it doesn't, create it within the solution context. For future registrations, always create the step while working within a solution in PRT (select the solution context before registering). This aligns with solution management best practices.
Symptom: A plug-in creates records that appear to be owned by the system, bypassing expected security role restrictions. Users with limited access can trigger operations that create records they shouldn't be able to create.
Cause: The step is configured to run in System context, which uses the SYSTEM user's full privileges regardless of who triggered the operation.
Fix: Change the step's "Run in User's Context" setting to the calling user. Be aware that this may cause the plug-in to fail if the calling user doesn't have the necessary privileges for the plug-in's operations. The right answer depends on your security design — review your security role configuration before deciding.
You've now seen three mechanisms for automating business logic in Dataverse:
The decision tree looks roughly like this:
Plug-ins can trigger other plug-in steps. A plug-in that creates a record will fire the Create message's registered steps. This can create recursive chains — Create triggers a plug-in, which updates a field, which triggers an Update plug-in, which creates another record, which triggers another Create plug-in...
Dataverse has a plug-in execution depth limit. The IExecutionContext.Depth property tells you how deep in the chain you are. If depth exceeds 8, Dataverse throws an exception to break the recursion. Defensive plug-in code checks context.Depth > 1 and returns early if it detects re-entrant execution — otherwise recursive loops are only stopped by the depth limit, and they're very expensive.
Plug-ins running in System context create or modify records attributed to the SYSTEM user. If you have auditing enabled, the audit logs for those records will show SYSTEM as the modifier, not the human user who triggered the operation. This is sometimes confusing for compliance review — auditors see records being modified by SYSTEM with no apparent human actor.
If audit traceability matters for your plug-in's side effects, consider running the plug-in in the calling user's context (if they have appropriate permissions) or explicitly setting the createdby/modifiedby context through the CallerId property in the execution context to associate operations with the originating user.
You've now covered the complete picture of server-side logic configuration in Dataverse — from the fundamental architecture of the event pipeline through practical step configuration, image registration, business event handlers, and production troubleshooting.
The key architectural principles to carry forward:
For your next steps, consider exploring how formula columns and rollup columns can handle calculated data needs that don't require plug-in complexity. If your platform events need to reach external systems beyond Power Automate, look into service endpoint configuration for Azure Service Bus and Event Hubs in the Plugin Registration Tool. And if you're working in an environment with complex security requirements, revisit how plug-in execution context interacts with the Dataverse security model — the combination of plug-in context, user context, and field-level security creates edge cases that require deliberate design.
The event pipeline is the backbone of everything sophisticated Dataverse does. Master it, and you can build systems that are genuinely reliable, transactionally sound, and maintainable at scale.