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, Profile | Account, Contact, Opportunity |
| Group, GroupMember, Queue | Task, Event, Case |
| PermissionSet, PermissionSetAssignment | Lead, custom objects |
| ObjectPermissions, FieldPermissions | Files, 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.
