Salesforce Dictionary - Free Salesforce GlossarySalesforce Dictionary
All errors
Apex

MIXED_DML_OPERATION: DML operation on setup object is not allowed after you have updated a non-setup object (or vice versa)

Salesforce splits objects into "setup" (User, Group, GroupMember, PermissionSet, Profile, Queue, etc.) and "non-setup" (everything else). A single transaction cannot DML both kinds — you'll have to push one of them into an asynchronous context.

Also seen asMIXED_DML_OPERATION·DML operation on setup object is not allowed after you have updated a non-setup object·MIXED_DML

The platform draws a line between objects that configure the org (setup objects) and objects that hold business data (non-setup objects). One transaction may DML one side or the other, but not both.

Setup vs non-setup, briefly

Setup objects (locked together)Non-setup objects (everyday data)
User, UserRole, ProfileAccount, Contact, Opportunity
Group, GroupMember, QueueTask, Event, Case
PermissionSet, PermissionSetAssignmentLead, custom objects
ObjectPermissions, FieldPermissionsFiles, Notes, ContentDocument

If your code touches one row in a setup object and one row in a non-setup object inside the same DML transaction, you get this error.

The classic shape of the bug

// In a trigger after-insert on Account:
Account a = Trigger.new[0];          // touched a non-setup object first
GroupMember gm = new GroupMember(    // ... and now want a setup object change
    GroupId = someGroupId,
    UserOrGroupId = a.OwnerId
);
insert gm;   // throws MIXED_DML_OPERATION

Fix: move the setup DML into a future call

The textbook workaround is to push the setup-side work into @future — that starts a fresh transaction, so the "you already touched non-setup" memory is gone.

public class GroupAssigner {
    @future
    public static void addToGroup(Id groupId, Id userId) {
        insert new GroupMember(GroupId = groupId, UserOrGroupId = userId);
    }
}

// Now from your trigger:
GroupAssigner.addToGroup(groupId, a.OwnerId);

Equivalent options, depending on your context:

  • Queueable Apex (System.enqueueJob) — same effect, but with a returned job ID so you can monitor it.
  • Scheduled Apex at a 1-second offset — useful when ordering matters.
  • Platform Event with a subscriber trigger — the subscriber runs in its own transaction.

Tests are the special case

MIXED_DML_OPERATION does not fire in test code if both DMLs are wrapped in System.runAs(someUser):

@isTest
static void givenAUserContext_thenMixedIsAllowed() {
    User u = makeUser();
    insert u;                                // setup DML
    System.runAs(u) {
        insert new Account(Name = 'Test'); // non-setup DML — allowed inside runAs
    }
}

That's an exception carved out specifically so test data setup doesn't require a future call for every test.

When you can't refactor

Some legacy code patterns are stuck. A pragmatic escape hatch is to wrap every setup-object DML in a queueable from day one, regardless of whether the current call path needs it. It costs you a queueable per setup change but never throws this error again.

Related dictionary terms