---
id: CAP-PFM-MTD-012
title: Maintain Valid Custom Records
feature: FEAT-PFM-MTD-05
product_area: platform
module: metadata
revision: 1
status: draft
product_intent: confirmed
implementation_evidence: observed
supersedes:
  - CAP-CO-REC-001
  - CAP-CO-REC-002
  - CAP-CO-REC-003
  - CAP-CO-REC-004
  - CAP-CO-REC-005
  - CAP-CO-REC-009
  - CAP-CO-REC-010
  - CAP-CO-REC-011
  - CAP-CO-REC-012
  - CAP-CO-REC-013
  - CAP-CO-REC-015
  - CAP-CO-REC-016
  - CAP-CO-REC-017
  - CAP-CO-REC-022
  - CAP-CO-REC-023
  - CAP-CO-REC-024
  - CAP-CO-XAP-002
  - CAP-CO-XAP-006
source_sha:
  services-platform: ad3f6a6582843008ed0f5a17b131878d15829af1
  react-console: baf6dae382d6f41dce976c7f848667ccc37e076f
intent_sources:
  - >-
    services-platform:docs/milestones/PLAT-M5-custom-records.md#Scope (the record store), PLAT-M5 D3, D4, D5,
    D9, D17
  - 'services-platform:docs/milestones/plans/PLAT-M5-plan.md#PLAT-M5-S03, S04, S05, S06'
  - 'services-platform:docs/proposals/custom-objects/09-decisions.md#D-01, D-02'
purpose: >-
  Record keepers need to capture and correct the organization information without breaking its definitions.
  The record store applies the object contract to every write.
outcomes:
  - Record keepers need to capture and correct the organization information without breaking its definitions.
  - A rejected write identifies the field that needs attention without returning the rejected value.
actors:
  - ACT-PFM-008
  - ACT-PFM-009
  - ACT-PFM-012
surfaces:
  - console
  - api
preconditions:
  - Writes target an active object; identity reads can address records of other object states.
postconditions:
  - Record mutations leave transactional audits; individual API reads record a view.
permissions:
  - key: platform.custom-records.read
    where_checked: api
    scope: record
  - key: platform.custom-records.create
    where_checked: api
    scope: record
  - key: platform.custom-records.update
    where_checked: api
    scope: record
