---
id: CAP-PFM-MTD-018
title: Maintain Dated Links Between Records
feature: FEAT-PFM-MTD-06
product_area: platform
module: metadata
revision: 1
status: draft
product_intent: confirmed
implementation_evidence: observed
supersedes:
  - CAP-CO-ASC-002
  - CAP-CO-ASC-003
  - CAP-CO-ASC-004
  - CAP-CO-SEC-006
  - CAP-CO-XAP-013
source_sha:
  services-platform: ad3f6a6582843008ed0f5a17b131878d15829af1
  react-console: baf6dae382d6f41dce976c7f848667ccc37e076f
intent_sources:
  - >-
    services-platform:docs/milestones/plans/PLAT-M6-plan.md#3. How it works for the user; 5. D2 (Andrew,
    2026-10-21: in force until it ends)
  - >-
    services-platform:docs/milestones/plans/PLAT-M6-plan.md#9. Acceptance scenarios PLAT-M6-S03 to S07, S10,
    S12
  - 'services-platform:docs/proposals/custom-objects/09-decisions.md#D-04, D-12, D-16'
purpose: >-
  Record keepers need to express current and past connections without losing their dates. Relationship links
  hold those facts under administrator-defined types.
outcomes:
  - Record keepers need to express current and past connections without losing their dates.
  - Both endpoint cards expose permitted actions and the option to inspect ended links.
actors:
  - ACT-PFM-008
  - ACT-PFM-009
surfaces:
  - console
  - api
preconditions:
  - The type is active and both endpoints belong to the workspace.
postconditions: []
permissions:
  - key: platform.associations.read
    where_checked: api
    scope: tenant
  - key: platform.associations.create
    where_checked: api
    scope: tenant
  - key: platform.associations.update
    where_checked: api
    scope: tenant
  - key: platform.associations.delete
    where_checked: api
    scope: tenant
