---
id: STATE-PFM-MTD-010
title: Link lifecycle
object: DOM-PFM-MTD-006
product_area: platform
module: metadata
revision: 1
status: draft
design_intent: confirmed
origin: 'PLAT-M6 D2 (Andrew, 2026-10-21: in force until it ends) and plan step 5, plan approved 2026-10-21 (services-platform:docs/associations.md#1a.
  In force (PLAT-M6 D2); docs/milestones/plans/PLAT-M6-plan.md)'
implementation_evidence: observed
source_sha:
  services-platform: ad3f6a6582843008ed0f5a17b131878d15829af1
  react-console: baf6dae382d6f41dce976c7f848667ccc37e076f
states:
- name: absent
  meaning: No stored link exists under this identity.
- name: in_force
  meaning: The end day is absent or has not passed in the workspace calendar; occupancy follows the link’s inclusive
    span.
- name: ended
  meaning: The inclusive end day has passed; the link remains as requested history and no longer occupies a place
    today.
- name: removed
  meaning: The stored link is gone, while captured record revisions retain its removal.
transitions:
- from: absent
  to: in_force
  trigger: A person or load creates a current link.
  guard: The type is active, endpoints exist and match, dates and primary validate, no self-link or place conflict
    exists, and the ordinary creation cap holds.
  side_effects:
  - Acquire endpoint/type occupancy locks, capture endpoint revisions and append the created event.
  who: Record Keeper (platform.associations.create) or a data job
  provenance: test
  source: services-platform:modules/associations/tests/integration/services/persistence/associations.integration.spec.js#refuses
    a record associated with itself
- from: in_force
  to: in_force
  trigger: A person or load replaces current link attributes.
  guard: The version matches, no update lock applies, endpoints and type stay fixed, and changed occupancy satisfies
    place checks; inactive types still allow maintenance.
  side_effects:
  - Capture business changes and emit an ended event only when the update newly supplies an end day.
  who: Record Keeper (platform.associations.update) or a data job
  provenance: test
  source: services-platform:modules/associations/tests/e2e/associations.e2e.spec.js#answers 400 to a replacement
    that names another record
- from: in_force
  to: ended
  trigger: An ending, removal of an open dated link, calendar passage or merge collision reaches an ended state.
  guard: Explicit ending checks dating, version, update lock and a valid nonconflicting span; calendar passage needs
    no write, and merge follows its own collision rules.
  side_effects:
  - A write stores the inclusive end day and revision; explicit endings emit an event, whereas calendar passage
    and merge do not.
  who: Record Keeper (platform.associations.update or .delete), the calendar, or CRM's merge
  provenance: test
  source: services-platform:modules/associations/tests/integration/services/persistence/associations.in-force.integration.spec.js#still
    holds the place on the end date it
- from: ended
  to: in_force
  trigger: A person or load reopens an ended link.
  guard: The version and lock checks pass and the widened or cleared end span has no place conflict.
  side_effects:
  - Capture the date correction without a business event; omitted load dates can reopen the link.
  who: Record Keeper (platform.associations.update) or a data job
  provenance: test
  source: 'services-platform:tests/acceptance/custom-objects/journeys/data-operations/CO-ACC-0402.spec.js#step 6:
    a CRW-1 line with only isPrimary replaces the association'
- from: in_force
  to: removed
  trigger: A direct removal or endpoint deletion removes a current link.
  guard: Direct removal needs no expected version but rejects any lock; dated links must already have an end day
    to be erased, otherwise removal sets today’s end.
  side_effects:
  - Remove the connection with revision and deleted event when the deletion path applies.
  who: Record Keeper (platform.associations.delete), or the deletion of an endpoint
  provenance: test
  source: services-platform:modules/associations/tests/integration/db/apply-endpoint-deletion.integration.spec.js#answers
    what endpointDeletionOutcome says for an open as
- from: ended
  to: removed
  trigger: A person or endpoint deletion removes an ended link.
  guard: Direct removal rejects any lock; endpoint deletion applies its transactional policy.
  side_effects:
  - Capture removal and append the deleted event.
  who: Record Keeper (platform.associations.delete), or the deletion of an endpoint
  provenance: test
  source: services-platform:modules/associations/tests/integration/services/persistence/associations.in-force.integration.spec.js#removes
    a link that has an end date when
- from: absent
  to: ended
  trigger: A person or load creates a link with an end day in the past.
  guard: Apply ordinary dated creation, endpoint, place and cap checks.
  side_effects:
  - Store the historical connection with creation revision and created event.
  who: Record Keeper (platform.associations.create) or a data job
  provenance: observed
  source: services-platform:modules/associations/services/persistence/associations.ts:createAssociation
invalid:
- from: ended
  to: ended
  why: A past-ended link cannot be explicitly ended again, and removal cannot be reversed under the deleted identity.
- from: removed
  to: in_force
  why: A past-ended link cannot be explicitly ended again, and removal cannot be reversed under the deleted identity.
concurrency: Creates and occupancy-changing writes acquire advisory locks before span checks. Replacements and explicit
  ends require a version; removal does not. Notes-only edits preserve historical merge overlaps. Opposite-direction
  custom links can deadlock and require caller retry.
invariants:
- INV-DOM-PFM-MTD-006-04
- INV-DOM-PFM-MTD-006-05
- INV-DOM-PFM-MTD-006-06
- INV-DOM-PFM-MTD-006-07
evidence:
- 'services-platform at ad3f6a65: modules/associations/services/persistence/associations.ts (createAssociation,
  updateAssociation, endAssociation, deleteAssociation, applyEndpointDeletion, advisory lock); rules.ts (spansOverlap);
  docs/associations.md §1a, §2, §6, §7; docs/record-history.md#What is open'
---

# Link lifecycle

## The Lifecycle

```mermaid
stateDiagram-v2
  [*] --> absent
  absent --> in_force: link, load or merge move
  in_force --> in_force: replace dates or notes
  in_force --> ended: end, delete dated open link, end day passes, merge collision
  ended --> in_force: end day cleared by correction
  in_force --> removed: delete undated, endpoint rule
  absent --> ended: create historical link
  ended --> removed: delete as correction, endpoint deletion
```

## Transitions Explained

Links can be created current or already ended. The inclusive end day determines calendar state, independent of whether an end date has just been stored. Clearing or extending the end can reopen a historical link; removing a dated link with any stored end date erases it as a correction.

## What Cannot Happen

A past-ended link cannot be explicitly ended again, and removal cannot be reversed under the deleted identity.

## Concurrency and Timing

Creates and occupancy-changing writes acquire advisory locks before span checks. Replacements and explicit ends require a version; removal does not. Notes-only edits preserve historical merge overlaps. Opposite-direction custom links can deadlock and require caller retry.
