I reported a duplicate-rule problem that did not exist
A test failed with DUPLICATES_DETECTED, so the rules were obviously set to block. They were not. Database.DMLOptions carries allowSave, but setOptions does not carry the duplicate-rule header — only the Database.insert overload does, and by default that overload does not throw when the save fails for some other reason.
A test inserted a record that looked like one already in the org, and the save was rejected. The conclusion was obvious: the duplicate rules are set to block rather than warn, which is too aggressive for an intake process where a second person at the same address is a normal thing that happens.
I wrote that up as a finding. A change was agreed on the strength of it.
Then I retrieved the metadata before touching anything, and the rules were already configured exactly the way I was about to “fix” them. The system was right. The test was wrong. This is the write-up of the thing I got wrong, because that is the more useful post.
What the rules actually said
force-app/main/default/duplicateRules/Account.Standard_Account_Duplicate_Rule.duplicateRule-meta.xml
<DuplicateRule xmlns="http://soap.sforce.com/2006/04/metadata">
<isActive>true</isActive>
<actionOnInsert>Allow</actionOnInsert>
<actionOnUpdate>Allow</actionOnUpdate>
<operationsOnInsert>Alert</operationsOnInsert>
<operationsOnUpdate>Alert</operationsOnUpdate>
</DuplicateRule> actionOnInsert=Allow with operationsOnInsert=Alert is an alerting rule: warn the user,
let them save anyway. All three standard rules in the org were set that way. Nobody had
configured anything to block. There was no misconfiguration to find.
So why was the save rejected?
Because an alerting rule still rejects a save that has not opted in, and Apex has no user to show a warning to.
That is the piece I had skipped over. “Alert” describes an interactive experience — a person sees a warning and clicks through it. A programmatic caller has no equivalent moment, so the platform demands the equivalent statement up front: pass the duplicate-rule header saying save it anyway. Without it, an alerting rule and a blocking rule behave identically from Apex, which is exactly why a failed insert tells you nothing about which one you have.
I had passed the header. That is what made me confident. It just never arrived.
The two ways to say it, one of which does nothing
three variants, run side by side
// 1 — plain insert. Rejected: DUPLICATES_DETECTED
insert acct;
// 2 — options set on the record, then a DML statement.
// Compiles. Runs. Rejected: DUPLICATES_DETECTED
Database.DMLOptions opts = new Database.DMLOptions();
opts.DuplicateRuleHeader.allowSave = true;
acct.setOptions(opts);
insert acct;
// 3 — options passed to the Database.insert overload. Saves.
// Note the return value: this overload allows partial success and
// reports failure in SaveResults rather than throwing. See below.
Database.DMLOptions opts2 = new Database.DMLOptions();
opts2.DuplicateRuleHeader.allowSave = true;
List<Database.SaveResult> results =
Database.insert(new List<Account>{ acct }, opts2); Variant 2 is what my test did, and it is the one that looks most like idiomatic Apex.
Database.DMLOptions is a real class, DuplicateRuleHeader.allowSave is a real property,
setOptions() is a real method that really does attach options to the record, and the
combination is accepted by the compiler without a murmur. Other headers set that way —
assignment rules, email headers — do carry.
setOptions() does not carry the duplicate-rule header. Only the explicit
Database.insert(records, dmlOptions) overload honours it.
Correction: the overload has a silence of its own
Adding this some days later, because the fix in this post went on to cost me an afternoon.
Database.insert(records, dmlOptions) is the right call for the duplicate-rule header. It is
also the overload that does not throw. There is no allOrNone argument in that signature —
partial success is governed by DMLOptions.optAllOrNone, which defaults to false — so unless
you set it, partial success is permitted, and a failed save comes back as a
Database.SaveResult with isSuccess() == false rather than as a DmlException.
Swapping a plain insert statement for it therefore does two things, and only one of them is
the thing you wanted:
what the swap actually changes
// Before: carries no duplicate-rule header, and throws loudly on any failure.
insert lead;
// After: carries the header — and now fails silently on everything else.
Database.insert(lead, opts); // returns a SaveResult nobody read
// What it has to be:
Database.SaveResult result = Database.insert(lead, opts);
if (!result.isSuccess()) {
throw new BookingException(describe(result.getErrors()));
} I found this the expensive way. A sandbox probe checking lead conversion field mapping reported
valid leadId is required: [leadId] — INVALID_CROSS_REFERENCE_KEY
which reads as a conversion problem, and I spent a while treating it as one. It was not. The
Lead insert two lines earlier had already failed on
REQUIRED_FIELD_MISSING: Required fields are missing: [Lead_Channel__c], returned a
SaveResult saying so, and carried on. The conversion was then handed an Id that was null.
The error that surfaced was three steps downstream of the error that mattered.
This is the same shape as the mistake the rest of the post is about, which is why it belongs here rather than in a post of its own: the platform accepted everything, reported nothing, and the first visible symptom pointed at innocent code.
Running all three next to each other is what settled it. Two rejected, one saved, same record, same rules, same org, in the same execution. At that point the variable is not the configuration.
The part I want to keep
The technical finding is useful and it is not the thing I would take away from this.
What actually happened is that a failing test produced a confident, specific, wrong claim about someone else’s system, and that claim very nearly caused a correctly configured org to be changed. The change would have been small and it would have been real: three standard duplicate rules loosened for no reason, on a data-quality control that was doing its job, and nothing afterwards would have pointed back at why.
The only thing that stopped it was reading the configuration before editing it — which I did out of habit rather than doubt, and which took under a minute.
There is a version of this job where the consultant’s failed test becomes the client’s configuration problem, and it is a very easy version to fall into, because the test is the thing you can see and the org is the thing you inherited. The discipline that prevents it is small and mechanical: when the evidence points at something you did not write, check the thing you did not write before you say so, not after.
Practical summary
DUPLICATES_DETECTEDis a statement about a save, not about a rule’s configuration. ReadactionOnInsertandoperationsOnInserton theDuplicateRulemetadata to learn the configuration.- An alerting rule still rejects programmatic saves that do not opt in. That is correct behaviour, not a misconfiguration.
- Set
DuplicateRuleHeader.allowSaveand pass the options throughDatabase.insert(records, dmlOptions). Do not rely onsetOptions()for this header. - That overload takes no
allOrNoneargument. WithDMLOptions.optAllOrNoneat its default offalseit allows partial success and returnsDatabase.SaveResultinstead of throwing; setoptAllOrNone = trueto make it all-or-nothing. CheckisSuccess()on every result, or adopting it to fix duplicates will lose you the error handling you had. - If a test in a data-loading or intake path fails on duplicates, suspect the test’s DML form before suspecting the rules.
The org was fine. The test was not. That is a better outcome than the reverse, but only because it was checked — and it was nearly not.