Dataverse plug-ins explained

By Emil Björk · Microsoft business apps consultant, Gothenburg

How Dataverse plug-ins work — pipeline stages, sync vs async, registration, debugging, and when to use a plug-in vs a Power Automate flow.

Reviewed May 20266 min read · 1,342 wordsPublished
On this page (12)

A Dataverse plug-in is a piece of .NET code that runs server-side in response to Dataverse events. Plug-ins are the most powerful customisation mechanism in the CRM-side Dynamics 365 stack — they run in the same transaction as the originating operation, can read and modify the entire context, and can call any external service. They are also the most demanding mechanism, with operational realities that Power Automate flows usually let you avoid.

The event pipeline

Every Dataverse operation (Create, Update, Delete, Associate, etc.) runs through a defined pipeline with four stages where plug-ins can register:

  1. Pre-validation (stage 10) — runs outside the database transaction, before any validation. Used for plug-ins that need to inspect or modify the request before security or duplicate-detection runs. Common for caller-context-based logic.

  2. Pre-operation (stage 20) — runs inside the transaction, before the database write. Used to modify the input target (e.g. compute a derived field that should be stored), or to throw an exception to cancel the operation.

  3. Main operation — the database write itself. No custom plug-ins here.

  4. Post-operation (stage 40) — runs inside the transaction, after the database write. Used to do follow-on work that should be transactional with the main operation: write a related record, raise a notification, call an external API (carefully).

Synchronous vs asynchronous

Plug-ins register as either:

  • Synchronous — runs immediately, blocks the user's request until complete. Failures roll back the transaction. Use for operations that must complete before the user moves on, or that need transactional rollback semantics.

  • Asynchronous — queued for later execution by the async service. Failures are retried; the user doesn't wait. Use for follow-on work that doesn't need to complete synchronously: notifications, integrations, indexing.

The execution context

Each plug-in receives an IPluginExecutionContext with the originating message, the target entity, the user, the depth (to prevent infinite recursion if plug-ins trigger each other), and shared variables for inter-plug-in data passing.

Registration

Plug-ins are packaged as signed .NET assemblies and registered through the Plug-in Registration Tool or via solution import. Registration includes: assembly, plug-in class, message (Create/Update/etc.), entity, stage, filtering attributes (only fire when these fields change), and async flag.

When to use plug-ins vs Power Automate flows.

  • Use plug-ins when:
    • The logic must be in the same transaction as the originating operation.
    • Performance demands milliseconds rather than seconds.
    • You need access to the pre/post images of the entity.
    • You need to throw exceptions to cancel the operation cleanly.
  • Use Power Automate flows when:
    • The logic is asynchronous and orchestration-heavy.
    • It crosses systems through connectors.
    • It involves human approval.
    • Maintenance is by low-code makers, not developers.

In modern practice, flows replace many plug-ins; plug-ins survive for transactional, performance-sensitive, server-only logic.

Debugging

Plug-ins are notoriously hard to debug. The Plug-in Registration Tool supports plug-in profiling that captures runtime context and lets you replay it locally in Visual Studio. Trace logs are visible in the Plug-in Trace Log if enabled.

Operational caveats

Synchronous plug-ins block the user; slow plug-ins make the system feel slow. Asynchronous plug-ins compete for the async service queue; bursty volume can backlog. External API calls from inside plug-ins risk timeouts; wrap them or move to async.

The sandbox and its limits

Every plug-in runs in the sandbox — an isolated .NET process with strict resource limits enforced by Dataverse. Understand these or your plug-in will fail in ways the local debugger never surfaces:

  • Two-minute execution ceiling. A single plug-in step must complete within roughly two minutes. Long-running work does not belong in a plug-in — offload to an Azure Function via Service Bus or to an async job.
  • Outbound HTTP is allowed but limited. Sandbox plug-ins may call external services, but only to the ports Microsoft permits (80, 443, plus a small set) and with no local filesystem, no P/Invoke, no reflection into System.Configuration, and no arbitrary threading. Use HttpClient with a short timeout and set ServicePointManager early only if you truly need it.
  • Memory and IL restrictions. Assemblies must be signed, sized reasonably, and only reference types on the sandbox allow-list. A build that works on your machine can silently fail to register — check the registration tool's error, not the runtime.
  • Impersonation is scoped. A plug-in can call IOrganizationService as the initiating user, the system user, or a specific user configured on the step — but the calling user's security roles still constrain what the executed operation can do. Read impersonation in plug-ins before assuming "run as SYSTEM" solves your permissions problem.

Decision matrix — plug-in vs flow vs Azure Function

The choice is not binary. Three tools, three homes:

