SfSkills Domains Docs GitHub

Home / data / duplicate-rule-person-account-edge-cases

duplicate-rule-person-account-edge-cases

Duplicate Rules and Matching Rules for Person Accounts — PA shape, lead convert, PII. Triggers: person account duplicate rule, PA matching, IsPersonAccount filter. NOT for general B2B duplicate management — use admin/duplicate-management. NOT for Person Account enablement — use data/person-accounts.

Open the skill folder on GitHub


Duplicate Rule Person Account Edge Cases

Activate this skill when authoring or reviewing Duplicate Rules and Matching Rules in an org that has Person Accounts enabled (B2C, FSC, or mixed B2B/B2C). Person Accounts are a dual-record beast: one logical "consumer" is stored as a 001-prefix Account row plus a 003-prefix Contact row, kept in lock-step by the platform. Rules written for B2B Accounts or for stand-alone Contacts will silently mis-fire on PA — surfacing wrong matches, missing real duplicates, breaking Lead conversion, or worse, exposing PII across record-type boundaries.

This skill is NOT a general duplicate-management primer (see data/duplicate-management for the platform model) and does NOT cover Data.com / Lightning Data (see integration/data-com). It assumes the practitioner already knows what a Matching Rule, a Duplicate Rule, and a Match Formula are.


Before Starting

Gather this context before writing or editing any rule:

The single most common wrong assumption: "PA is just an Account with a flag, so my B2B match rule will work." It will not. PA matching needs Account-side person fields, an IsPersonAccount = true filter, and (in mixed orgs) a RecordType filter.


Core Concepts

1. Person Account is a composite record (one 001 + one 003)

A Person Account is two physical rows that the platform keeps synced:

SideId prefixField surface
Account row001Account.PersonEmail, Account.PersonHomePhone, Account.PersonMobilePhone, Account.Name (computed from FirstName/LastName), Account.IsPersonAccount = true, Account.RecordType.IsPersonType = true
Person Contact row003Contact.Email (synced from Account.PersonEmail), Contact.HomePhone, Contact.MobilePhone, Contact.AccountId points at the matching 001

A Match Rule is built on one SObject. If it is built on Account, the formula must reference Account.PersonEmail (not Contact.Email). If it is built on Contact, it can reference Contact.Email and the Person Contact row will be matched — but the Account-side row will not, so de-dupe on Account UI/imports will miss the duplicate.

Implication for B2C orgs: build the consumer match rule on Account, target Person fields, and add IsPersonAccount = true to the formula.

2. The IsPersonAccount filter is load-bearing in mixed orgs

In an org that holds both B2B Accounts ("Acme Corp") and B2C PAs ("Acme Smith"), a match rule that compares Account.Name without an IsPersonAccount filter will report Acme Corp as a duplicate of Acme Smith — same name, different universe. The fix is a Match Formula gate:

IsPersonAccount = TRUE AND <person-field comparisons>

Without this gate, the rule produces noise that will be ignored by reps until a real duplicate slips through unnoticed. Salesforce's UI does not warn when the filter is missing.

3. Email matching: Account.PersonEmail, not Contact.Email

Account.PersonEmail and Contact.Email are kept in sync by the platform, but a match rule references fields on a single SObject. An Account-built rule that lists Contact.Email as a match field will fail validation; a B2B rule copy-pasted onto a B2C org that lists PersonEmail on a non-PA-enabled org will also fail. Email matching is case-insensitive by default when the matching method is Exact on an Email-type field; this is correct behaviour and should not be "fixed" with a LOWER() formula (see anti-patterns).

4. Phone matching: pick the right field, normalize first

PA exposes four phone fields on the Account side:

FieldTypical useSynced to Contact?
Account.PersonHomePhoneresidential landlineContact.HomePhone
Account.PersonMobilePhonemobileContact.MobilePhone
Account.Phoneprimary / "best" phoneContact.Phone (the Person Contact only)
Account.OtherPhonesecondaryContact.OtherPhone