rules:
  - id: BR-CAP-PFM-MTD-012-01
    proved_by: [ATS-PFM-MTD-008-12, ATS-PFM-MTD-012-01, TST-PFM-MTD-051, TST-PFM-MTD-065]
    statement: >-
      Record writes require an active object; records of draft or archived objects remain readable by identity
      with their links, reject writes and cannot be reached through another object path.
    kind: lifecycle
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-platform:modules/custom-objects/tests/e2e/custom-object-records.e2e.spec.js#refuses every write
      of it with 409 and changes nothing
    origin: >-
      services-platform:docs/milestones/PLAT-M5-custom-records.md#PLAT-M5 D3 (Andrew, 2026-10-19: readable,
      read-only)
  - id: BR-CAP-PFM-MTD-012-02
    proved_by: [ATS-PFM-MTD-012-02, TST-PFM-MTD-065]
    statement: >-
      Property validation uses live definitions and typed conversion, names invalid or unavailable properties
      without disclosing rejected values and uses the object label in its refusal.
    kind: validation
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-platform:modules/custom-objects/tests/integration/services/persistence/custom-object-records.integration.spec.js#names
      the object’s label in that refusal, never "custom records" (D4)
    origin: 'services-platform:docs/milestones/PLAT-M5-custom-records.md#PLAT-M5 D4'
  - id: BR-CAP-PFM-MTD-012-03
    proved_by: [ATS-PFM-MTD-012-03, TST-PFM-MTD-065]
    statement: API-required properties need a value at creation and reject attempts to clear them.
    kind: validation
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-platform:modules/custom-objects/tests/integration/services/persistence/custom-object-records.integration.spec.js#refuses
      a record without a property required by the API
    origin: none
  - id: BR-CAP-PFM-MTD-012-04
    proved_by: [ATS-PFM-MTD-012-04, ATS-PFM-MTD-012-15, TST-PFM-MTD-004, TST-PFM-MTD-065, TST-PFM-MTD-076]
    statement: >-
      Visibility validation rejects non-null submitted changes to hidden properties while retaining untouched
      stored values; conditional requirement checks resulting visible values, and unknown evaluations neither
      require nor hide.
    kind: validation
    enforced_at:
      - service
      - console
    must_prove: true
    provenance: observed
    source: >-
      services-platform:modules/custom-objects/services/persistence/custom-object-records.ts:requireRecordRules
    origin: 'services-platform:docs/milestones/plans/PLAT-M5-plan.md#PLAT-M5-S04'
  - id: BR-CAP-PFM-MTD-012-05
    proved_by: [ATS-PFM-MTD-012-05, TST-PFM-MTD-065]
    statement: >-
      Creation fills omitted, empty or null properties from compatible defaults using literals, workspace
      dates or caller contact, while supplied values override defaults.
    kind: calculation
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-platform:modules/custom-objects/tests/integration/services/persistence/custom-object-records.integration.spec.js#defaults
      a date to the tenant’s today, and a text to its literal
    origin: none
  - id: BR-CAP-PFM-MTD-012-06
    proved_by: [ATS-PFM-MTD-012-06, TST-PFM-MTD-065]
    statement: >-
      References must resolve to a record of their target type in the workspace, with custom-object targets
      requiring unarchived records of the specified object.
    kind: validation
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-platform:modules/custom-objects/tests/integration/services/persistence/custom-object-records.integration.spec.js#refuses
      a manufacturer that is a boat
    origin: none
  - id: BR-CAP-PFM-MTD-012-07
    proved_by: [ATS-PFM-MTD-006-10, ATS-PFM-MTD-012-07, TST-PFM-MTD-005, TST-PFM-MTD-065]
    statement: >-
      Unique-property reservations reject duplicate values without saving, name the holder, normalize text
      case and surrounding whitespace and release a reservation when its value changes.
    kind: validation
    enforced_at:
      - service
      - database
      - console
    must_prove: true
    provenance: test
    source: >-
      services-platform:modules/custom-objects/tests/integration/services/persistence/custom-object-records.integration.spec.js#refuses
      a hull number another boat holds, naming the property and the boat
    origin: 'services-platform:docs/milestones/plans/PLAT-M5-plan.md#PLAT-M5-S05'
  - id: BR-CAP-PFM-MTD-012-08
    proved_by: [ATS-PFM-MTD-012-08, TST-PFM-MTD-065]
    statement: >-
      Display properties reject internal line breaks by property name, trim boundary line breaks and leave
      unrelated properties unaffected.
    kind: validation
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-platform:modules/custom-objects/tests/integration/services/persistence/custom-object-records.integration.spec.js#refuses
      a create whose secondary display property holds one, naming the property, and writes no record
    origin: >-
      services-platform:docs/milestones/PLAT-M5-custom-records.md#PLAT-M5 D5 (Andrew, 2026-10-19: "Refuse,
      name the field")
  - id: BR-CAP-PFM-MTD-012-09
    proved_by: [ATS-PFM-MTD-012-09, TST-PFM-MTD-065, TST-PFM-MTD-076]
    statement: >-
      Each save derives the name from the primary display value or prefixed record number plus populated
      secondary values, limits it to five hundred characters and assigns numbers from the object sequence.
    kind: calculation
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-platform:modules/custom-objects/tests/unit/record-rules.spec.ts#is the number with the object’s
      prefix when the display property holds nothing
    origin: >-
      services-platform:docs/proposals/custom-objects/09-decisions.md#2. Choices taken as internal (record
      numbering, display name composition)
  - id: BR-CAP-PFM-MTD-012-10
    proved_by: [ATS-PFM-MTD-012-10, ATS-PFM-MTD-012-19, TST-PFM-MTD-065]
    statement: >-
      Patches affect named properties and use null to clear them, while replacements clear omitted live
      properties and accept a complete record representation.
    kind: policy
    enforced_at:
      - api
      - service
    must_prove: true
    provenance: test
    source: >-
      services-platform:modules/custom-objects/tests/integration/services/persistence/custom-object-records.integration.spec.js#clears
      every property a replacement leaves out
    origin: none
  - id: BR-CAP-PFM-MTD-012-11
    proved_by: [ATS-PFM-MTD-012-11, ATS-PFM-MTD-012-16, TST-PFM-MTD-006, TST-PFM-MTD-065]
    statement: >-
      Updates require the read version and respect update locks; archived records reject editing and
      publication but remain deletable when the object is active and deletion safeguards allow it.
    kind: validation
    enforced_at:
      - service
      - console
    must_prove: true
    provenance: test
    source: >-
      react-console:acceptance/custom-objects/playwright/journeys/PLAT-M5-S06.spec.ts#PLAT-M5-S06: two
      registrars edit one boat; the second is told and nothing is lost
    origin: 'services-platform:docs/milestones/plans/PLAT-M5-plan.md#PLAT-M5-S06'
  - id: BR-CAP-PFM-MTD-012-12
    proved_by: [ATS-PFM-MTD-012-12, TST-PFM-MTD-065]
    statement: >-
      Caller-supplied external identifiers are unique within the object and reject conflicting creation or
      patch requests.
    kind: validation
    enforced_at:
      - service
      - database
    must_prove: true
    provenance: test
    source: >-
      services-platform:modules/custom-objects/tests/integration/services/persistence/custom-object-records.integration.spec.js#refuses
      a create under an external id another record of the object holds, naming sys_external_id
    origin: none
  - id: BR-CAP-PFM-MTD-012-13
    proved_by: [ATS-PFM-MTD-007-08, ATS-PFM-MTD-012-13, ATS-PFM-MTD-012-18, TST-PFM-MTD-065]
    statement: >-
      New image and file values pass through managed asset preparation before saving, with unavailable or
      unsavable sources reported at the property without exposing the address.
    kind: validation
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-platform:modules/custom-objects/tests/integration/services/persistence/custom-object-records.integration.spec.js#names
      a create’s missing upload at properties.<key>, without the address sent
    origin: none
  - id: BR-CAP-PFM-MTD-012-14
    proved_by: [ATS-PFM-MTD-012-14, TST-PFM-MTD-051, TST-PFM-MTD-065]
    statement: >-
      Record lists default to unarchived entries ordered by name, offer bounded pages and publication, archive
      and prefix filters, while reads include grouped links and write view audits and mutations audit actions
      without values.
    kind: policy
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-platform:modules/custom-objects/tests/e2e/custom-object-records.e2e.spec.js#audits the read of
      a record as a view
    origin: none
  - id: BR-CAP-PFM-MTD-012-15
    proved_by: [ATS-PFM-MTD-012-17, TST-PFM-MTD-065, TST-PFM-MTD-076]
    statement: >-
      Decimal, currency and percent storage also records an exact precision-scaled integer, leaving that
      shadow empty when the value exceeds the supported safe integer range.
    kind: calculation
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-platform:modules/custom-objects/tests/unit/record-rules.spec.ts#is null for a value whose
      scaled form is beyond a safe integer
    origin: none
