---
id: CAP-CRM-DQ-016
title: Execute the approved merge atomically
feature: FEAT-CRM-DQ-04
product_area: crm
module: data-quality
revision: 1
status: draft
product_intent: confirmed
implementation_evidence: observed
supersedes:
  - CAP-CRM-MRG-016
source_sha:
  services-crm: a8d1364f1dd44b723bb5f9acba12f983e9ec8d69
  react-console: baf6dae382d6f41dce976c7f848667ccc37e076f
intent_sources:
  - 'services-crm:docs/merging/decisions.md#D1 (decided 2026-10-11: option 1)'
  - 'services-crm:docs/milestones/CRM-M4-duplicates-and-merge.md#D-31'
  - >-
    services-crm:docs/merging/decisions.md#D7 (decided 2026-10-12), D8 (decided 2026-10-12), D9 and D10
    (decided 2026-10-14)
  - >-
    services-crm:docs/merging/experience/functional-requirements.md#EXE-1 to EXE-7, CFM-1 to CFM-5, INT-3,
    INT-4 (package approved by Andrew 2026-10-13)
purpose: Administrators can run a reviewed merge with one accountable outcome and no partial record changes.
outcomes:
  - Record and participant changes commit together or roll back together.
  - Material changes after approval require renewed review.
  - Repeated submissions and overlapping work cannot absorb one identity twice.
concepts:
  - CON-CRM-DQ-004
actors:
  - ACT-CRM-009
  - ACT-CRM-005
surfaces:
  - console
  - api
  - job
permissions:
  - key: crm.contacts.merge
    where_checked: api
    scope: tenant
  - key: crm.organizations.merge
    where_checked: api
    scope: tenant
  - key: crm.merges.read
    where_checked: api
    scope: tenant
