All articles

5 Plugin Errors You'll Hit in Dataverse (and What's Actually Wrong)

Exact error messages, root causes, and fixes for the five Dataverse plugin errors you will hit sooner or later. Save yourself the 2am debugging session.

· 7 min read

If you’ve written plugins for Dataverse, you’ve seen at least three of these. If you’ve been doing it for a few years, you’ve seen all five — probably at the worst possible moment. This is the post I wish existed the first time I stared at a stack trace in the Plugin Trace Log at 2am, trying to figure out why a production workflow was silently eating records.

Each section below gives you the exact error message, what’s actually going wrong, how to fix it, and how to stop it from happening again.

Before We Start: The Plugin Trace Log Is Your Best Friend

Go to Settings → Plugin Trace Log (or navigate to https://yourorg.crm.dynamics.com/tools/plugintraceviewer). Set the trace log setting to All during development, Exception in production.

When an error fires, the trace log captures:

  • The full exception message and stack trace
  • The plugin step that triggered it
  • The entity and message (Create, Update, etc.)
  • The execution depth
  • The timestamp

Every error in this article will show up here. If you’re debugging without the trace log turned on, you’re guessing.

1. The Infinite Loop

The error

This workflow job was canceled because the workflow that started it included an infinite loop.

You might also see it phrased as:

This plugin execution exceeded the maximum depth limit of 8.

What’s happening

Your plugin triggers on Update of an entity. Inside the plugin, you update that same entity. That triggers the plugin again. Which updates the entity again. Dataverse tracks execution depth and kills the chain at depth 8.

This also happens with mixed chains: Plugin A fires a flow, the flow updates a record, that triggers Plugin B, which updates the original record, which triggers Plugin A again.

How to fix it

Option 1: Check the execution depth. The IPluginExecutionContext gives you the current depth. If it’s greater than 1, bail out early.

if (context.Depth > 1)
    return;

This works, but it’s a blunt instrument. It kills all re-entrant calls, even legitimate ones.

Option 2: Use a shared variable to mark your own updates. Before your plugin updates the entity, set a shared variable on the context. On entry, check for it.

// On entry
if (context.SharedVariables.ContainsKey("MyPlugin_SkipExecution"))
    return;

// Before your update
context.SharedVariables["MyPlugin_SkipExecution"] = true;

Option 3: Filter your plugin step. Register your plugin step with filtering attributes so it only triggers on the specific fields you care about — not the fields you’re updating. If your plugin triggers on statuscode changes and only updates description, it won’t re-trigger.

How to prevent it

Design your plugin registrations with filtering attributes from the start. Draw out the trigger chain on paper: “This plugin fires on X, updates Y, which triggers Z…” If the chain circles back, you have a loop.

2. The SQL Timeout

The error

Sql error: Execution Timeout Expired. The timeout period elapsed prior
to completion of the operation or the server is not responding.

Or the shorter variant:

Generic SQL error. CRM ErrorCode: -2147204784

What’s happening

Your plugin is running in the synchronous pipeline and doing something slow: a complex query against a large table, a loop that retrieves records one at a time, or an update that triggers cascading operations on thousands of child records. The synchronous pipeline has a 2-minute timeout. Hit it, and the entire transaction rolls back.

How to fix it

Reduce the query scope. If you’re querying a table with millions of rows, make sure your QueryExpression or FetchXML has tight filters and only returns the columns you need. Never use new ColumnSet(true) in production code.

// Bad — retrieves all columns from potentially millions of rows
var query = new QueryExpression("contact")
{
    ColumnSet = new ColumnSet(true)
};

// Good — retrieves only what you need, filtered
var query = new QueryExpression("contact")
{
    ColumnSet = new ColumnSet("fullname", "emailaddress1"),
    Criteria = new FilterExpression
    {
        Conditions =
        {
            new ConditionExpression("parentcustomerid",
                ConditionOperator.Equal, accountId)
        }
    }
};

Batch your operations. If you’re updating 500 records in a loop, use ExecuteMultipleRequest instead of 500 individual Update calls.

Move heavy work to async. Register the plugin step as asynchronous. The async pipeline has a 2-hour timeout. If the user doesn’t need to see the result immediately, async is almost always the right call.

How to prevent it

Profile your queries during development. The Dataverse SDK has a RetrieveMultiple call counter you can watch. If your plugin makes more than 3-4 service calls, question whether it belongs in the sync pipeline.

3. The Null Reference

The error

The given key was not present in the dictionary.

Or:

System.NullReferenceException: Object reference not set to an instance of an object.

The Plugin Trace Log might also show this as the record “doesn’t exist” when you try to access an attribute that wasn’t included in the target entity.

What’s happening

The Target entity in the plugin context only contains the fields that changed in the current operation. If a user updates only the name field on an account, the target entity won’t contain revenue, telephone1, or any other field. Your plugin tries to read target["revenue"] and crashes.

This also happens with pre-images and post-images when you forget to register them, or when you register them but don’t include the fields you need.

How to fix it

Always check before you access. Use Contains and null-conditional patterns:

// Safe access with Contains
decimal revenue = 0m;
if (target.Contains("revenue") && target["revenue"] != null)
{
    revenue = ((Money)target["revenue"]).Value;
}

// Or use GetAttributeValue<T> which returns default(T) if missing
var revenue = target.GetAttributeValue<Money>("revenue")?.Value ?? 0m;

Register entity images. If your plugin needs fields that might not be in the target, register a Pre-Image on the plugin step. The pre-image gives you the record’s state before the operation, with whatever fields you specify.

// Get value from target if it changed, otherwise fall back to pre-image
Entity preImage = context.PreEntityImages["PreImage"];
var revenue = target.Contains("revenue")
    ? target.GetAttributeValue<Money>("revenue")
    : preImage.GetAttributeValue<Money>("revenue");

How to prevent it

Establish a pattern: always use GetAttributeValue<T>(), always register pre-images with the fields your plugin reads, and always null-check entity references before calling .Id on them. Make this a code review checklist item.

4. The Privilege Error

The error

SecLib::AccessCheckEx failed. Returned hr = -2147187962,
ObjectID: <guid>, OwnerId: <guid>,
OwnerIdType: 8 and CallingUser: <guid>.

Or the friendlier version:

Principal user (Id=<guid>, type=8) is missing prvReadAccount privilege
(Id=<guid>) on OTC=1 for entity 'account'.

What’s happening

Your plugin runs under a specific security context. By default, that’s the calling user — the person who triggered the operation. If that user doesn’t have permission to read, write, or create the records your plugin is working with, you get this error.

This is especially common when:

  • Your plugin reads from a restricted table the end user doesn’t have access to
  • You used context.UserId (calling user) instead of context.InitiatingUserId when it mattered, or vice versa
  • Your plugin creates records owned by a team or user the calling user can’t assign to

How to fix it

Use the system user context when appropriate. When you create the IOrganizationService, you can pass null as the user ID to run under the SYSTEM account, which bypasses security roles:

// Runs as the calling user (default)
IOrganizationService userService =
    serviceFactory.CreateOrganizationService(context.UserId);

// Runs as SYSTEM — full privileges
IOrganizationService systemService =
    serviceFactory.CreateOrganizationService(null);

Use the system service only for operations that genuinely need elevated access. Don’t use it as a blanket fix — that defeats the security model.

Check your plugin step registration. In the Plugin Registration Tool, each step has an option to Run in User’s Context. You can set this to a specific user (like an admin service account). This is cleaner than hardcoding null in your code.

How to prevent it

Document which tables and operations your plugin touches. During deployment, verify that the security roles assigned to end users (or the execution context user) include the required privileges. Test with a non-admin user before calling it done.

5. The Swallowed Exception

The error

Unexpected exception from plug-in (Execute):
MyCompany.Plugins.MyPlugin: System.Exception: Something went wrong

Or worse, the user just sees:

An unexpected error occurred.

What’s happening

Your plugin threw a plain System.Exception (or let a NullReferenceException, FormatException, etc. bubble up uncaught). Dataverse doesn’t know what to do with it. In the synchronous pipeline, the user sees a useless generic error. In async, the system job fails with minimal information.

Dataverse only surfaces custom error messages to the user when you throw an InvalidPluginExecutionException.

How to fix it

Wrap your entire plugin in a try-catch and throw InvalidPluginExecutionException with a meaningful message:

public void Execute(IServiceProvider serviceProvider)
{
    var tracingService = (ITracingService)serviceProvider
        .GetService(typeof(ITracingService));

    try
    {
        // Your plugin logic here
        tracingService.Trace("Starting plugin execution...");

        // ... do work ...
    }
    catch (InvalidPluginExecutionException)
    {
        // Already the right type — let it propagate
        throw;
    }
    catch (Exception ex)
    {
        tracingService.Trace("Error: {0}", ex.ToString());
        throw new InvalidPluginExecutionException(
            $"An error occurred in MyPlugin: {ex.Message}", ex);
    }
}

Use the tracing service. Notice the tracingService.Trace() calls above. Whatever you write to the tracing service shows up in the Plugin Trace Log. Trace liberally during development — entry/exit of methods, key variable values, decision points. This is your primary debugging tool.

How to prevent it

Make the try-catch-rethrow pattern your standard plugin template. Every plugin. No exceptions (pun intended). Build a base plugin class that handles this automatically:

public abstract class PluginBase : IPlugin
{
    public void Execute(IServiceProvider serviceProvider)
    {
        var context = (IPluginExecutionContext)serviceProvider
            .GetService(typeof(IPluginExecutionContext));
        var tracingService = (ITracingService)serviceProvider
            .GetService(typeof(ITracingService));
        var serviceFactory = (IOrganizationServiceFactory)serviceProvider
            .GetService(typeof(IOrganizationServiceFactory));
        var service = serviceFactory
            .CreateOrganizationService(context.UserId);

        try
        {
            ExecutePlugin(context, tracingService, service);
        }
        catch (InvalidPluginExecutionException)
        {
            throw;
        }
        catch (Exception ex)
        {
            tracingService.Trace(ex.ToString());
            throw new InvalidPluginExecutionException(ex.Message, ex);
        }
    }

    protected abstract void ExecutePlugin(
        IPluginExecutionContext context,
        ITracingService tracingService,
        IOrganizationService service);
}

Now every plugin inherits from PluginBase and only implements ExecutePlugin. The error handling is baked in.

Quick Reference Table

ErrorRoot CauseFirst Thing to Check
Infinite loop / depth > 8Plugin triggers itselfFiltering attributes on step registration
SQL timeoutSlow query or too many operations in syncMove to async, optimize query
Key not in dictionary / null refMissing field in target entityUse GetAttributeValue<T>(), register images
SecLib::AccessCheckExCalling user lacks privilegesPlugin execution context user, security roles
Unexpected exceptionRaw exception instead of InvalidPluginExecutionExceptionAdd try-catch, use tracing service

Debugging Checklist

When a plugin fails and you’re staring at the trace log:

  1. Read the full exception. Not just the message — the stack trace tells you the exact line.
  2. Check the execution depth. If it’s 8, you have a loop.
  3. Check the entity and message. Is the plugin firing on the step you expect?
  4. Check the user context. The trace log shows the calling user. Do they have the right roles?
  5. Look at your Trace() output. If you don’t have any, add some and reproduce the error.

These five errors account for the vast majority of plugin failures I’ve debugged over the years. Once you’ve built the patterns to handle them — filtering attributes, null-safe access, proper exception wrapping, correct service context — they stop being emergencies and start being things you catch in code review before they ever reach production.

Share this article LinkedIn X / Twitter

Related articles