scenarios:
  - id: SC-CAP-PFM-MTD-012-01
    given: Boat is active with contact ownership configured.
    when: Create Miss Daisy for Jane Smith from its app.
    then: The first boat number appears in the Explorer and Jane Boats card.
    provenance: test
    source: >-
      react-console:acceptance/custom-objects/playwright/journeys/PLAT-M5-S03.spec.ts#PLAT-M5-S03: a registrar
      creates a boat owned by a contact: numbered 1, listed, and on the contact's Boats card
  - id: SC-CAP-PFM-MTD-012-02
    given: A boat already reserves a unique hull number.
    when: Create another boat using the same text in different case with extra boundary spaces.
    then: The refusal identifies Hull Number and its existing holder without saving the duplicate.
    provenance: test
    source: >-
      services-platform:modules/custom-objects/tests/integration/services/persistence/custom-object-records.integration.spec.js#refuses
      the same hull number in another case and with spaces around it
edge_cases:
  - id: SC-CAP-PFM-MTD-012-03
    given: A custom object includes an HTML property.
    when: Save markup in that property.
    then: The finding records an unresolved sanitization obligation in the storage path.
    provenance: unknown
    source: FND-PFM-MTD-012
constraints: []
depends_on:
  - CAP-PFM-MTD-009
  - CAP-PFM-MTD-006