rules:
  - id: BR-CAP-CRM-DQ-016-01
    proved_by: [ATS-CRM-DQ-016-01, ATS-CRM-DQ-016-15, ATS-CRM-DQ-016-38, TST-CRM-DQ-183]
    statement: >-
      Execution rejects open required questions and blockers with their details, and accepts defaulted
      recommendations only when the request explicitly accepts them.
    kind: validation
    enforced_at:
      - api
      - service
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/merges/tests/e2e/merges.e2e.spec.js#names the conflict that is still open, so a
      console can take the reader to it
    origin: 'services-crm:docs/merging/README.md#Status (architecture and plan approved by Andrew 2026-10-11)'
  - id: BR-CAP-CRM-DQ-016-02
    proved_by: [ATS-CRM-DQ-016-02, ATS-CRM-DQ-016-13, ATS-CRM-DQ-016-38, TST-CRM-DQ-181, TST-CRM-DQ-183]
    statement: >-
      Before queueing, execution compares the current preview with the stored last-shown plan and rejects
      participant, dependent-row identity/version or chosen-source changes as stale with refreshed details,
      including requests without a submitted plan fingerprint.
    kind: validation
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/merges/tests/integration/services/merges/approval-binding.integration.spec.js#refuses
      an execution that submits no hash too, because the stored plan is the one last shown
    origin: 'services-crm:docs/milestones/CRM-M4-duplicates-and-merge.md#D-31'
  - id: BR-CAP-CRM-DQ-016-03
    proved_by: [ATS-CRM-DQ-016-03, ATS-CRM-DQ-016-38, TST-CRM-DQ-181]
    statement: >-
      Presentation wording, labels, duration estimates, survivor suggestions and carried dismissals do not
      affect approval binding.
    kind: policy
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/merges/tests/integration/services/merges/lookup-labels.integration.spec.js#answers
      the same material hash, a name being no part of what staff are approving
    origin: 'services-crm:docs/milestones/CRM-M4-duplicates-and-merge.md#D-31'
  - id: BR-CAP-CRM-DQ-016-04
    proved_by: [ATS-CRM-DQ-016-04, ATS-CRM-DQ-016-13, ATS-CRM-DQ-016-38, TST-CRM-DQ-181, TST-CRM-DQ-183]
    statement: >-
      Duplicate or simultaneous execution submissions return the same merge and job, with later requests
      reporting no new creation.
    kind: invariant
    enforced_at:
      - service
      - database
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/merges/tests/integration/services/merges/merge-settlement.integration.spec.js#answers
      both requests the same merge
    origin: 'services-crm:docs/merging/experience/functional-requirements.md#EXE-3'
  - id: BR-CAP-CRM-DQ-016-05
    proved_by: [ATS-CRM-DQ-016-05, ATS-CRM-DQ-016-15, ATS-CRM-DQ-016-19, ATS-CRM-DQ-016-22, ATS-CRM-DQ-016-23, ATS-CRM-DQ-016-38, TST-CRM-DQ-181, TST-CRM-DQ-183]
    statement: >-
      A durable background job exposes plan-check, record-lock, participant-change and merge-commit steps;
      merge and job creation commit together, and reconciliation starts or fails jobs whose start was lost.
    kind: lifecycle
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/merges/tests/integration/services/jobs/job-reconciliation.integration.spec.js#settles
      the job failed, rather than leaving it in flight for ever
    origin: 'services-crm:docs/merging/decisions.md#D9 (decided 2026-10-14)'
  - id: BR-CAP-CRM-DQ-016-06
    proved_by: [ATS-CRM-DQ-016-06, ATS-CRM-DQ-016-13, ATS-CRM-DQ-016-16, ATS-CRM-DQ-016-20, ATS-CRM-DQ-016-21, ATS-CRM-DQ-016-38, TST-CRM-DQ-181]
    statement: >-
      Execution re-plans under lock in one transaction and rolls back all changes on stale data,
      already-merged participants, blockers, participant refusals, ordinary survivor validation or internal
      failure, recording the reason.
    kind: invariant
    enforced_at:
      - service
      - database
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/merges/tests/integration/services/merges/approval-binding.integration.spec.js#fails
      the merge Stale inside its transaction rather than moving a task nobody approved
    origin: 'services-crm:docs/merging/decisions.md#D1 (decided 2026-10-11: option 1)'
  - id: BR-CAP-CRM-DQ-016-07
    proved_by: [ATS-CRM-DQ-016-07, ATS-CRM-DQ-016-13, ATS-CRM-DQ-016-38, TST-CRM-DQ-181]
    statement: >-
      Of overlapping merges sharing a participant, only one succeeds; the other reports already merged with
      record names and numbers, preventing two survivor destinations.
    kind: invariant
    enforced_at:
      - service
      - database
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/merges/tests/integration/services/merges/overlapping-merges.integration.spec.js#lets
      exactly one of the two merges happen
    origin: 'services-crm:docs/merging/README.md#Status (architecture and plan approved by Andrew 2026-10-11)'
  - id: BR-CAP-CRM-DQ-016-08
    proved_by: [ATS-CRM-DQ-016-08, ATS-CRM-DQ-016-38]
    statement: A record or owned detail locked against deletion also blocks merging.
    kind: validation
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/merges/tests/unit/services/planning/merge-decisions.spec.js#blocks the merge when a
      participant is locked
    origin: none
  - id: BR-CAP-CRM-DQ-016-09
    proved_by: [ATS-CRM-DQ-016-09, ATS-CRM-DQ-016-38]
    statement: >-
      Merges above 100,000 related writes are rejected; those above 10,000 are classified large and warn about
      moved records, estimated duration and editing waits.
    kind: validation
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/merges/tests/unit/services/planning/merge-decisions.spec.js#blocks a merge above
      the row ceiling of this release (decision D7)
    origin: 'services-crm:docs/merging/decisions.md#D7 (decided 2026-10-12) and D10 (decided 2026-10-14)'
  - id: BR-CAP-CRM-DQ-016-10
    proved_by: [ATS-CRM-DQ-016-10, ATS-CRM-DQ-016-17, ATS-CRM-DQ-016-18, ATS-CRM-DQ-016-38, TST-CRM-DQ-181]
    statement: >-
      Failure restores the plan's earlier status and decisions; another execution creates a new attempt while
      preserving the failed merge in history.
    kind: lifecycle
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/merges/tests/integration/services/merges/merge-settlement.integration.spec.js#returns
      the plan to the status it had before it was executed, rather than leaving it executing
    origin: 'services-crm:docs/merging/experience/functional-requirements.md#EXE-6'
  - id: BR-CAP-CRM-DQ-016-11
    proved_by: [ATS-CRM-DQ-016-36, ATS-CRM-DQ-016-38]
    statement: 'Workflow retries apply only to transient errors such as lock waits, excluding business refusals.'
    kind: policy
    enforced_at:
      - service
    must_prove: true
    provenance: observed
    source: 'services-crm:modules/merges/services/jobs/record-merge-job.ts:recordMergeWorkflow'
    origin: none
  - id: BR-CAP-CRM-DQ-016-12
    proved_by: [ATS-CRM-DQ-016-11, ATS-CRM-DQ-016-38, TST-CRM-DQ-181]
    statement: >-
      Every successful participant record receives an audit merge pointer naming the merge, survivor and
      participant role within the merge transaction.
    kind: lifecycle
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/merges/tests/integration/services/merges/merged-from-history.integration.spec.js#says
      who merged which record into which, and when: the merge pointer
    origin: 'services-crm:docs/merging/decisions.md#D8 (decided 2026-10-12)'
  - id: BR-CAP-CRM-DQ-016-13
    proved_by: [ATS-CRM-DQ-016-12, ATS-CRM-DQ-016-38, TST-CRM-DQ-183]
    statement: >-
      After a contact merge commits, a business event announces the survivor and absorbed identities to
      subscribers; organization merges publish no corresponding event.
    kind: lifecycle
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/merges/tests/e2e/business-events.e2e.spec.js#hands the subscriber the envelope of
      crm.contact.merged, about the survivor, in the merge’s tenant
    origin: none
  - id: BR-CAP-CRM-DQ-016-14
    proved_by: [ATS-CRM-DQ-016-37, ATS-CRM-DQ-016-38]
    statement: >-
      Console confirmation identifies the survivor and deleted records, states irreversibility, labels the
      outcome and polls every second for 30 seconds then every five seconds, while allowing staff to leave.
    kind: policy
    enforced_at:
      - console
    must_prove: true
    provenance: test
    source: 'react-console:acceptance/crm/playwright/journeys/merging/CRM-ACC-0253.spec.ts#CRM-ACC-0253'
    origin: 'services-crm:docs/merging/experience/functional-requirements.md#CFM-2'
  - id: BR-CAP-CRM-DQ-016-15
    proved_by: [ATS-CRM-DQ-016-14, ATS-CRM-DQ-016-38]
    statement: >-
      Estimated duration is the rounded sum of half a second and 1.7 milliseconds per estimated touched row,
      with negative row counts treated as zero and a minimum of one second.
    kind: calculation
    enforced_at:
      - service
    must_prove: true
    provenance: observed
    source: 'services-crm:modules/merges/services/planning/merge-engine.ts:expectedMergeSeconds'
    origin: 'services-crm:docs/merging/decisions.md#D10'
