---
id: STATE-CRM-DQ-004
title: Merge plan lifecycle
object: DOM-CRM-DQ-003
product_area: crm
module: data-quality
revision: 1
status: draft
design_intent: proposed
implementation_evidence: observed
source_sha:
  services-crm: a8d1364f1dd44b723bb5f9acba12f983e9ec8d69
states:
  - name: needs_decisions
    meaning: At least one required question lacks an answer.
  - name: ready
    meaning: >-
      No blocker or unanswered required question remains; defaults still need
      execution acceptance.
  - name: blocked
    meaning: >-
      A policy or execution condition prevents merging regardless of field
      answers.
  - name: executing
    meaning: An execution request has created a pending or active attempt.
  - name: executed
    meaning: The business merge succeeded and the plan retains its settled preview.
  - name: expired
    meaning: >-
      The stored expiry instant has closed the plan; implementation measures it
      from creation.
transitions:
  - from: needs_decisions
    to: ready
    trigger: Answers or current data remove the last required question.
    guard: Changes use a current version and valid participant choices.
    side_effects:
      - 'persist refreshed preview, material hash and standing'
    who: >-
      Authorized administrator with the type read and merge permissions; any
      reader of an open plan
    provenance: test
    source: >-
      services-crm:modules/merges/tests/e2e/merges.e2e.spec.js#refuses the
      execution request as a plan that is not ready
  - from: ready
    to: needs_decisions
    trigger: Changed participants or data introduce an unanswered question.
    guard: Recomputation discovers the new conflict.
    side_effects:
      - persist the recomputed preview
      - report decisions that no longer apply
    who: 'Authorized administrator, or any read or validation'
    provenance: test
    source: >-
      services-crm:modules/merges/tests/unit/services/planning/merge-decisions.spec.js#is
      dropped and reported when the plan is simply read again after the data
      moved on
  - from: ready
    to: blocked
    trigger: Current planning discovers a blocking condition.
    guard: Recomputation identifies a blocker.
    side_effects:
      - persist the blocked preview
    who: 'Each read, change or validation'
    provenance: test
    source: >-
      services-crm:modules/merges/tests/unit/services/planning/merge-decisions.spec.js#blocks
      the merge for a column the registry declares no policy for
  - from: blocked
    to: ready
    trigger: A blocking condition clears and no required question remains.
    guard: No blocker or required unanswered conflict remains.
    side_effects:
      - persist the ready preview
    who: 'Each read, change or validation'
    provenance: test
    source: >-
      services-crm:modules/merges/tests/e2e/merges.e2e.spec.js#answers the
      validation with the plan as it is now, ready to execute
  - from: needs_decisions
    to: blocked
    trigger: Planning discovers a blocker before questions are settled.
    guard: Recomputation identifies a blocker.
    side_effects:
      - persist the blocked preview
    who: 'Each read, change or validation'
    provenance: test
    source: >-
      services-crm:modules/merges/tests/unit/services/planning/merge-decisions.spec.js#blocks
      the merge for a column the registry declares no policy for
  - from: blocked
    to: needs_decisions
    trigger: A blocker clears while unanswered questions remain.
    guard: Recomputation still needs a required answer.
    side_effects:
      - persist the preview awaiting decisions
    who: 'Each read, change or validation'
    provenance: test
    source: >-
      services-crm:modules/merges/tests/e2e/merges.e2e.spec.js#answers the
      validation with the plan as it is now, ready to execute
  - from: ready
    to: executing
    trigger: An execution request passes submission checks.
    guard: >-
      Defaults are accepted, readiness and size checks pass, hashes match and
      participants remain available.
    side_effects:
      - atomically create the queued attempt and job
      - start workflow execution after commit
    who: Authorized administrator with the type read and merge permissions
    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
  - from: executing
    to: executed
    trigger: Successful execution commits the business transaction.
    guard: The attempt holds its required participants under lock.
    side_effects:
      - mark the plan executed within that transaction
    who: merge task
    provenance: observed
    source: >-
      services-crm:modules/merges/services/merges/merge-execution.ts:executeMerge
  - from: executing
    to: ready
    trigger: Executor or reconciliation settles a failed ready attempt.
    guard: The plan remains executing and its prior preview was ready.
    side_effects:
      - fail the attempt and reopen the plan atomically
      - retain decisions for the next recomputation
    who: merge task; the job reconciler
    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
  - from: executing
    to: needs_decisions
    trigger: Failure settlement finds a different prior preview state.
    guard: The plan still has executing standing.
    side_effects:
      - restore the stored working state or the fallback needing decisions
    who: merge task; the job reconciler
    provenance: observed
    source: >-
      services-crm:modules/merges/services/merges/merge-settlement.ts:statusBeforeExecution
  - from: needs_decisions
    to: expired
    trigger: The expiry sweep finds an overdue working preview.
    guard: Only executed and already-expired plans are excluded.
    side_effects:
      - mark expiry and advance the version
    who: 'job reconciler, every five minutes'
    provenance: test
    source: >-
      services-crm:modules/merges/tests/integration/services/merges/merge-plan-expiry.integration.spec.js#expires
      a blocked plan too, a plan nobody cleared being as abandoned as one nobody
      decided
  - from: ready
    to: expired
    trigger: The expiry sweep finds an overdue ready preview.
    guard: Only executed and already-expired plans are excluded.
    side_effects:
      - mark expiry
    who: job reconciler
    provenance: test
    source: >-
      services-crm:modules/merges/tests/integration/services/merges/merge-plan-expiry.integration.spec.js#expires
      a blocked plan too, a plan nobody cleared being as abandoned as one nobody
      decided
  - from: blocked
    to: expired
    trigger: The expiry sweep finds an overdue blocked preview.
    guard: Only executed and already-expired plans are excluded.
    side_effects:
      - mark expiry
    who: job reconciler
    provenance: test
    source: >-
      services-crm:modules/merges/tests/integration/services/merges/merge-plan-expiry.integration.spec.js#expires
      a blocked plan too, a plan nobody cleared being as abandoned as one nobody
      decided
  - from: executing
    to: expired
    trigger: The expiry sweep finds an overdue active attempt’s plan.
    guard: Executing standing is not excluded by the sweep.
    side_effects:
      - mark expiry while the attempt may still execute
    who: job reconciler
    provenance: observed
    source: >-
      services-crm:modules/merges/services/persistence/record-merge-plans.ts:expireRecordMergePlans