rules:
  - id: BR-CAP-PFM-MTD-018-01
    proved_by: [ATS-PFM-MTD-012-18, ATS-PFM-MTD-012-19, ATS-PFM-MTD-017-09, ATS-PFM-MTD-018-01, TST-PFM-MTD-036, TST-PFM-MTD-045]
    statement: >-
      Creating a link requires an active workspace type and two existing compatible endpoints, rejects
      self-links and fills an omitted endpoint from the record route.
    kind: validation
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-platform:modules/associations/tests/integration/services/persistence/associations.integration.spec.js#refuses
      a record associated with itself
    origin: none
  - id: BR-CAP-PFM-MTD-018-02
    proved_by: [ATS-PFM-MTD-018-02, TST-PFM-MTD-020, TST-PFM-MTD-044]
    statement: >-
      A link remains in force through its inclusive end date in the workspace calendar, or indefinitely when
      no end date exists.
    kind: calculation
    enforced_at:
      - service
      - database
    must_prove: true
    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 itself when that date is today
    origin: >-
      services-platform:docs/milestones/plans/PLAT-M6-plan.md#5. D2 (Andrew, 2026-10-21: "In force until it
      ends")
  - id: BR-CAP-PFM-MTD-018-03
    proved_by: [ATS-PFM-MTD-018-02, ATS-PFM-MTD-018-03, TST-PFM-MTD-018, TST-PFM-MTD-020, TST-PFM-MTD-044, TST-PFM-MTD-045]
    statement: >-
      Overlapping date spans cannot occupy a duplicate pair, a cardinality-limited endpoint or the primary
      place of a record and type; unbounded ends and inclusive dates determine conflicts while adjacent spans
      are allowed.
    kind: validation
    enforced_at:
      - service
      - database
    must_prove: true
    provenance: test
    source: >-
      services-platform:modules/associations/tests/integration/services/persistence/associations.in-force.integration.spec.js#refuses
      a second link of a one-place type that starts today, naming the link whose end date has not passed
    origin: 'services-platform:docs/milestones/plans/PLAT-M6-plan.md#5. D2'
  - id: BR-CAP-PFM-MTD-018-04
    proved_by: [ATS-PFM-MTD-018-04, TST-PFM-MTD-044, TST-PFM-MTD-045]
    statement: >-
      Concurrent writers cannot both occupy a conflicting relationship place, and the losing write receives a
      conflict refusal.
    kind: invariant
    enforced_at:
      - service
      - database
    must_prove: true
    provenance: test
    source: >-
      services-platform:modules/associations/tests/integration/services/persistence/associations.in-force.integration.spec.js#refuses
      one of two writers of overlapping links of one pair, as a refusal and not a database error
    origin: none
  - id: BR-CAP-PFM-MTD-018-05
    proved_by: [ATS-PFM-MTD-018-05, TST-PFM-MTD-045]
    statement: >-
      Only dated types accept calendar dates, an end cannot precede its start and only primary-enabled types
      accept primary links.
    kind: validation
    enforced_at:
      - service
      - database
    must_prove: true
    provenance: test
    source: >-
      services-platform:modules/associations/tests/integration/services/persistence/associations.integration.spec.js#refuses
      an end before the start on a dated type
    origin: none
  - id: BR-CAP-PFM-MTD-018-06
    proved_by: [ATS-PFM-MTD-018-06, TST-PFM-MTD-036, TST-PFM-MTD-045]
    statement: >-
      Type and endpoint identities remain fixed, while versioned replacements can change dates, primary
      designation, position and notes bounded to four thousand characters.
    kind: invariant
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-platform:modules/associations/tests/e2e/associations.e2e.spec.js#answers 400 to a replacement
      that names another record
    origin: 'services-platform:docs/proposals/custom-objects/09-decisions.md#D-12 (fixed link attributes)'
  - id: BR-CAP-PFM-MTD-018-07
    proved_by: [ATS-PFM-MTD-018-07, ATS-PFM-MTD-018-13, TST-PFM-MTD-019, TST-PFM-MTD-045]
    statement: >-
      Ending a dated link uses a supplied day or the workspace today and refuses already-ended, invalid or
      conflicting spans; ending an undated link is refused in favour of removal.
    kind: lifecycle
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-platform:modules/associations/tests/integration/services/persistence/associations.integration.spec.js#refuses
      an end before the association started, leaving it open
    origin: none
  - id: BR-CAP-PFM-MTD-018-08
    proved_by: [ATS-PFM-MTD-018-08, TST-PFM-MTD-045]
    statement: >-
      Removing a dated link without an end date ends it today, removing a dated link with an end date deletes
      it as a correction, and undated links are deleted.
    kind: lifecycle
    enforced_at:
      - service
    must_prove: true
    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 it is deleted, correcting history, and ends one with none
    origin: 'services-platform:docs/milestones/plans/PLAT-M6-plan.md#3. How it works (step 5)'
  - id: BR-CAP-PFM-MTD-018-09
    proved_by: [ATS-PFM-MTD-018-09, TST-PFM-MTD-044, TST-PFM-MTD-045]
    statement: >-
      Creation permits one thousand links per record and type on each side, counting current and ended links
      together, while merge can exceed that limit.
    kind: validation
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-platform:modules/associations/tests/integration/services/persistence/associations.integration.spec.js#refuses
      an association past the per-record limit of the type, writing nothing
    origin: 'services-platform:docs/proposals/custom-objects/09-decisions.md#D-16'
  - id: BR-CAP-PFM-MTD-018-10
    proved_by: [ATS-PFM-MTD-018-10, TST-PFM-MTD-017, TST-PFM-MTD-024, TST-PFM-MTD-036]
    statement: >-
      Link lists use the current endpoint label and opposite record name, default to in-force links, offer
      ended history when requested and isolate workspaces.
    kind: policy
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-platform:modules/associations/tests/e2e/associations.e2e.spec.js#lists it on the organization’s
      path in the reverse direction, naming the contact
    origin: none
  - id: BR-CAP-PFM-MTD-018-11
    proved_by: [ATS-PFM-MTD-018-11, TST-PFM-MTD-026, TST-PFM-MTD-036]
    statement: >-
      Relationship cards on custom, contact and organization pages expose permitted link actions and ended
      history, allowing standard users to maintain links while readers receive no actions.
    kind: permission
    enforced_at:
      - console
      - api
    must_prove: true
    provenance: test
    source: >-
      react-console:acceptance/custom-objects/playwright/journeys/PLAT-M6-S12.spec.ts#PLAT-M6-S12: a standard
      user links, ends and removes but has no Relationship Types screen, and a read-only colleague sees no
      actions
    origin: 'services-platform:docs/milestones/plans/PLAT-M6-plan.md#PLAT-M6-S12'
  - id: BR-CAP-PFM-MTD-018-12
    proved_by: []
    statement: General relationships leave contact-to-organization affiliations under the separate CRM model.
    kind: policy
    enforced_at:
      - service
    must_prove: false
    provenance: documented
    source: 'services-platform:docs/associations.md#Associations'
    origin: 'services-platform:docs/proposals/custom-objects/09-decisions.md#D-04 (approved 2026-10-18)'
  - id: BR-CAP-PFM-MTD-018-13
    proved_by: [ATS-PFM-MTD-018-12]
    statement: >-
      Update locks refuse edits and ending, any lock refuses removal, and deactivation does not prevent
      maintenance of existing unlocked links.
    kind: policy
    enforced_at:
      - service
    must_prove: true
    provenance: observed
    source: 'services-platform:modules/associations/services/persistence/associations.ts:updateAssociation'
    origin: none