RequirementRight home
Same transaction as the write, rollback semantics on failurePlug-in (sync)
Sub-second response required inside the user's requestPlug-in (sync)
Pre/post image comparison of an entityPlug-in
Cross-system connector logic, human approval, orchestrationPower Automate flow
Low-code maintenance by makersPower Automate flow
Long-running work (> 2 min), heavy compute, complex retryAzure Function via Service Bus
Scheduled batch that shouldn't touch user requestsAzure Function or async Dataverse job
Simple derived field on the row being writtenFormula column or plug-in — try formula first

Notice the modern pattern: use the platform's declarative tools first (formula columns, business rules, real-time workflows), reach for Power Automate for orchestration, and reserve plug-ins for the narrow set of transactional, latency-sensitive, image-comparison workloads that nothing else fits.

Common mistakes

  • Calling out to slow external APIs from a synchronous plug-in. A 4-second HTTP timeout on the vendor side becomes a 4-second freeze for every user who touches the record. Always async-out, or wrap with a hard timeout of 2 seconds and a fallback.
  • Registering on all attributes. Update plug-ins should specify filtering attributes — otherwise they fire on every save, even when the fields they care about did not change. This is the single largest performance-tax mistake in real environments.
  • Infinite recursion because of depth. A plug-in that updates the same record it was triggered on will loop unless you check context.Depth or use SharedVariables to signal "I already ran". The default recursion limit is 8; hitting it throws.
  • Trace-log noise. The Plug-in Trace Log is off by default and can grow fast when on. Enable it, harvest what you need, then turn it back down. Leaving it on Exception (not All) in production is a defensible balance.
  • Registering messages that don't exist for the entity. Retrieve on a custom table works; Retrieve on an out-of-box table with a specific message name may not. Always test the exact message + entity pair before deploying.
  • Assuming a plug-in and its flow can both handle a scenario. If both are registered and both retry on failure, you can double-post. Own each event with one mechanism.

Wrong-fit signals

If you are writing plug-ins to:

  • Send emails or notifications when nothing needs to be transactional — that is a flow.
  • Call a REST API on save "for logging" — that is a Service Bus enqueue and a downstream consumer.
  • Compute a value that could be a formula column — that is a formula column.
  • Enforce a business rule a user should see immediately in the UI — that is a business rule, or on-load JavaScript in the model-driven form.

Each of those choices moves the logic to a lower-maintenance surface with no cost to what the user experiences.

Rollout checklist

  • Choose the mechanism using the decision matrix above; document the reason on the code review.
  • Register on specific filtering attributes; never on the whole entity for an Update step.
  • Enable the Plug-in Trace Log during rollout, tune it down in steady state.
  • Wrap external calls in a bounded HttpClient timeout; prefer async pipeline.
  • Cover the plug-in with unit tests using FakeXrmEasy or the Microsoft Power Platform Tools test harness — replay real profiler captures where possible.
  • Deploy through solutions, not manual registration.
  • Monitor the async system jobs view for backlogs, especially after a data migration.

Where to go next

Next: the plug-in execution pipeline for exactly what runs when, plug-in exceptions explained for the errors you will meet, and plug-ins vs Power Automate for the decision that precedes writing one. The Plug-in Registration Tool covers registration and profiling; impersonation in plug-ins covers running as someone else.

Frequently asked questions

What are the stages of the Dataverse plug-in pipeline?

Pre-validation (stage 10) runs outside the database transaction before security and duplicate detection; pre-operation (stage 20) runs inside the transaction before the write and can modify the target or cancel the operation; post-operation (stage 40) runs inside the transaction after the write for transactional follow-on work.

When should I use a plug-in instead of a Power Automate flow?

When the logic must run in the same transaction as the triggering operation, needs to complete in milliseconds, needs pre- and post-images, or must throw an exception to cancel the operation cleanly. Flows win for asynchronous orchestration, cross-system connectors, human approvals, and anything a low-code maker will maintain.

What is the difference between synchronous and asynchronous plug-ins?

Synchronous plug-ins block the user's request until they finish, and a failure rolls back the transaction. Asynchronous plug-ins queue for the async service, retry on failure, and do not make the user wait — the right choice for notifications, integrations, and indexing.

How do I debug a Dataverse plug-in?

Use the Plug-in Registration Tool's profiler to capture the runtime context and replay it locally in Visual Studio, and enable the Plug-in Trace Log to see trace output. Keep external API calls out of synchronous plug-ins or wrap them tightly — a timeout there freezes the user.

Related guides

Browse every guide in Customer Engagement or just Dataverse platform.

Was this helpful?

Signals which guides land and which need work. No account, no comment box — corrections go through the contact page.

Spot something wrong or want a topic covered? Send a correction or a topic request — both are welcome.