invalid:
  - from: executed
    to: ready
    why: Successful plans are not reopened for another combination.
  - from: expired
    to: ready
    why: Expired plans require replacement rather than revival.
  - from: executing
    to: executing
    why: >-
      Repeated execution returns the existing active attempt instead of creating
      another.
  - from: blocked
    to: executing
    why: No override permits execution with a blocker.
concurrency: >-
  Updates compare optimistic versions, including writes made by open-plan reads.
  Concurrent execution inserts are protected by the partial unique attempt
  index; a losing request returns the attempt that already exists. Qualified
  administrators share the plan and can make one another’s changes stale.
invariants:
  - INV-DOM-CRM-DQ-003-03
  - INV-DOM-CRM-DQ-003-06
  - INV-DOM-CRM-DQ-003-07
  - INV-DOM-CRM-DQ-003-08
  - INV-DOM-CRM-DQ-003-09
  - INV-DOM-CRM-DQ-003-12
evidence:
  - >-
    services-crm at a8d1364f: modules/merges/db/record-merge-plans.table.ts;
    services/merges/merge-plans.ts, merge-settlement.ts, merge-plan-expiry.ts;
    services/persistence/record-merge-plans.ts;
    services/merges/merge-execution.ts; docs/merging/api.md §4 (lifecycle).
---

# Merge Plan Lifecycle

## The Lifecycle

```mermaid
stateDiagram-v2
  state "needs_decisions" as needs_decisions
  state "ready" as ready
  state "blocked" as blocked
  state "executing" as executing
  state "executed" as executed
  state "expired" as expired
  needs_decisions --> ready: Answers or current data remove the last required question
  ready --> needs_decisions: Changed participants or data introduce an unanswered question
  ready --> blocked: Current planning discovers a blocking condition
  blocked --> ready: A blocking condition clears and no required question remains
  needs_decisions --> blocked: Planning discovers a blocker before questions are settled
  blocked --> needs_decisions: A blocker clears while unanswered questions remain
  ready --> executing: An execution request passes submission checks
  executing --> executed: Successful execution commits the business transaction
  executing --> ready: Executor or reconciliation settles a failed ready attempt
  executing --> needs_decisions: Failure settlement finds a different prior preview state
  needs_decisions --> expired: The expiry sweep finds an overdue working preview
  ready --> expired: The expiry sweep finds an overdue ready preview
  blocked --> expired: The expiry sweep finds an overdue blocked preview
  executing --> expired: The expiry sweep finds an overdue active attempt’s plan
```

## Transitions Explained

The planner derives the three working standings on read, edit or validation. Accepted execution creates an attempt and marks the plan executing; successful business commit settles it. Failure reopens eligible executing plans with decisions retained.

## What Cannot Happen

Working edits are refused for executing, executed or expired plans. Blockers have no override, and expiry prevents further execution.

## Concurrency and Timing

The five-minute sweep processes at most one hundred plans per workspace under a shared time budget. It does not extend lifetime on edits or exclude executing plans, so it can expire active work contrary to the approved last-change rule. Settled working copies are not cleaned up.