scenarios:
  - id: SC-CAP-PFM-MTD-018-01
    given: Tom charter on Osprey ends on the last day of November.
    when: 'In October, request Maria charter from today and then from the first day of December.'
    then: 'The overlapping request identifies Tom dates and is refused, while the adjacent charter succeeds.'
    provenance: documented
    source: 'services-platform:docs/associations.md#1a. In force (PLAT-M6 D2)'
edge_cases:
  - id: SC-CAP-PFM-MTD-018-02
    given: A member has two sponsor links.
    when: Designate the second sponsor primary in the console.
    then: Primary designation transfers to the second sponsor.
    provenance: test
    source: >-
      react-console:acceptance/custom-objects/playwright/journeys/PLAT-M6-S07.spec.ts#PLAT-M6-S07: marking a
      second sponsor primary moves the primary flag
constraints: []
depends_on:
  - CAP-PFM-MTD-017
related:
  - CAP-PFM-MTD-014
  - CAP-PFM-MTD-015
  - CAP-PFM-MTD-019
journeys:
  - JNY-PFM-MTD-004
open_questions: []
concepts:
  - CON-PFM-MTD-006
evidence:
  - >-
    services-platform at ad3f6a6582843008ed0f5a17b131878d15829af1:
    modules/associations/aws/api/associations.routes.ts , services/persistence/associations.ts, rules.ts,
    paging.ts, db/associations.table.ts, association-change-events.table.ts; tests
    e2e/associations.e2e.spec.js, integration/services/persistence/associations*.integration.spec.js,
    integration/db/associations.table.integration.spec.js, unit/rules.spec.ts, unit/paging.spec.ts;
    tests/acceptance/custom-objects/journeys/associations/**; docs/associations.md §1a to §4.
  - >-
    react-console at baf6dae382d6f41dce976c7f848667ccc37e076f: components/custom-objects/related-records.tsx,
    related-link-dialogs.tsx, owned-records.tsx; Playwright PLAT-M6-S03 to S07, S10, S12.
controls: [TC-AZ-001, TC-AZ-002, TC-AZ-003, TC-AZ-004, TC-DAT-001, TC-DAT-002, TC-DAT-003, TC-DAT-004, TC-LOG-002, TC-LOG-003, TC-LOG-004, TC-LOG-005, TC-PI-001, TC-PI-003, TC-PI-005, TC-PRV-001, TC-PRV-002, TC-TEN-001, TC-TEN-002, TC-TEN-003, TC-VEN-001, TC-VEN-002, TC-VEN-003]
exercised_by: [FAS-PFM-MTD-06-01, FAS-PFM-MTD-06-02, FAS-PFM-MTD-06-03, FAS-PFM-MTD-06-04, FAS-PFM-MTD-06-05]
suites: [TST-PFM-MTD-017, TST-PFM-MTD-018, TST-PFM-MTD-019, TST-PFM-MTD-020, TST-PFM-MTD-024, TST-PFM-MTD-026, TST-PFM-MTD-036, TST-PFM-MTD-044, TST-PFM-MTD-045]
---

# Maintain Dated Links Between Records

## Purpose

Record keepers need to express current and past connections without losing their dates. Relationship links hold those facts under administrator-defined types.

## How It Works

A staff member starts from either record and chooses its counterpart. The service checks the type, endpoints and available relationship place. Dating and primary rules determine whether the proposed link conflicts with existing links. Staff can correct its own attributes at the version read. Ending preserves a dated connection, while removing can end an open connection or delete a historical correction. Both endpoint cards expose permitted actions and the option to inspect ended links. Locks remain effective when the type is deactivated, while unlocked existing links can still be maintained.

## Relationships

Relationship definitions determine link shape. Endpoint deletion and merge apply the corresponding policies, and history captures link changes.