Two pitfalls:

  1. Matching only on Account.Phone misses every PA whose primary phone is recorded as PersonMobilePhone.
  2. Phone equality is case-sensitive on the wire (the comparison is byte-equality after light normalization). +1 (555) 010-1234 does not equal 5550101234 does not equal +15550101234. Normalize before storing — Salesforce's standard Matching Rule has a built-in "Phone" matching method that strips formatting; use it. If you need cross-region matching, store an E.164 normalized value in a custom field and match on that.

4. Lead-to-PA convert — which rule fires?

When a Lead converts in a PA-enabled org, Salesforce evaluates the active Duplicate Rule on the target object:

Practical consequence: B2C orgs need their Account-targeted Duplicate Rule to be PA-aware (IsPersonAccount filter, PersonEmail match, etc.) specifically because that is the rule lead-convert will run.


Common Patterns

Pattern A — Consumer email + phone match rule for a pure B2C org

When to use: the org is exclusively Person Accounts (no B2B Accounts at all).

How it works: build the matching rule on Account, reference PersonEmail (Exact, email-aware) and a normalized phone (Exact via the standard Phone matching method). No IsPersonAccount filter is strictly required because every record is PA — but include it anyway as forward-compatibility insurance for the day someone adds a B2B record.

<MatchingRule fullName="Account.B2C_Person_Match">
    <booleanFilter>1 AND 2 AND 3</booleanFilter>
    <description>B2C consumer match: PersonEmail OR (PersonMobilePhone AND IsPersonAccount).</description>
    <label>B2C Person Match</label>
    <matchingRuleItems>
        <fieldName>PersonEmail</fieldName>
        <matchingMethod>Exact</matchingMethod>
    </matchingRuleItems>
    <matchingRuleItems>
        <fieldName>PersonMobilePhone</fieldName>
        <matchingMethod>Phone</matchingMethod>
    </matchingRuleItems>
    <matchingRuleItems>
        <fieldName>IsPersonAccount</fieldName>
        <matchingMethod>Exact</matchingMethod>
    </matchingRuleItems>
</MatchingRule>

Why not the alternative: building this on Contact would catch the Person Contact row but leave Account-UI dedupe and lead-convert blind, since lead-convert evaluates the Account-targeted rule.

Pattern B — Mixed B2B + B2C org, parallel match rules

When to use: the same org has Acme Corp (B2B) and Acme Smith (B2C PA) on the Account object.

How it works: two Account-targeted matching rules, both required to fire conditionally:

  1. B2B rule — IsPersonAccount = false, matches on Name + BillingStreet + Website.
  2. B2C rule — IsPersonAccount = true, matches on PersonEmail + PersonMobilePhone.

Each rule's first match-formula clause is the IsPersonAccount gate. The Duplicate Rule sits on top and chooses the right matching rule by IsPersonAccount.

Why not the alternative: a single rule that tries to OR together B2B Name and B2C PersonEmail will fire on every record-type boundary crossing — Acme Corp matched against Acme Smith because they share a tokenized "Acme".

Pattern C — Lead convert wired to the PA Duplicate Rule

When to use: every B2C org. Configure once at duplicate-rule deploy time.

How it works: the Account-targeted Duplicate Rule is set to actionOnInsert = Block (or Allow with alert), and lead-convert runs as an insert against Account. Confirm the rule's <isActive>true</isActive> and that it points at the PA-aware matching rule from Pattern A. Add Lead.IsConverted = false is NOT a thing — the lead-side rule is separate and lives under objects/Lead/duplicateRules/.

Why not the alternative: wiring the Lead-targeted rule and forgetting the Account-targeted one means convert succeeds even when the resulting PA collides with an existing Person Account — the dupe is silently created.


Decision Guidance