scenarios:
  - id: SC-CAP-CRM-DQ-016-01
    given: An approved preview exists before a task is added to an absorbed record.
    when: The administrator confirms execution.
    then: The stale response names the added task; renewed review allows it to move.
    provenance: test
    source: >-
      services-crm:modules/merges/tests/integration/services/merges/approval-binding.integration.spec.js#executes
      once the plan is looked at again and confirmed, moving the task the reviewer saw
  - id: SC-CAP-CRM-DQ-016-02
    given: Two administrators submit overlapping merges and one repeats confirmation.
    when: The jobs attempt execution.
    then: >-
      One merge succeeds, the conflicting merge reports already merged and repeated confirmation creates no
      extra attempt.
    provenance: test
    source: 'react-console:acceptance/crm/playwright/journeys/merging/CRM-ACC-0263.spec.ts#CRM-ACC-0263'
edge_cases:
  - id: SC-CAP-CRM-DQ-016-03
    given: A merge has a durable job whose execution never starts.
    when: Job reconciliation settles the lost work.
    then: The merge records internal failure and its plan becomes available for another attempt.
    provenance: test
    source: >-
      services-crm:modules/merges/tests/integration/services/jobs/job-reconciliation.integration.spec.js#returns
      the plan to the status it had before it was executed, so it can be executed again (gap G4)