related:
  - CAP-PFM-MTD-011
  - CAP-PFM-MTD-013
  - CAP-PFM-MTD-014
  - CAP-PFM-MTD-015
journeys:
  - JNY-PFM-MTD-001
  - JNY-PFM-MTD-002
open_questions: []
concepts:
  - CON-PFM-MTD-005
evidence:
  - >-
    services-platform at ad3f6a6582843008ed0f5a17b131878d15829af1:
    modules/custom-objects/aws/api/custom-object-records.routes.ts ,
    services/persistence/custom-object-records.ts, record-rules.ts,
    services/assets/custom-object-record-assets.ts, db/custom-object-records.table.ts,
    custom-object-record-values.table.ts, custom-object-unique-values.table.ts, managed-assets.table.ts; tests
    e2e/custom-object-records.e2e.spec.js,
    integration/services/persistence/custom-object-records*.integration.spec.js,
    integration/db/custom-object-records.table.integration.spec.js, unit/record-rules.spec.ts;
    tests/acceptance/custom-objects/journeys/records/**, properties-and-rules/**; docs/custom-objects.md §12
    to §17.
  - >-
    react-console at baf6dae382d6f41dce976c7f848667ccc37e076f: app/(console)/console/[tenantId]/objects/**,
    components/console/custom-objects/*, components/custom-objects/owned-records.tsx,
    components/record-explorer; Playwright PLAT-M5-S03, S04, S05, S06.
controls: [TC-AVL-005, TC-AZ-001, TC-AZ-002, TC-AZ-003, TC-AZ-004, TC-DAT-001, TC-DAT-003, TC-DAT-004, TC-IAM-001, TC-IAM-002, TC-IAM-004, TC-IAM-005, TC-LOG-002, TC-LOG-003, TC-LOG-004, TC-LOG-005, TC-MON-002, 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-TEN-004, TC-VEN-001, TC-VEN-002, TC-VEN-003]
exercised_by: [FAS-PFM-MTD-03-02, FAS-PFM-MTD-05-01, FAS-PFM-MTD-05-02, FAS-PFM-MTD-05-03, FAS-PFM-MTD-05-05]
suites: [TST-PFM-MTD-004, TST-PFM-MTD-005, TST-PFM-MTD-006, TST-PFM-MTD-051, TST-PFM-MTD-065, TST-PFM-MTD-076]
---

# Maintain Valid Custom Records

## Purpose

Record keepers need to capture and correct the organization information without breaking its definitions. The record store applies the object contract to every write.

## How It Works

A record keeper creates a record for an active object. Platform validates the supplied properties and applies creation defaults. It assigns the number and derives a readable name. Edits use the version the person read and distinguish a patch from a replacement. File values are prepared through the managed asset pipeline. A rejected write identifies the field that needs attention without returning the rejected value. Successful writes retain audit information and can produce history and business events, while reads return the available property values and linked records.

## Relationships

Custom objects provide the definition and custom fields provide value contracts. Relationships, history and event delivery extend the effects of a successful record write.
