Dynamics 365 plugin filtering attributes: stop plugins running on every save

A plugin registered on Update with no filtering attributes runs on every save of that table. Any column, any user, any integration pushing rows in. It's one of the first things I check when someone tells me Dynamics has got slow.

What goes wrong without them

When you register a step on the Update message and leave Filtering Attributes empty, Dataverse runs the plugin for every update to that table. It doesn't matter which column changed. With auto-save on in a model-driven app, that can be several times while one person has a form open.

What you usually see:

  • Saves take a bit longer than they should, and nobody can say why.
  • Bulk updates and data imports crawl, because every row runs code that has nothing to do.
  • Now and then a plugin updates its own record, triggers itself again, and keeps going until the platform stops it with an infinite loop error.

None of this shows up in dev with ten records and one user. It shows up in production on a busy day.

How filtering attributes actually work

Filtering attributes are the list of columns that cause the step to run when they're included in the update request. Two details catch people out:

  • Included, not changed. If an integration sends every column on every update, including ones whose value hasn't changed, the plugin still fires. Filtering attributes help most when the callers only send what changed. Worth checking your integrations do that.
  • Never add the primary key. The record ID is always in an update, so putting it in the list makes the step fire every time and cancels out the rest.

The same idea applies to Power Automate. In the Dataverse trigger "When a row is added, modified or deleted", the Select columns box sets the filtering attributes for that flow. Leave it empty and the flow runs on every update too.

The fix, step by step

  1. List every Update step. Open the Plugin Registration Tool, connect to the environment, and go through each assembly. Note every step on Update (and UpdateMultiple) with an empty Filtering Attributes field.
  2. Read the code, not the step name. For each one, find the columns the code actually reads from Target or uses to decide whether to do anything. That's your filtering list.
  3. Set filtering attributes to just those columns. Update the step and pick them from the list.
  4. Use a pre-image for old values. If the code needs the previous value of a column, register a pre-image with only those columns, instead of retrieving the record again inside the plugin. Don't accept the default of all columns on the image; it costs performance for nothing.
  5. Stop the plugin updating its own record. If a post-operation step calls Update on the same record, see whether it can move to pre-operation and set the value on Target directly. The value gets saved in the same transaction, with no second update and no chance of a loop.
  6. Make the change through your pipeline. Step registrations live in the solution. Change them in dev and deploy, rather than editing steps directly in production, or the next release will put the old registration back.

None of this changes what the plugin does. It just stops it running when there's nothing for it to do.

About that infinite loop error

Every time a plugin or workflow makes a request that triggers another plugin, the execution context's Depth goes up by one. If it hits the limit (8 by default) within the time window (one hour by default), Dataverse treats it as an infinite loop and stops the chain.

The usual cause is a plugin on Update with no filtering attributes that updates the record it's running on. Filtering attributes plus the pre-operation pattern above fixes most of these properly.

You'll see code that checks context.Depth > 1 and returns early. It stops the loop, but it also stops the plugin running when a legitimate process (another plugin, a flow, a workflow) updates the record. Treat it as a last resort and comment why it's there.

If you're moving to UpdateMultiple

Filtering attributes work on UpdateMultiple too, but the step runs if any of the records in the batch include one of the columns. So inside the plugin you still need to check each record in Targets for the columns you care about. Don't assume they're all there.

A quick way to find the worst offenders

  • Plugin trace log. Turn tracing on in test, run a normal day's activity, and look at which plugins show up far more often than you'd expect.
  • Application Insights. If Dataverse telemetry is going to Application Insights, look at plugin executions by count and duration, and at anything running with a depth above two.
  • Tables with lots of sync steps. Contact, account and any custom table that integrations write to are the usual suspects.

When it's more than a tidy-up

Most of the time this is an afternoon's work and a release. Sometimes it turns up something bigger: a plugin calling an external system during the save, or several plugins and flows all reacting to the same update in an order nobody planned. Those need a proper look at where the logic should live. I wrote about that choice on the Dynamics 365 integrations page.

Plugin registrations, steps and images are part of every Dynamics 365 health check I do. If you'd rather have someone senior on your team fix it with you, that's what I do as a Dynamics 365 CE consultant.

Microsoft's documentation

Dynamics got slow and nobody knows why?

Book 20 minutes. Tell me what's happening and I'll tell you where I'd look first.

Book a 20-min call