Demonstrate the use of the different event execution pipeline stages
The introduction shows why Contoso's developers reach for a plug-in instead of a business rule or a flow. Before you write that code, you need a mental model for when it runs. Dataverse processes every data operation through an event execution pipeline, and the stage you choose determines what your plug-in can safely do and how expensive a mistake becomes.
Event execution pipeline stages
When an employee submits a Request record in Contoso's provisioning system, Dataverse doesn't apply that change in one step. It moves the operation through four ordered stages: PreValidation, PreOperation, MainOperation, and PostOperation.
Each stage gives extensions, including plug-ins, a different opportunity to inspect or react to the data involved. IPluginExecutionContext.Stage and Dataverse telemetry report these values, so one class registered on several steps can tell stages apart.
PreValidation runs first, before the main system operation and before Dataverse even checks whether the calling user has permission to perform the action. This early position makes PreValidation the right place to cancel an operation outright, because the cancellation happens before the database transaction opens.
For Contoso's Request table, a check that blocks the save entirely (say, confirming the employee has any allowance record at all) fits naturally here. When a PreValidation step is triggered by another extension's operation, it can run inside that outer transaction; IsInTransaction tells the plug-in whether it is.
PreOperation also runs before the main operation, but by this point Dataverse has already opened the database transaction. That timing makes PreOperation the stage for changing values on the entity undergoing the operation, since those changes become part of the same transaction as the save.
You can still cancel an operation from PreOperation, but doing so rolls back a transaction that's already open, which costs more than stopping the operation earlier in PreValidation.
Dataverse reserves MainOperation for the platform itself, along with custom APIs and virtual table data providers. You don't register ordinary business logic plug-ins against this stage; it's not a lever available to Contoso's developers for Request validation or enrichment.
PostOperation runs after the main operation completes. A synchronous PostOperation step still runs inside the open transaction, before Dataverse commits it, so an exception here can still roll back the whole operation. An asynchronous PostOperation step runs later, outside the caller's wait, after Dataverse finishes the triggering operation.
This stage is where you react to a change rather than try to influence whether it happens. Notifying a coordinator that a Request now needs approval, or calling out to another system to confirm device availability, both belong in PostOperation.
| Stage | Value | Timing | Typical use | Key constraint |
|---|---|---|---|---|
| PreValidation | 10 | Before the main operation and before security checks | Cancel an operation early, before any transaction opens | Runs before permission checks occur |
| PreOperation | 20 | Before the main operation, inside the transaction | Change values on the entity undergoing the operation | Canceling here rolls back an open transaction and carries a performance cost |
| MainOperation | 30 | The platform's core data operation | Reserved for custom APIs and virtual table data providers | Not available for your own business-logic plug-ins |
| PostOperation | 40 | After the main operation, inside the transaction for synchronous steps | React to a completed change; call other systems | The only stage that supports asynchronous execution |
Guiding question: Contoso needs to stop a Request from saving whenever the employee has no allowance left. Which stage fits that requirement, and why would PreOperation be a more expensive choice for the same check?
PreValidation fits best because the check can run before the transaction opens. PreOperation can still cancel the save, but Dataverse must roll back an open transaction, adding work without improving the decision.
Choose an execution mode
Every plug-in step also runs in one of two execution modes. A synchronous step runs in line with the triggering operation: the caller, whether that's the model-driven app form or an external API client, waits for your code to finish before the operation completes.
Because the caller waits, synchronous plug-ins need to run fast. That's why PreValidation and PreOperation steps, which exist to validate or shape data before the save completes, almost always run synchronously.
Asynchronous steps work differently. Dataverse queues the step, and the asynchronous service runs it later, after the triggering operation finishes, outside the caller's wait time. Asynchronous execution mode is only available for PostOperation steps, which fits what PostOperation is for: reacting to a change after it happens, not deciding whether it happens.
Suppose Contoso's coordinator-notification logic calls an external system to check device availability. Running that call asynchronously from PostOperation keeps the employee's Request submission fast, since the person submitting the request doesn't wait on a call to another system.
Set execution order for plug-in steps
A single stage and message combination can carry more than one registered step. Suppose Contoso eventually registers two separate PreValidation steps on the Create message for Request: one that checks allowance balance, and another that checks whether the Device table contains an eligible replacement category.
| Step | Checks | Execution order |
|---|---|---|
| 1 | Allowance balance | 10 |
| 2 | Device table for an eligible replacement category | 20 |
Dataverse uses this value to decide which step runs first: a lower execution order value runs before a higher one, for the same stage and message. Here, the allowance check (order 10) runs before the replacement-category check (order 20). Don't give two steps on the same message and stage the same execution order, because Dataverse doesn't guarantee which one runs first.
Keeping related checks in separate, ordered steps, rather than one large step, makes each piece of logic easier to test and reuse later. Contoso's developers build out the full plug-in that way in the next unit.
Choose a stage based on transaction behavior
The reason stage choice matters so much comes down to what happens when something goes wrong. An exception thrown by a synchronous plug-in at any stage inside the database transaction (PreOperation or PostOperation) rolls back the entire operation, not just the plug-in's own work.
That's a safety net, but it's also a reason to fail fast and cheap rather than fail late and expensive.
Consider what this means for Contoso's allowance check again. If that check runs in PreValidation and throws an exception, Dataverse cancels the Request before any transaction exists, which is inexpensive and immediate.
If the same check instead waits until PostOperation to throw that exception, Dataverse has already written the Request record and possibly triggered other synchronous logic, so now it has to unwind all of it.
The earlier stage doesn't just happen to be convenient. It's structurally cheaper for any logic whose entire job is to say no.
Now that you understand which stage fits which kind of logic, you're ready to see how a plug-in actually reads and changes data at that stage. The next unit shows how the execution context carries the Request record's values into your code, so you can turn this pipeline model into working logic.