6.1The event execution pipeline
Stages, and where an extension attaches to a message
A data operation in Dataverse is a message: Create, Update, Delete, Assign and the rest, plus the messages custom actions add. Your extension attaches to a message as a step, and the step declares which stage of that message's processing it runs at.
A step's stage controls two things: what data your extension sees, and what happens to the operation if your code throws. This section covers what you can see; §6.2 covers what a failure does.
Each message is processed in a series of four stages. PreValidation, for the initial operation, occurs before the main system operation, before the database transaction, and before the security checks that verify the calling user's permission for the operation. PreOperation occurs before the main system operation and within the database transaction, and is where values on a table row in the message are changed. MainOperation is for internal use except for custom APIs and custom virtual table data providers. PostOperation occurs after the main system operation and within the database transaction, and is where properties of the message are modified before it returns to the caller.citedsource: Power Apps docs — Event Framework in Microsoft Dataverse — Event execution pipeline
| Stage | Numeric value | Relative to the transaction | What it is for |
|---|---|---|---|
| PreValidation | 10 | Outside, for the initial operation | Cancelling the operation before the transaction opens |
| PreOperation | 20 | Inside | Changing values before they reach the database |
| MainOperation | — | Inside | Internal, plus custom APIs and virtual table providers |
| PostOperation | 40 | Inside, unless registered asynchronously | Reading the committed shape of the operation, and reacting to it |
6.1.1The stages as the platform's own registrations
Stage names aren't just docs vocabulary — they're values in the stage column of sdkmessageprocessingstep, so you can query your own environment for them.
Grouping that table on its stage and mode columns returns the labels the platform uses, with the number of registered steps beside each one. It runs twice below: filtered to the stage values an external registration may name, then unfiltered.
Evidence — the registered stages, filtered and whole
<fetch aggregate="true">
<entity name="sdkmessageprocessingstep">
<attribute name="stage" alias="stage" groupby="true" />
<attribute name="mode" alias="mode" groupby="true" />
<attribute name="sdkmessageprocessingstepid" alias="steps" aggregate="count" />
<filter>
<condition attribute="stage" operator="in">
<value>10</value><value>20</value><value>40</value>
</condition>
</filter>
<order alias="stage" />
</entity>
</fetch>
Grouping sdkmessageprocessingstep by stage and mode under a filter on the numeric stage values 10, 20 and 40 returned the labels Pre-validation, Pre-operation and Post-operation and no others, and a grouping of the same table by mode alone, under a filter on mode 0, returned a single label, Synchronous.verifiedartifact: ch06-external-stage-values · artifacts-ch06.md
Connected as skydude@itrancyngergmail.onmicrosoft.com Connected to... skydude's Environment steps stage mode 291 Pre-operation Synchronous 64 Post-operation Asynchronous 270 Post-operation Synchronous 206 Pre-validation Synchronous
The unfiltered grouping of the same table returns the three externally registerable stages alongside internal ones the documentation does not name — Initial Pre-operation, Internal Pre-operation Before and After External Plugins, Main Operation, Internal Post-operation Before and After External Plugins, Final Post-operation, and a deprecated Post-operation — with the Main Operation row carrying by far the largest count.verifiedartifact: ch06-pipeline-stage-inventory · artifacts-ch06.md
Those internal stages are the platform's own scaffolding around the four the pipeline exposes. Your step lands on Pre-validation, Pre-operation or Post-operation — the three the filtered query above returned — and the internal ones bracket it.
6.1.2Synchronous and asynchronous registration
A step declares a mode as well as a stage, and the two aren't independent.
Mode says where your step runs relative to the operation that triggered it: inside the same execution, or afterwards on the asynchronous service. The platform fixes which stages accept which mode.
A step registered for the PostOperation stage may use asynchronous execution mode, and such steps run outside the database transaction, on the asynchronous service, after the record operation completes. Asynchronous mode is mandatory for a step that performs an update and is registered on the Create message of the SystemUser table.citedsource: Power Apps docs — Event Framework in Microsoft Dataverse — Asynchronous plug-in steps
Evidence — the Create message, by stage and by name
Narrowed to the Create message alone, the same grouping returns 61 synchronous steps at Pre-validation, 86 at Pre-operation, 57 at Post-operation, and 16 asynchronous steps, again at Post-operation.verifiedartifact: ch06-create-message-steps · artifacts-ch06.md
Reading those Pre-validation registrations by name shows what the stage is used for in practice. The rows staged from that run are guards and validators — among them ValidateAIPluginObjectJson, ValidateClassicRulesDefinitionPlugin, ODataEntityDataSourceValidationPlugin, RestrictCertificatePlugin, ValidateStateTransitions — and each of them is synchronous.verifiedartifact: ch06-prevalidation-step-names · artifacts-ch06.md
Caution
A grouping returns a row where rows exist. No asynchronous row at Pre-validation or Pre-operation tells you what this one environment has registered, and it agrees with the documented rule rather than establishing it. The rule is the cited claim at the head of this subsection.
6.2Transaction participation and rollback
What a failure inside the transaction does to the operation that opened it
A stage inside the transaction shares its fate. If your code throws there, the operation that opened the transaction goes down with it.
An exception thrown by custom code at any synchronous stage within the database transaction causes the entire transaction to roll back. To cancel an operation deliberately, the condition is detected in the PreValidation stage and an InvalidPluginExecutionException is thrown carrying a message that describes why the operation was cancelled.citedsource: Power Apps docs — Event Framework in Microsoft Dataverse — the exception at a synchronous stage
So a deliberate cancel is a choice of stage, and the docs put a price on the later one.
Cancelling an operation at PreOperation triggers a rollback of the transaction and has significant performance impact, which is why the documentation places deliberate cancellation at PreValidation instead.citedsource: Power Apps docs — Event Framework in Microsoft Dataverse — PreOperation cancellation cost
Two consequences follow. First, a rejection at the stage that runs before the transaction opens has no committed work behind it to undo; one at a later stage does. Second, if your step throws at a later stage for an ordinary business reason, you have turned a routine refusal into a rollback, and the caller sees the operation fail rather than your step.
Guidance
Put your refusal in PreValidation and your mutation in PreOperation, and keep them in separate steps even when one condition governs both. A step that does both has to run at the later stage — the stage where refusing is expensive.
6.2.1A rollback observed
This guide's lab reaches the platform through the pac CLI, which cannot write a table row, so a plug-in step throwing inside a record operation is out of reach here. A solution import is a transaction the CLI does reach, and it answers a narrower version of the same question: when an operation carrying several changes fails partway, what happens to the changes it had already made?
The run used one unmanaged solution, ch67Lab, holding two hand-authored cloud flows. The zip carried an edit to the stored definition of ch67DailyNote and, beside it, a component the target rejects. The flow's definition was read before the import and again after.
The import failed. The edit that travelled in the same zip did not land: the flow's clientdata and its modifiedon value were identical before and after, and the solution's version in the environment stayed at the version the previous successful import had set. The failure surfaced through the asynchronous import operation as System.InvalidOperationException: The specified node cannot be inserted as the valid child of this node, because the specified node is the wrong type.verifiedartifact: ch06-import-rollback · artifacts-ch06.md
Evidence — the reads either side of the failed import
Before: the flow's stored definition ends "inputs":"ch67 lab", modifiedon 8/5/2026 5:42 AM, and pac solution list reports ch67Lab at 1.0.0.9. The zip carries 1.0.0.10 with a new Compose input. Both reads are in artifacts-ch06.md with the command that produced them.Error: An unexpected error occurred.After: "inputs":"ch67 lab", modifiedon 8/5/2026 5:42 AM, ch67Lab still 1.0.0.9.
Caution
The transaction above is a solution import, not a step in the event pipeline of §6.1. It shows a Dataverse operation carrying several changes discard the ones it had made when a later one failed. It does not show a plug-in step rolling back a record operation, and no claim on this page does.
6.3The two-minute bound
The bound on the message operation, and what exceeding it does
The bound gets quoted as if it were your plug-in's own. It is stated of the whole message operation instead, and that difference bites as soon as several extensions share one.
There is a hard 2-minute time limit for a Dataverse message operation to complete, and that limit covers the intended message operation together with all registered synchronous and asynchronous plug-ins. Limitations on CPU and memory resources apply alongside it, and exceeding those limits makes Dataverse throw an exception and cancel — roll back — the entire message operation; for the time limit the exception is a TimeoutException. An extension that exceeds threshold CPU, memory or handle limits, or is otherwise unresponsive, has its process killed instead, and any current extension in that process fails with exceptions; the next execution of that extension runs normally.citedsource: Power Apps docs — Analyze plug-in performance — Time and resource constraints
So the budget belongs to the operation, not to your step. The documentation is explicit about how little of it you control.
An extension author has no control over how long the message operation or the other synchronous steps registered on it take, and controls only the duration of their own code.citedsource: Power Apps docs — Analyze plug-in performance — what an extension author controls
The bound is a shared budget, then. Your step can be comfortable in isolation and still be sharing two minutes with the operation itself and with every other synchronous step on the same message, and the arithmetic changes again once a bulk operation invokes that message many times over.
Work whose duration scales with input volume is what breaks this bound, and it usually breaks it long after you shipped it. A step written for the one row a user saves gets invoked once per row by a migration, and the budget it exhausts is the migration's, not the form's — so the failure lands on a process nobody connects to your step.experientialbasis: implementations where a synchronous step written for a form save was later reached by a data migration and by an integration writing in batches
Guidance
Microsoft's general recommendation is to limit a plug-in's execution to no more than 2 seconds, and to consider asynchronous registration where the work needs longer — asynchronous execution being the option to weigh first wherever it is possible, for responsiveness and scalability.citedsource: Power Apps docs — Analyze plug-in performance — the two-second recommendation
6.4Classic workflows: background and real-time
One designer, two execution models, and one stored column that tells them apart
You author a classic Dataverse workflow once, and it executes in one of two ways. You pick in the designer, and the pick is recorded on the workflow row.
There are two types of workflow. Background workflows run when the system has resource availability, that is asynchronously. Real-time workflows run immediately, that is synchronously. A real-time workflow is created by clearing the Run workflow in the background option in the process designer; a background workflow by selecting Run this workflow in the background (recommended).citedsource: Power Apps docs — Microsoft Dataverse real-time workflows — the two types
That pick is the mode column on the workflow table, so you can read a whole environment's population out of a query rather than opening a designer. The grouping reaches further than classic workflows: the category column beside mode separates business rules, actions, business process flows and cloud flows.
Evidence — the workflow table by category, mode and type
Grouping workflow by category, mode and type in the lab environment returns Real-time in three categories. Business Rule carries it on every row it has. Workflow carries it on two definitions and two activations, alongside Background rows of both types. Action carries it on one definition and one activation, alongside Background rows. Business Process Flow and Modern Flow return Background rows and no Real-time ones.verifiedartifact: ch06-workflow-inventory · artifacts-ch06.md
Connected as skydude@itrancyngergmail.onmicrosoft.com Connected to... skydude's Environment type rows category mode Definition 3 Workflow Background Definition 5 Modern Flow Background Activation 1 Action Real-time Definition 4 Business Process Flow Background Definition 102 Action Background Activation 2 Workflow Background Definition 18 Business Rule Real-time Activation 102 Action Background Template 1 Action Background Definition 1 Action Real-time Activation 5 Business Rule Real-time Definition 2 Workflow Real-time Activation 2 Workflow Real-time
Reading the classic workflow definitions by row shows the two Real-time ones the platform ships in this environment, UpdateInvitationCode and Invite Redemption. Both carry triggeroncreate and syncworkflowlogonfailure set, both are Activated, and both are scoped Organization; the Background definitions beside them carry neither flag.verifiedartifact: ch06-realtime-workflow-rows · artifacts-ch06.md
6.4.1The capabilities a real-time workflow gives up
Choosing real-time costs you capabilities the background model has. The first one follows straight from what synchrony is.
Microsoft's capability table, classic workflow compared with Power Automate, gives classic workflow wait conditions on columns, access to the pre-image of the event data, custom background workflow activities and synchronous execution, and gives cloud flows looping, parallel branches, connectors to external systems, scheduled execution, approvals, run analytics and grouping steps into a transaction through changesets. That table compares classic workflow as a whole with Power Automate, and it does not say where wait conditions sit within classic workflow.citedsource: Power Automate docs — Replace classic Dataverse workflows with Power Automate — the capability comparison
Real-time workflows cannot use wait conditions; wait conditions can be used with background workflows, and a background workflow that uses one becomes invalid on conversion to a real-time workflow and cannot be activated until the wait condition is removed. What a real-time workflow has instead is the Parallel Wait Branch, which defines an alternative wait condition with its own steps and exists to keep the workflow from waiting indefinitely. Its Stop Workflow action takes a status of Succeeded or Canceled, and a status of Canceled prevents the event action from completing and shows the stop action's status message under the heading Business Process Error.citedsource: Power Apps docs — Configure real-time workflow stages and steps — wait conditions and cancelling the operation
The designer also tells you where in the pipeline of §6.1 your workflow runs, in the vocabulary of the form you fill in.
The designer places a real-time workflow in the pipeline of §6.1 explicitly. Under Options for Automatic Processes, Before corresponds to the preoperation stage and After to the postoperation stage. Row is created offers After alone, because the row has no unique identifier until after the internal MainOperation stage; row is deleted offers Before alone, because after MainOperation the row is gone.citedsource: Power Apps docs — Configure real-time workflow stages and steps — Start When and the pipeline stages
The rest of the cost is about the traces a real-time workflow leaves: how the platform stops it looping, and what record of its runs survives.
A real-time workflow that updates the column it triggers on re-triggers itself, and the platform stops that rather than the author: a real-time workflow run more than a certain number of times on one record in a short period fails with This workflow job was canceled because the workflow that started it included an infinite loop. Correct the workflow logic and try again, and the documented limit of times is 16.citedsource: Power Apps docs — Best practices for managing real-time workflow processes — the infinite-loop bound
Logs from successful synchronous workflow executions are always deleted to save space, so a real-time workflow's failures are recorded only where the Keep logs for workflow jobs that encountered errors option is selected on the definition. And more than one real-time workflow updating the same table can produce resource lock issues, recorded as SQL Timeout: Cannot obtain lock on resource.citedsource: Power Apps docs — Best practices for managing real-time workflow processes — logging and resource locks
The syncworkflowlogonfailure column read in §6.4 is that option, stored. Both of the platform's own real-time workflows carry it set, and that is the shape you want: successful runs leave no trace by design, so the failures are the only record you get.experientialbasis: field use of the process designer, where selecting the option is what changes this column, together with the two platform workflows above that carry it set; no source read here maps the one to the other
A real-time workflow you inherit with that option clear is close to unauditable. There is no run history to read, the designer shows you the logic rather than the traffic, and the only evidence of what it has been doing is the shape of the data it wrote.experientialbasis: handovers where a real-time workflow's behaviour had to be reconstructed from the records it had changed, because its own job records had been discarded
6.4.2The current posture on classic workflows
Classic workflows are discouraged, not deprecated. A migration planned around deprecation moves work nobody is forcing you to move.
The real-time workflow page opens its authoring section with an Important note reading There are better ways to create modern automations. Consider using Power Automate flows to automate your processes. It states no retirement date and no deprecation, and the authoring steps follow the note unchanged.citedsource: Power Apps docs — Microsoft Dataverse real-time workflows — the posture stated on the real-time page
The replacement guidance is scoped to the background model: Power Automate has significant advantages over the classic background workflow model, new automation processes are to be built as flows, and existing classic background processes are to be reviewed for replacement. On synchronous workflows the same page says only that the objective, or parts of it, should be evaluated for a cloud flow, and that splitting actions out as asynchronous lets the user continue working.citedsource: Power Automate docs — Replace classic Dataverse workflows with Power Automate — the recommendation and its scope
Guidance
Treat a classic background workflow as a migration candidate, and a real-time workflow as a design decision to take again. The first has a documented replacement with more capability than it had. The second has no like-for-like replacement in Power Automate, so replacing it means deciding whether the behaviour needed to be synchronous in the first place.
6.5Cloud flows and the asynchronous side
What a webhook trigger settles about consistency
A cloud flow always runs on the asynchronous side of the line this chapter is about, and no setting on it changes that.
In Microsoft's capability comparison the row Run synchronously (real-time) reads No for Power Automate and Yes for classic workflow.citedsource: Power Automate docs — Replace classic Dataverse workflows with Power Automate — synchronous execution
Asynchronous does not tell you how long your flow waits to start. The wait has its own answer, below.
Dataverse-triggered flows run near real-time after the trigger because they use webhooks, with no polling involved.citedsource: Power Automate docs — Replace classic Dataverse workflows with Power Automate — trigger latency
Near real-time and real-time are not the same thing, and the gap between them is where your consistency problems come from. The originating operation has committed before your flow is invoked, so the flow reads a database other work may have changed since, and it acts on a row a user may already have edited again.
The defect this produces is intermittent by construction, which is what makes it expensive to find. A flow that reads its own trigger row is reading a later version of it. Under ordinary use the two agree; under a rapid second edit, a bulk update or a second flow touching the same row, they do not — and your run history shows a flow that behaved correctly on the data it was given.experientialbasis: implementations where a flow computed a derived value from a row it read after the trigger, and the value disagreed with the row under quick successive edits
Evidence — the mode column on the lab's cloud flows
Every row of category Modern Flow in the lab environment carries mode Background — the three the platform installed and the two this guide authored and imported for §7.7. The mode column that distinguishes a real-time classic workflow from a background one takes only its background value here.verifiedartifact: ch06-modern-flow-mode · artifacts-ch06.md
Guidance
Where your rule has to hold at the moment of the write — a value another user is about to read, a refusal, an invariant spanning related records — put it on the synchronous side and pay for it. Reach for a flow where the work is a consequence of the write rather than part of it: a notification, an integration, a document, a downstream record. Once you have decided that, §7.1 takes up what a flow's trigger can be told to ignore.
6.6Low-code plug-ins and pro-code plug-ins
Two ways to put a step in the pipeline, and their current standing
Both extension points in this section register in the pipeline of §6.1. They differ in what you need in order to ship one, and in how settled each is.
Low-code plug-ins are documented as a preview feature, on a page carrying the note [This topic is pre-release documentation and is subject to change.] and the statement that preview features are not meant for production use and might have restricted functionality. Of the two types, instant low-code plug-ins are deprioritized and are not being delivered as a feature, replaced with functions; automated low-code plug-ins are triggered by a Dataverse table event, are bound to a table, take no parameters, and are registered at a stage the maker chooses between Pre-operation and Post operation. The behaviour is written in Power Fx, stored in Dataverse, and authored in the Dataverse accelerator app rather than registered by hand.citedsource: Power Apps docs — Streamline app development with low-code plug-ins — status and shape
A pro-code plug-in is a custom class compiled into a .NET Framework assembly, uploaded to Dataverse and registered as a step through the Plug-in Registration tool, which is also where the pipeline stage, the message, the table filter and the execution order are chosen. A plug-in receives an IPluginExecutionContext, which carries the stage it registered for and the parent context of any operation that triggered the current one, and it runs in the isolated sandbox service.citedsource: Power Apps docs — Event Framework in Microsoft Dataverse — registering a pro-code plug-in
On a project with a delivery date, what decides between the two is usually the preview label rather than the language. Picking a preview surface commits you to deciding again later, and the instant type being deprioritized rather than delivered is what that second decision looks like when it arrives.experientialbasis: teams that adopted a preview server-side surface and then had to carry it through a support case and a release wave with no first-party migration path
Caution
No claim in this section was run in the lab. The Dataverse accelerator app that authors a low-code plug-in is a model-driven app, and the Plug-in Registration tool that registers a pro-code step is a Windows desktop application; neither is reachable from the CLI session this guide is written against. The status above is what Microsoft's page said on the date in the footer, and preview pages move.
6.7The decision ladder
Six rungs, ordered by where the work runs relative to the transaction
The ladder is ordered on the chapter's own axis: where the work sits relative to the transaction that opened the operation. It runs from work the platform does while evaluating a row, through work inside the transaction, to work that runs after the commit. Your requirement stops at the first rung that reaches it.
| Rung | Relative to the transaction | Authored in | Reaches |
|---|---|---|---|
| Business rule | Inside, as a real-time workflow row (§6.4) | The rule designer | Columns on one table, and the form |
| Formula column | Neither: evaluated as the row is fetched (§2.2) | Power Fx on the column | A value derived from the row and its relations |
| Real-time workflow | Inside | The process designer | Dataverse rows and actions, without waiting |
| Low-code plug-in | Inside, at a chosen stage | Power Fx in the accelerator app | Dataverse data and connectors, in preview |
| Plug-in | Inside, at any registerable stage | A .NET assembly | The organization service and the sandbox's reach |
| Cloud flow | After the commit (§6.5) | The flow designer | Dataverse and the connector catalogue, unbounded by the operation's budget |
The ladder in §5.1 is the client-side counterpart of this one, and the two meet at the first rung: a business rule is the one surface on both, because you author it once and the platform evaluates it in two places.
The failure this ladder is written against is picking a second rung later and leaving the first in place, so one requirement ends up enforced by a business rule, a plug-in and a flow at once — three implementations that agree until someone changes one of them.experientialbasis: reviews of implementations where the same requirement had been met on three rungs in three different releases, and none of the three had been removed
Guidance
Read the ladder downwards and stop at the first rung that reaches your requirement, and read it again whenever a rung's cost changes rather than when the requirement does. What decides the rung is whether the write has to succeed or fail with the behaviour, and you can answer that before comparing any of the authoring surfaces.