A plugin is a small piece of custom code that Dynamics 365 (or "Dataverse," the database and platform underneath it) runs automatically whenever something happens to a record — a save, a delete, a status change. Think of the platform's event pipeline as an airport: passengers (your data) pass through a sequence of checkpoints on their way from check-in to the gate, and some checkpoints already exist for built-in platform behavior, like catching duplicate travelers. A plugin is you adding your own checkpoint into that same sequence. Once you understand how the checkpoints are ordered and what each is for, most plugin questions — "should this run instantly or in the background," "which checkpoint do I use," "why did my own change trigger my own code again" — start to answer themselves. This tutorial walks through that checkpoint system in plain language, plus the patterns that show up again and again on real projects.

The plugin execution pipeline

Every action taken against Dataverse — creating a record, updating one, deleting one, linking two together — passes through this same sequence of checkpoints. A plugin is a small compiled piece of code (a .NET class, usually written in C#) that implements a required interface called IPlugin, and you register it against three things: which action to watch for (Create, Update, Delete, and so on), which type of record it applies to, and which checkpoint it should run at. When the moment comes, the platform builds an instance of your plugin and calls its Execute method, handing it a toolbox (IServiceProvider) your code uses to pull out what it needs: details about what's happening right now (the execution context), a way to talk back to Dataverse, and a logging tool.

The execution context is the object your plugin code touches on nearly every line. Among other things, it carries:

Synchronous vs. asynchronous: waiting at the counter vs. dropping off a form

Picture two ways of dealing with a bank. A synchronous plugin is like standing at the counter: you wait while the teller finishes your transaction, and if something goes wrong partway through, the whole thing is cancelled on the spot — nothing gets recorded. An asynchronous plugin is like dropping a form into a box and getting a text later once it's processed; you don't wait around, and by the time anything happens, the original transaction has already gone through and can't be cancelled because of it.

Synchronous (waiting at the counter)Asynchronous (drop off, get a text later)
Runs as part of the same save; can stop it or roll it backRuns after the save has already finished, separately
Can run at any of the three checkpoints belowOnly runs at the last checkpoint (post-operation)
Use when the person must see the result immediately — validation, or filling in a field before it savesUse for anything that doesn't need to block the user: sending a notification, calling a slow outside system, minor follow-up updates
Adds directly to how long the save takesDoesn't slow the save down, but isn't guaranteed to finish before the user's next click

The choice really comes down to trade-offs between user experience and data integrity, not a hard technical limit. If a field absolutely must be checked or calculated before a record is allowed to save, that has to be synchronous — there's no other way to stop the save from happening. If you're calling an outside system that might take a few seconds to respond, and the user doesn't need to wait on it, forcing that into the synchronous "wait at the counter" path is one of the most common — and most avoidable — reasons a system starts to feel slow.

The three checkpoints: pre-validation, pre-operation, post-operation

Within the synchronous part of the pipeline, there are three distinct checkpoints you can attach your code to — a bit like security checks that happen before you're allowed past the gate, versus a check that only happens once you're already seated on the plane:

A common rookie mistake is filling in a field at the post-operation checkpoint when pre-operation would have worked just as well, and more cheaply. If you're setting a value on the very record that's currently being saved, pre-operation lets the platform write it in one pass. Doing it after the fact means the record was already saved once, so your code has to make a whole separate update call — an extra round trip, and one more event rippling back through the pipeline.

What order do plugins run in?

When more than one plugin is registered on the exact same action, record type, and checkpoint, they run in an order controlled by a number called Rank — lower numbers go first, a bit like a queue number at a deli counter. You set this yourself, and on any project with more than a couple of plugins touching the same record type, it's worth assigning these numbers on purpose rather than letting them default and drift. I've debugged more than one issue where a validation check needed to run before a field got filled in, but both plugins had been left at the default rank and happened to run in registration order instead — not something you want to depend on.

Secure vs. unsecure configuration

Each plugin registration can carry two optional boxes of settings, handed to your code when it starts up: unsecure configuration and secure configuration. Unsecure configuration is visible to anyone who can open the plugin's registration screen — fine for something harmless like a threshold number or an on/off flag. Secure configuration is stored separately and isn't visible through that screen — the right place for anything sensitive, like a password or an API key your plugin needs to call an outside service. Treat "unsecure" as genuinely public to anyone with admin access, and default anything sensitive to secure configuration rather than assuming nobody will look.

Custom workflow activities vs. plugins

Both are pieces of compiled code registered into Dataverse, but they get used very differently. A plugin is wired directly into the event pipeline and runs automatically the instant its registered action happens — nobody has to remember to trigger it. A custom workflow activity is more like a tool sitting in a shared toolbox: a reusable step that shows up inside a workflow (or certain automated flows), and it only runs when someone building that workflow deliberately drags it in and connects it up.

In practice: reach for a plugin when something absolutely must happen every time an event occurs, with zero chance of a non-technical user forgetting to include it in a process. Reach for a custom workflow activity when you want to hand a reusable piece of logic — a tricky calculation, an external lookup — to admins or business users so they can drop it into different automated processes themselves, without a developer wiring up a brand-new registration each time. Modern projects increasingly reach for Power Automate to do what custom workflow activities used to cover, but the underlying idea — automatic pipeline hook versus reusable callable tool — still holds.

Common real-world plugin scenarios

A handful of patterns show up on nearly every project, in some form:

A minimal plugin, in plain terms

The example below is deliberately small — a pre-operation plugin that looks at an account's revenue field and stamps a "tier" field based on it, with basic safety checks and a log message so it can be traced later if something looks wrong:

public class SetAccountTierPlugin : IPlugin
{
    public void Execute(IServiceProvider serviceProvider)
    {
        var context = (IPluginExecutionContext)
            serviceProvider.GetService(typeof(IPluginExecutionContext));
        var tracing = (ITracingService)
            serviceProvider.GetService(typeof(ITracingService));

        if (context.InputParameters.Contains("Target") &&
            context.InputParameters["Target"] is Entity target &&
            target.LogicalName == "account")
        {
            tracing.Trace("SetAccountTierPlugin: evaluating annual revenue.");

            if (target.Contains("revenue"))
            {
                var revenue = target.GetAttributeValue<Money>("revenue").Value;
                target["new_accounttier"] = revenue >= 1000000
                    ? new OptionSetValue(1)  // Enterprise
                    : new OptionSetValue(2); // Standard
            }
        }
    }
}

Read it top to bottom and it says almost exactly what it does in plain English: "if this save is about an Account record, and it has a revenue value, mark it Enterprise or Standard, and leave a note in the log saying you checked."

Debugging, tracing, and common pitfalls

The logging tool (ITracingService) is the main way to debug code once it's live — you can't attach a live debugger to a server you don't control, so the trace messages your code leaves behind (visible in the plugin's trace logs once tracing is turned on) are often the only window into what happened. Leaving a trail of log messages at key decision points, not just when something goes wrong, pays off the first time you're chasing a live issue you can't reproduce on your own machine.

The pitfall that catches almost everyone eventually is the infinite loop: a plugin registered on "update an Account" that itself updates that same Account, which re-triggers the same plugin, which updates the Account again, forever. The platform's built-in safety net is the Depth counter mentioned earlier — it goes up by one every time a plugin triggers another, and once it crosses a fixed limit, the platform stops and throws an error instead of looping forever. Relying on that safety net alone is a bit like relying on a smoke alarm instead of not starting the fire in the first place — the better habit is to guard explicitly: check whether the field you're about to set already holds the value you're setting, so your own update doesn't needlessly set the loop back in motion. Two more things worth watching for: registering against a "before" or "after" snapshot that was never actually configured on that step, which quietly hands you nothing instead of an error, and forgetting that a "before" snapshot reflects the database just before the current operation — not before the whole request, if several operations are chained together in one call.