constraints:
  - >-
    Rows changed by execution remain locked through commit, so simultaneous ordinary edits wait for the
    transaction.
depends_on:
  - CAP-CRM-DQ-013
  - CAP-CRM-DQ-015
related:
  - CAP-CRM-DQ-017
  - CAP-CRM-DQ-018
  - CAP-CRM-DQ-019
journeys:
  - JNY-CRM-DQ-002
  - JNY-CRM-DQ-003
open_questions:
  - OQ-CRM-DQ-005
evidence:
  - repository: services-crm
    source_sha: a8d1364f1dd44b723bb5f9acba12f983e9ec8d69
    paths:
      - docs/merging/README.md
      - docs/merging/decisions.md
      - docs/merging/experience/functional-requirements.md
      - docs/milestones/CRM-M4-duplicates-and-merge.md
      - modules/merges/db/record-merge-jobs.table.ts
      - modules/merges/services/jobs/record-merge-job.ts
      - modules/merges/services/planning/merge-engine.ts
      - modules/merges/tests/e2e/business-events.e2e.spec.js
      - modules/merges/tests/e2e/merges.e2e.spec.js
      - modules/merges/tests/integration/services/jobs/job-reconciliation.integration.spec.js
      - modules/merges/tests/integration/services/merges/approval-binding.integration.spec.js
      - modules/merges/tests/integration/services/merges/lookup-labels.integration.spec.js
      - modules/merges/tests/integration/services/merges/merge-settlement.integration.spec.js
      - modules/merges/tests/integration/services/merges/merged-from-history.integration.spec.js
      - modules/merges/tests/integration/services/merges/overlapping-merges.integration.spec.js
      - modules/merges/tests/unit/services/planning/merge-decisions.spec.js
    note: 'Static examination connects rule claims to implementation, declared checks and the cited tests.'
  - repository: react-console
    source_sha: baf6dae382d6f41dce976c7f848667ccc37e076f
    paths:
      - acceptance/crm/playwright/journeys/merging/CRM-ACC-0253.spec.ts
      - acceptance/crm/playwright/journeys/merging/CRM-ACC-0263.spec.ts
    note: 'Static examination connects rule claims to implementation, declared checks and the cited tests.'
preconditions: []
postconditions: []
controls: [TC-AVL-004, TC-AZ-001, TC-AZ-002, TC-AZ-003, TC-AZ-004, TC-AZ-005, TC-AZ-006, TC-DAT-001, TC-DAT-002, TC-DAT-003, TC-DAT-004, TC-IAM-001, TC-IAM-002, TC-IAM-004, TC-IAM-005, TC-IAM-006, TC-LOG-001, TC-LOG-002, TC-LOG-003, TC-LOG-004, TC-LOG-005, TC-MON-001, TC-MON-002, TC-PI-001, TC-PI-002, TC-PI-003, TC-PI-004, TC-PI-005, TC-PRV-001, TC-PRV-002, TC-PRV-003, TC-TEN-001, TC-TEN-002, TC-TEN-003]
exercised_by: [FAS-CRM-DQ-03-05, FAS-CRM-DQ-04-01, FAS-CRM-DQ-04-03, FAS-CRM-DQ-04-04, FAS-CRM-DQ-04-05]
suites: [TST-CRM-DQ-181, TST-CRM-DQ-183]
---

# Execute the approved merge atomically

## Purpose

Administrators can run a reviewed merge with one accountable outcome and no partial record changes.

## How It Works

The administrator confirms a ready preview and any recommendations it uses. The service compares the current material plan with the last preview before queueing work. Changed participants, moved rows or chosen source values require renewed review. Repeated submission returns the same queued merge and job. The background worker reports four progress steps. It locks the records, recomputes the plan and performs CRM and participant changes in one transaction. A stale plan, conflict or execution refusal rolls the transaction back. Overlapping merges cannot both absorb the same record. Failure returns the plan to its previous workable state while retaining the failed attempt. Only transient failures receive workflow retries. Success writes audit pointers and publishes the contact merge event after commit.

## Relationships

Planning defines the approved result. Related-record policies govern every reference movement. History preserves successful and failed attempts, and Platform owns the durable-job framework.
