Real-time workflows in Dataverse enforce business rules at the pipeline level — blocking invalid operations regardless of whether they come from a form, an API call, or a bulk import. This lesson teaches you to design, configure, and troubleshoot synchronous workflows that genuinely enforce complex multi-condition logic without writing a single line of code.

Picture this: your organization's sales team has a hard rule — no opportunity can be closed as "Won" unless it has at least one associated quote that's been approved, the account it belongs to is marked as active, and the estimated revenue exceeds a floor set by the business unit manager. You've tried expressing this with form-level validation. Users find workarounds by editing records through the API, importing data via Excel, or triggering changes through Power Automate flows. The logic breaks every time someone touches the data from a path you didn't anticipate.
This is the fundamental limitation of client-side and form-level enforcement: it only governs one entry point. Dataverse, however, offers a different enforcement model entirely. By pushing business logic down into the platform's event processing pipeline — through real-time workflows and the classic plugin architecture — you make your rules apply uniformly regardless of how a record is created, updated, deleted, or associated. A direct API call, a bulk import, a canvas app, a Power Automate action, a model-driven form — they all pass through the same pipeline, and your logic fires every time.
By the end of this lesson, you'll have a working command of Dataverse's synchronous event system, understand where real-time workflows fit versus plugin steps, know how to configure both without writing C# code, and be equipped to troubleshoot when logic silently fails or throws unexpected errors.
What you'll learn:
This lesson assumes you are comfortable with Dataverse fundamentals — you understand tables, columns, and rows and have worked through the model-driven app stack. You should also understand business rules in Dataverse well enough to recognize why they're insufficient for the scenarios we'll tackle here. Familiarity with Dataverse table relationships — particularly cascade behaviors — will help you understand some of the sequencing nuances. Basic awareness of solutions and managed/unmanaged layers is assumed; if you need a refresher, review Solutions for Model-Driven Apps.
Before you configure anything, you need a mental model of what happens when a user or system attempts to create, update, or delete a Dataverse record. Every data operation triggers a pipeline — a structured sequence of stages that Dataverse processes synchronously before the transaction commits to the database.
Dataverse's event pipeline has four named stages, but only two are relevant for the enforcement patterns we're discussing:
Pre-Validation fires before Dataverse begins the database transaction. At this stage, the operation hasn't been locked into a transaction yet. Logic that runs here can cancel the operation cleanly, but it can't access shared row locks, which means it's not suitable for operations that need to read related records with transactional consistency.
Pre-Operation fires after the transaction begins but before Dataverse writes the record. This is where most enforcement logic belongs. You can inspect the incoming data, read related records within the same transaction boundary, and throw an error that rolls the entire transaction back cleanly.
Post-Operation fires after the record has been written to the database, still within the same transaction. This is where you'd trigger side effects — creating related records, updating a parent record's aggregate field, calling an external system — but note that any error here still rolls back the entire transaction, including the original write.
Asynchronous fires after the transaction commits, outside the main thread. This is where background workflows, Power Automate cloud flows triggered by Dataverse, and asynchronous plugins run. These cannot block the original operation.
Key insight
Real-time workflows and synchronous plugin steps both run inside the transaction boundary. When they throw an error, the entire database transaction is rolled back, including any changes made earlier in the same pipeline. This is what makes them reliable for enforcement — there's no partial state to clean up.
Each pipeline execution is associated with a message — essentially the operation type. The most common are Create, Update, Delete, Assign, SetState, Associate, and Disassociate. Each table/message combination is its own pipeline entry point.
This matters because "update" is deceptively narrow. When a user changes the owner of a record, that fires Assign, not Update. When a user activates or deactivates a record using the status buttons, that fires SetState, not Update. When a user adds a record to an N:N relationship, that fires Associate. If your business rule should fire on any of these, you need to register logic on each relevant message separately.
Real-time workflows (also called synchronous workflows) are configured through the classic Workflow Designer in the legacy Dynamics 365 / Dataverse customizations interface. They run inside the Pre-Operation or Post-Operation stage depending on your configuration. Unlike plugin steps — which require compiled C# assemblies registered through the Plugin Registration Tool — real-time workflows are purely configuration-based, which is why they're the focus of this lesson's "without custom code" framing.
That said, real-time workflows have meaningful limitations: they can't query arbitrary data using FetchXML, they can't make HTTP calls, and they can only operate on the record they're triggered on plus directly related records through lookups. For scenarios within those constraints, they're the right tool.
The default behavior when you create a workflow in the classic designer is background (asynchronous). This trips up nearly everyone the first time. A background workflow runs after the operation commits — it has no ability to block, roll back, or modify the data being saved. If you're trying to enforce a rule that should prevent a record from saving in an invalid state, a background workflow will fail silently: the record saves, the workflow runs a few seconds later, detects the violation, and then what? You can try to reverse it, but you're now chasing a committed state rather than preventing it.
Use a real-time (synchronous) workflow when:
Use a background (asynchronous) workflow when:
Warning
Real-time workflows add synchronous latency to every operation they trigger on. A poorly designed real-time workflow that runs 10 cross-table queries can add 2-3 seconds to every save operation on that table. Design them carefully and test under realistic load.
You might wonder: can Power Automate replace real-time workflows entirely? For background logic, yes — Power Automate cloud flows triggered by Dataverse record changes are far more capable (HTTP calls, branching, loops, multi-system integration). But they are always asynchronous. As of this writing, there is no mechanism in Power Automate to run a cloud flow synchronously inside a Dataverse transaction. If you need to block a commit, real-time workflows remain the no-code option.
Let's build the scenario from the introduction in stages. We want to block an opportunity from being closed as "Won" unless its parent account is Active.
Navigate to your Power Apps maker portal at make.powerapps.com. In the left navigation, select your solution. Inside the solution, click New and look for Automation → Process. If you don't see this option, you may need to switch to the classic interface: look for the "..." menu and "Switch to classic."
In the classic interface, go to Settings → Process Center → Processes, or navigate via the solution's component list. Click New.
The dialog that appears asks for a name, category, and table. Set:
Note
The "Category" dropdown is where you choose between Workflow (the classic async/sync engine) and other types like Business Process Flow or Action. Make sure you select Workflow.
Once the workflow editor opens, you'll see a properties panel on the right and a canvas in the center. Find the checkbox or dropdown labeled Run this workflow in the background (the exact label varies slightly by environment). Uncheck it. This converts the workflow to real-time (synchronous) mode.
With background mode disabled, two new options appear:
For validation logic that blocks invalid data, select Pre-Operation. This fires after the transaction begins, giving you consistent reads of related records, and any error you throw rolls back the transaction cleanly.
Set the Scope to Organization if the rule applies to all users regardless of business unit. Scope here controls which records trigger the workflow based on ownership, not which users the workflow applies to — a common point of confusion.
Below the scope settings, you'll see checkboxes for when the workflow should run:
For our scenario, we want this to fire when a record's status changes (because "Won" is a status reason change, which fires SetState). Check Record status changes.
Warning
"Record fields change" is for Update operations only. Changing the opportunity status through the "Close Opportunity" dialog fires SetState, not Update. If you only check field changes, your validation will never fire on the Close action. Always test the exact path your users will take.
In the workflow canvas, click Add Step and select Check Condition. The condition editor opens — it's a structured query builder that reads roughly like: [Field] [Operator] [Value or Field].
For our rule, we want to check whether the related Account's status is Active. In the condition editor:
This condition reads: "If the related account's status is NOT Active, then..." — inside the "If True" branch, add a Stop Workflow step with the action Cancel and write your error message: "This opportunity cannot be closed as Won because the associated account is not active. Please reactivate the account or contact your administrator."
The error message you provide here is exactly what Dataverse will surface to the user (or to the calling application via an exception) when the workflow cancels the operation.
Right now, this workflow will fire and potentially cancel any status change, not just "Won." We need to add a gate. Before the account status check, add another Check Condition step:
Only if this outer condition is true should we evaluate the account's status. Nest the account check inside this outer condition's "If True" branch.
Your logic tree now reads:
IF Status Reason = Won
THEN
IF Account.Status ≠ Active
THEN Stop Workflow (Cancel) with error message
This is a critical structural pattern in the workflow designer: outer conditions gate inner conditions. Without this outer gate, every status change on every opportunity would trigger the full evaluation.
Click Save, then Activate. The system will prompt you to confirm activation. Once active, the workflow registers as a step in the Dataverse pipeline for the SetState message on the Opportunity table.
To verify it's working, open an opportunity whose parent account is Inactive and attempt to close it as Won. You should receive the error message you configured. Test from:
All three should be blocked. If the API call goes through, double-check that your workflow is Active and that the trigger is correctly set to Record status changes, not Record fields change.
The simple account-status check above is a good starting point, but the full scenario requires multiple conditions: at least one approved quote must exist, and the estimated revenue must exceed a threshold. Let's tackle each.
The workflow designer allows you to add a Check Condition against related child entities using the relationship navigation. For the "approved quote" check, within the same workflow:
Add a new condition step. In the field selector, navigate from Opportunity through the relationship to Quotes. The system lets you query the existence and count of related records meeting criteria. Select Quote → Status Reason and set it to Approved.
However, here's where the classic workflow engine shows its age: it doesn't support aggregate queries ("count of related records > 0") directly. Instead, it supports a pattern where you navigate into the child collection and check if at least one record matches. The condition evaluator in the workflow designer treats a related entity query as "does any related record meet these criteria?" — which is exactly what you need.
The condition: [Related Quote].[Status Reason] Equals Approved — if no related quote has Status Reason = Approved, this condition evaluates to false, and you branch to your cancellation message.
Key insight
The workflow designer's child entity condition check evaluates to true if any related record meets the criteria. This is "exists" semantics, not "all" semantics. If you need to verify that ALL related records meet a condition, you need a different approach — either a Pre-Operation plugin (which requires code) or a calculated/rollup column that you can then check in the workflow.
This is one of the most powerful architectural patterns available to no-code enforcement. If you can't express a complex aggregate check in the workflow designer, push the aggregate calculation into a rollup column on the table. Create a rollup column that counts approved quotes. Your real-time workflow then simply checks whether this count is greater than zero.
The caveat: rollup columns recalculate asynchronously by default (every 12 hours, or triggered manually/via workflow). For a real-time enforcement scenario, this means the rollup value might be stale. The fix is to force the rollup recalculation before your condition check runs — you can do this by adding a Perform Action step that calls the CalculateRollupField action on the current record before evaluating the rollup column value.
Your workflow step sequence becomes:
1. IF Status Reason = Won
THEN
2. Perform Action: CalculateRollupField (Count of Approved Quotes)
3. IF [Count of Approved Quotes] < 1
THEN Stop Workflow (Cancel) - "No approved quote found"
4. IF Account.Status ≠ Active
THEN Stop Workflow (Cancel) - "Account is not active"
This sequence guarantees fresh data at the moment of validation.
For the revenue threshold — which is set at the business unit level — you're reading from the related business unit record. In the condition editor, navigate: Opportunity → Owner (User) → Business Unit → [Your Custom Revenue Floor Column].
Compare this against Opportunity → Estimated Revenue using the Is Less Than operator. If Estimated Revenue is less than the business unit's floor, cancel.
This cross-table navigation (two hops: user → business unit → column) is fully supported in the workflow designer's field picker. The depth limit is approximately three relationship hops, which covers the vast majority of real business rules.
We've been using Pre-Operation for validation. But the same workflow engine also supports Post-Operation, which has different semantics and different use cases.
Pre-Operation fires after the transaction begins but before the write. Changes you make to the target record in this stage are merged into the record being saved — you don't need to do a separate update. This is the ideal stage for:
In the real-time workflow designer, you can add Update Record steps in the Pre-Operation stage. These updates merge into the original operation rather than creating a second database roundtrip. This is more efficient than updating in Post-Operation.
Post-Operation fires after the write but still within the transaction. Use this when:
For example: when an Opportunity is created, automatically create a Follow-Up Task. In Post-Operation, the opportunity's row ID exists and you can set the task's Regarding lookup to it.
Warning
Every step in a Post-Operation real-time workflow participates in the original transaction. If your Post-Operation workflow creates a related record and that creation fails (e.g., a required field is missing), the entire transaction rolls back — including the original record save. Design Post-Operation steps defensively, with fallback conditions that prevent hard failures on non-critical side effects.
Even if you're not writing plugin code yourself, you'll encounter registered plugin steps in every mature Dataverse environment. Understanding how they interact with your real-time workflows is essential for anyone doing serious platform configuration.
The Plugin Registration Tool (PRT) is a desktop application available as part of the Microsoft.CrmSdk.XrmTooling.PluginRegistrationTool NuGet package, and also accessible through the open-source XrmToolBox. It shows you every registered step across the entire pipeline — which assembly, which message, which stage, which table, and the execution order (rank) of each step.
Even if you're not deploying custom plugins, you should use the PRT or XrmToolBox to see what's already registered. In a heavily customized environment, you might find:
Your real-time workflows appear as steps registered under an assembly called something like Microsoft.Crm.Workflow.WorkflowPlugin. They have an execution order (rank) of 1 by default. If a third-party plugin also runs Pre-Operation on the same message with rank 1, the execution order between your workflow and theirs is non-deterministic. You can request the third-party vendor change their rank, or reorder by converting your logic to a proper plugin step with a specific rank.
Steps within the same stage and message execute in ascending rank order. Rank 1 runs before rank 2. If your workflow must run before a specific plugin (e.g., to validate data that the plugin will act on), you need rank-based sequencing. Real-time workflows register at a fixed rank that you can't change through the workflow designer — another argument for converting to plugin steps when precise sequencing is critical.
Note
The execution order applies within a stage. Pre-Validation always runs before Pre-Operation, which always runs before Post-Operation. You can't make a Post-Operation step run before a Pre-Operation step by adjusting rank.
As your real-time workflow library grows, you'll find yourself repeating the same validation pattern across multiple workflows. The Dataverse classic Action solves this through reusability — an Action is a custom message that can encapsulate a chunk of logic and be called from other workflows, plugins, or the API.
In the same Process designer, select Category: Action instead of Workflow. Unlike workflows, Actions:
Define an Action called ValidateOpportunityCanWin that takes an Opportunity ID as input. Inside the action, place all the validation conditions. Now your various triggers (Close Opportunity, custom button, API call) all invoke the same action rather than duplicating the condition logic.
From your real-time workflow, add a Perform Action step, select ValidateOpportunityCanWin, and map the current opportunity's ID to the input parameter. If the action cancels with an error, that error propagates up through the calling workflow and cancels the original operation.
This pattern dramatically reduces maintenance surface area. When the business rule changes, you update one Action rather than hunting through a dozen workflows.
Let's build something complete. The scenario: a Customer Service team has a rule that a service case (incident table in Dataverse) cannot be Resolved unless:
Before building the workflow, create a rollup column on the Case table:
This gives you a count of confirmation notes. Navigate to Tables → Case → Columns → New Column, set the type to Rollup, and configure the related entity aggregation as described. Save the column. (See Formula Columns and Rollup Columns in Dataverse for a detailed walkthrough of the rollup configuration interface.)
Create a new Workflow process:
Step 1 — Gate on Resolved status: Add Check Condition: Status Reason = Resolved (or Problem Solved, depending on your org's configuration). All subsequent steps nest inside this condition's "If True" branch.
Step 2 — Refresh the rollup:
Add Perform Action: CalculateRollupField for the Has Confirmation Note column on the current Case.
Step 3 — Check confirmation note: Add Check Condition: Has Confirmation Note < 1. If true, Stop Workflow (Cancel) with message: "Cases cannot be resolved without a note containing 'Resolution confirmed'. Please add the required compliance note."
Step 4 — Check actual duration: Add Check Condition: Actual Duration (minutes) = 0. If true, Stop Workflow (Cancel) with message: "You must log time worked before resolving this case."
Step 5 — High priority date check: Add Check Condition: Priority = High. If true, nest another condition: Resolve By Date is After Today. If true, Stop Workflow (Cancel) with message: "High priority cases cannot be resolved after their target resolution date. Please contact a supervisor to escalate."
Save and activate. Test each condition independently:
Test via the model-driven form, via the Dataverse Web API, and via a Power Automate flow that uses the Update a row action with status set to Resolved. All three paths should be uniformly blocked.
Symptom: the operation completes, but your workflow appears in the workflow history as "Succeeded" even though conditions should have triggered a cancellation.
Cause: The workflow is running as a background workflow, not real-time. A background workflow that calls "Stop Workflow (Cancel)" logs a cancellation in its own history but has no ability to roll back the already-committed transaction.
Fix: Open the workflow, deactivate it, check that "Run this workflow in the background" is unchecked, and reactivate.
Symptom: the validation triggers when you expect it not to (or doesn't trigger when you expect it to).
Cause: Mismatched trigger selection. "Record status changes" covers SetState. "Record is updated" covers Update. Status changes via the Close dialog, the Status field on the form, and direct SetState API calls all fire SetState, not Update. But if your status field is directly editable on the form as a regular field and saved via the normal Save button, that fires Update, not SetState.
Fix: Check your form configuration. If the status field is an editable form field, you need both triggers. If it's only changeable via dialog (like Close Opportunity), SetState alone is sufficient.
Symptom: the condition checking a related record's field evaluates incorrectly — for example, the account status check always passes even when the account is inactive.
Cause: The field picker resolved to the wrong relationship. If Opportunity has multiple lookups to Account (a common scenario in complex data models), the workflow might be traversing the wrong one. Check Dataverse data model relationships to verify which lookup is semantically correct for your scenario.
Fix: In the condition editor, explicitly verify the full navigation path displayed in the condition (it shows table.relationship.field). If it's the wrong relationship, rebuild the condition from scratch with careful attention to which lookup you select.
Symptom: users see a generic "An error has occurred" message instead of your custom cancellation message.
Cause: Your workflow hit a runtime exception before reaching the "Stop Workflow (Cancel)" step. This could be a null reference (the related account lookup is empty, so the account.status navigation throws), a missing field, or a misconfigured action step.
Fix: Add a null-guard condition before any related-record navigation: Check Condition — Account (Lookup) Does Not Contain Data → Stop Workflow (Cancel) with message "This opportunity requires an associated account before it can be closed." This prevents null navigation exceptions.
Tip
Always guard against null lookups before navigating through them. Every cross-table condition is a potential null reference if the lookup field is optional or blank.
Symptom: the workflow works correctly for admins but throws errors or behaves incorrectly for regular users.
Cause: Real-time workflows run in the context of the calling user by default (unless the workflow is owned by a system account and configured to run in owner's context). If the calling user doesn't have read access to the related Account or Business Unit, the cross-table navigation returns null rather than throwing an access-denied error — which means your conditions evaluate incorrectly.
Fix: Change the workflow's Run As setting to Workflow Owner and ensure the workflow owner is a service account with read access to all relevant tables. Navigate to the workflow properties (the header section above the canvas) and look for the Run As dropdown.
This intersects with your security model — review Dataverse Security: Business Units, Security Roles, and Teams to ensure your service account has minimal-necessary read privileges rather than over-provisioned system administrator access.
Symptom: resolving a parent record triggers your validation workflow, but the workflow then triggers another workflow on a child record operation, which triggers another, and the whole thing cascades into timeout or stack overflow errors.
Cause: Your Post-Operation workflow creates or updates a related record, which triggers that related record's own workflows, which may in turn update the original record, creating a loop.
Fix: Design workflows with loop prevention in mind. Add a condition at the top that checks a "processing" flag field — set it to true at the start and check for it before the main logic runs. Alternatively, use the Stop Workflow step with Succeeded action at the end to ensure workflows don't chain unexpectedly. Review cascade behaviors on your relationships to understand which operations cascade automatically and which you're triggering manually.
Dataverse enforces a 2-minute limit on the total transaction time. If your real-time workflow, combined with all other synchronous steps in the pipeline, takes longer than 2 minutes, the transaction is killed and the user sees a timeout error. Complex workflows with many cross-table queries can approach this limit under load.
Profile your workflows by checking the Workflow history execution times (Workflow History entity in the advanced find). If you're seeing times above 5-10 seconds per execution, redesign: move non-critical logic to background, pre-calculate aggregates with rollup columns, and eliminate redundant condition checks.
Real-time Pre-Operation workflows run inside the database transaction, which means they hold row locks for their entire duration. If your workflow reads from a high-contention record (a parent record shared by many child operations running in parallel), you can create deadlock patterns under concurrency. This is particularly common in bulk import scenarios where thousands of records are created simultaneously.
For bulk operations, consider whether your enforcement logic needs to apply to imports or only to interactive operations. Dataverse provides a BypassCustomPluginExecution request header that callers with the prvBypassCustomPlugins privilege can use to skip plugin and workflow execution — useful for trusted bulk operations but a security consideration if you're relying on workflows for compliance. Track this privilege carefully in your security role configurations.
Design your workflows to be idempotent — running them twice should produce the same outcome as running them once. This matters because Dataverse can retry failed synchronous operations under certain infrastructure conditions. A Post-Operation workflow that creates a related record can create duplicates if it runs twice. Guard against this with an existence check before creation: "If related Follow-Up Task with name X does not already exist, then create it."
Real-time workflows in the classic designer are powerful, but they have a ceiling. You'll know you've hit it when:
At that point, you're looking at either a custom plugin (C# code registered as a pipeline step) or an Azure Function called from a custom plugin. That's outside the scope of this lesson, but it's important to know the boundary exists.
Within the no-code space, PCF controls can handle form-side interactions, and Power Fx formula columns can handle read-only derived values — but neither runs in the event pipeline. Real-time workflows are the highest-level, lowest-code option for synchronous data-layer enforcement.
Tip
Before escalating to a custom plugin, ask whether the scenario can be restructured. Often, a combination of a rollup column (to pre-aggregate data), a calculated column (to derive a value), and a real-time workflow (to enforce based on those values) can handle cases that initially seem to require code.
You've now worked through the complete mental model for data-layer enforcement in Dataverse: the event pipeline, its stages and messages, and how real-time workflows fit into it. You've built multi-condition validation logic, learned to use rollup columns as proxy aggregates, structured reusable Actions, and seen how to diagnose the most common failure modes.
The key architectural insight to carry forward is this: the data layer is the last line of defense, and real-time workflows are your most accessible tool for enforcing it. Everything above the data layer — forms, business rules, canvas app logic, Power Automate — can be bypassed by sufficiently determined or technical users. Real-time workflows cannot.
Here's where to go next:
The combination of server-side enforcement (this lesson), structured process guidance (business process flows), and security-layer access control (security roles and field permissions) gives you a defense-in-depth data quality architecture that holds regardless of how your data is accessed.
Model-Driven Apps & Dataverse