Match goal in a B2C / PA orgBuild rule onKey fieldsFilterNotes
PA email matchAccountPersonEmail (Exact, email-aware)IsPersonAccount = trueDo NOT use Contact.Email from an Account rule — wrong SObject. Email match is case-insensitive by default; do not wrap with LOWER().
PA phone matchAccountPersonMobilePhone and/or Phone (matching method Phone)IsPersonAccount = truePick the canonical phone field your data-entry path populates. Normalize to E.164 in a custom field if cross-region.
PA name+address fuzzyAccountFirstName + LastName (Fuzzy: First Name, Last Name) + BillingStreet (Fuzzy: Street)IsPersonAccount = true AND a record-type-name filterUse Salesforce-supplied fuzzy methods, not custom Levenshtein formulas. Add BillingPostalCode Exact as a tie-breaker to suppress cross-city false positives.
Lead-to-PA convert dedupeAccount (this is the rule that fires)Whatever the PA email/phone rule above definesIsPersonAccount = true (the converted record will be PA)The Lead-targeted rule does NOT fire on the synced Person Contact row. Make the Account-side rule the source of truth on convert.

  1. Confirm PA is enabled and inventory record counts — SELECT IsPersonAccount, Count(Id) FROM Account GROUP BY IsPersonAccount. If the B2B side is non-trivial, plan parallel rules (Pattern B). If B2C-only, plan Pattern A.
  2. List existing rules — read every file under objects/Account/duplicateRules/*.duplicateRule-meta.xml, objects/Lead/duplicateRules/*, and matchingRules/*.matchingRule-meta.xml. Note which reference PersonEmail vs Contact.Email, which carry an IsPersonAccount filter, which use the standard Phone matching method.
  3. Author the PA-aware matching rule — start from templates/duplicate-rule-person-account-edge-cases-template.md. Build on Account, target PersonEmail + a chosen Person phone field, gate with IsPersonAccount = true, add a record-type filter in mixed orgs.
  4. Wire the duplicate rule — link the matching rule, set actionOnInsert and actionOnUpdate deliberately (Block for hard duplicates, Allow with alert for soft), and confirm the rule will fire on lead-convert (it fires when convert inserts/updates Account).
  5. Validate with the skill-local checker — python3 scripts/check_duplicate_rule_person_account_edge_cases.py path/to/manifest. Resolve every P0 (missing IsPersonAccount filter on PA-enabled orgs, Contact.Email referenced from an Account rule, phone match without normalization).
  6. Test with deliberate fixtures — create one PA, one B2B Account with a similar name, one Lead that should convert into a duplicate PA. Confirm the rule blocks/alerts only the PA-vs-PA case and that lead-convert respects the rule.
  7. Document the GDPR / right-to-be-forgotten path — deletion of a PA cascades to the Person Contact automatically; capture this in the org's data-retention runbook. Do NOT model "delete the Contact only" as a privacy operation on PA.

Review Checklist

Run through these before marking the rule deployment complete:


Salesforce-Specific Gotchas

Non-obvious platform behaviours that cause real production problems:

  1. Match rule built on Contact does not catch PA dedupes on Account UI — the rule fires when a Person Contact is touched directly (rare), but the standard "Save" path on a PA writes to the Account side first; Account-targeted rules are the source of truth for PA dedupe.
  2. Lead-convert only runs the rule on the resulting SObject — convert that lands on PA fires the Account-targeted rule, not a (non-existent) "PA-targeted" rule. There is no PA-only Duplicate Rule type.
  3. PersonEmail is null on B2B Accounts — a match formula that references PersonEmail without an IsPersonAccount gate matches every B2B record against every other (null = null is FALSE in match logic, but the platform sometimes evaluates it surprisingly; gate explicitly).
  4. GDPR cascade is automatic — delete account of a PA deletes both the 001 and the 003 rows in one DML; manual delete contact on the Person Contact throws CANNOT_DELETE_LAST_DATED_CONVERSION style errors. Architect retention around the Account row.
  5. Time-zone of creation does not affect match — Duplicate Rules compare field values, not record-creation timestamps. Date-based dedupe needs an explicit Date or Datetime field in the match formula; it does not "drift" with the user's TZ.

Output Artifacts

ArtifactDescription
Account.<rule>.matchingRule-meta.xmlPA-aware matching rule on Account, gated by IsPersonAccount, targeting Person fields with the right matching methods
Account.<rule>.duplicateRule-meta.xmlDuplicate rule that consumes the matching rule above and is active for the lead-convert path
Optional parallel B2B matching ruleWhen the org is mixed; gated by IsPersonAccount = false
Checker runClean exit from scripts/check_duplicate_rule_person_account_edge_cases.py
Retention noteOne-paragraph runbook entry confirming PA delete cascades both rows

Package files