---
id: CAP-CRM-ACT-001
title: Log one interaction for all related people and organizations
feature: FEAT-CRM-ACT-01
product_area: crm
module: activities-tasks
revision: 2
status: draft
product_intent: proposed
implementation_evidence: observed
supersedes:
  - react-console acceptance inventory CAP-CRM-ACT-001
source_sha:
  services-crm: a8d1364f1dd44b723bb5f9acba12f983e9ec8d69
  react-console: baf6dae382d6f41dce976c7f848667ccc37e076f
intent_sources:
  - 'services-crm:docs/milestones/CRM-M8-activities-and-tasks.md#D-43'
  - 'services-crm:docs/milestones/CRM-M8-activities-and-tasks.md#D-50'
  - 'services-crm:docs/milestones/CRM-M8-activities-and-tasks.md#D-51'
  - >-
    R2-ARCH-001 D1 (Andrew 2026-10-19), recorded at
    services-crm:docs/migrations/activities/README.md#9. Decisions
purpose: Staff capture an interaction once and connect it to every CRM record involved.
outcomes:
  - A shared account of the interaction
  - Details suited to the activity type
  - No duplicate entry after a retried create
concepts:
  - CON-CRM-ACT-001
actors:
  - ACT-CRM-001
  - ACT-CRM-004
surfaces:
  - console
  - api
  - job
preconditions:
  - At least one activity type is active in the workspace.
  - All selected contacts and organizations belong to this workspace.
postconditions:
  - Activity details and links commit together or are rolled back together.
permissions:
  - key: crm.activities.create
    where_checked: api
    scope: tenant
  - key: crm.activities.create
    where_checked: console
    scope: tenant
rules:
  - id: BR-CAP-CRM-ACT-001-01
    proved_by: [ATS-CRM-ACT-001-01, TST-CRM-ACT-003, TST-CRM-ACT-007]
    statement: >-
      An activity links between one and 200 distinct workspace records, and each
      link identifies either a contact or an organization.
    kind: validation
    enforced_at:
      - api
      - service
      - database
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/activities/tests/integration/services/persistence/activities.integration.spec.js#refuses
      an activity linked to nothing
    origin: none
    examples:
      positive: >-
        A meeting involving two contacts and their organization saves as one
        activity.
      negative: >-
        A single link identifying a contact and an organization fails
        validation.
  - id: BR-CAP-CRM-ACT-001-02
    proved_by: [ATS-CRM-ACT-001-02, ATS-CRM-ACT-001-03, ATS-CRM-ACT-001-14, TST-CRM-ACT-003, TST-CRM-ACT-007, TST-CRM-ACT-035, TST-CRM-ACT-082]
    statement: >-
      Only one link can be primary; if none is selected, Rhythm makes the first
      link primary.
    kind: policy
    enforced_at:
      - service
      - database
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/activities/tests/integration/services/persistence/activities.integration.spec.js#makes
      the first link the primary one when none says it is
    origin: 'services-crm:docs/milestones/CRM-M8-activities-and-tasks.md#D-50'
  - id: BR-CAP-CRM-ACT-001-03
    proved_by: [ATS-CRM-ACT-001-04, ATS-CRM-ACT-008-10, TST-CRM-ACT-007]
    statement: >-
      Creating an activity requires an active activity type from the same
      workspace.
    kind: validation
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/activities/tests/integration/services/persistence/activities.integration.spec.js#refuses
      an inactive type for a new activity
    origin: none
  - id: BR-CAP-CRM-ACT-001-04
    proved_by: [ATS-CRM-ACT-001-05, TST-CRM-ACT-002, TST-CRM-ACT-007, TST-CRM-ACT-013]
    statement: >-
      Field modes control hidden and required details, with required duration
      and outcome checked only on completion and hidden attendees still
      permitted as links.
    kind: validation
    enforced_at:
      - console
      - service
    must_prove: true
    provenance: observed
    source: >-
      services-crm:modules/activities/services/activities/field-modes.ts:fieldModeProblems
    origin: none
    note: >-
      This source is an implementation decision record, so it does not confirm
      Product authority for the rule.
  - id: BR-CAP-CRM-ACT-001-05
    proved_by: [ATS-CRM-ACT-001-06, TST-CRM-ACT-007]
    statement: >-
      An activity type that requires attendees needs a linked contact other than
      the primary record.
    kind: validation
    enforced_at:
      - console
      - service
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/activities/tests/integration/services/persistence/activities.integration.spec.js#asks
      for an attendee besides the primary record when the type requires
      attendees
    origin: none
  - id: BR-CAP-CRM-ACT-001-06
    proved_by: [ATS-CRM-ACT-001-07, TST-CRM-ACT-003, TST-CRM-ACT-007]
    statement: >-
      A newly selected outcome must be active and belong to the selected
      activity type, with the type relationship also checked in storage.
    kind: validation
    enforced_at:
      - service
      - database
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/activities/tests/integration/services/persistence/activities.integration.spec.js#refuses
      an outcome of another type
    origin: none
  - id: BR-CAP-CRM-ACT-001-07
    proved_by: [ATS-CRM-ACT-001-08, TST-CRM-ACT-007]
    statement: >-
      The end cannot precede the start, duration must be between zero and 1,440
      minutes, and the subject cannot exceed 500 characters; refusals identify
      the affected field.
    kind: validation
    enforced_at:
      - service
      - database
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/activities/tests/integration/services/persistence/activities.integration.spec.js#refuses
      an end before the start
    origin: none
  - id: BR-CAP-CRM-ACT-001-08
    proved_by: [ATS-CRM-ACT-001-09, TST-CRM-ACT-002, TST-CRM-ACT-007, TST-CRM-ACT-012]
    statement: >-
      Rhythm cleans formatted notes of scripts, styles, frames, event handlers
      and unsafe links, treats content reduced to nothing as empty, and limits
      the result to 100,000 characters.
    kind: calculation
    enforced_at:
      - service
      - database
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/activities/tests/unit/services/activities/activity-body.spec.js#drops
      a script and its content
    origin: none
    note: >-
      This source is an implementation decision record, so it does not confirm
      Product authority for the rule.
  - id: BR-CAP-CRM-ACT-001-09
    proved_by: [ATS-CRM-ACT-001-10, TST-CRM-ACT-007]
    statement: >-
      A new activity defaults to the current time, its type’s initial status and
      visible default duration, and the signed-in user as owner.
    kind: calculation
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/activities/tests/integration/services/persistence/activities.integration.spec.js#defaults
      the status, the time and the owner from the type, the clock and the caller
    origin: none
  - id: BR-CAP-CRM-ACT-001-10
    proved_by: []
    statement: >-
      Ownership is optional and remains separate from creation and modification
      attribution.
    kind: policy
    enforced_at:
      - console
      - service
    must_prove: false
    provenance: test
    source: >-
      react-console:lib/activities/form-rules.test.ts#does not ask for an owner:
      an activity may have none (D-51)
    origin: 'services-crm:docs/milestones/CRM-M8-activities-and-tasks.md#D-51'
  - id: BR-CAP-CRM-ACT-001-11
    proved_by: [ATS-CRM-ACT-001-11, TST-CRM-ACT-006, TST-CRM-ACT-069]
    statement: >-
      Repeating an identical create under the same client-selected identifier
      returns the existing activity, while different content under that
      identifier causes a conflict.
    kind: invariant
    enforced_at:
      - service
      - database
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/activities/tests/integration/services/persistence/activities.create-retries.integration.spec.js#makes
      one activity
    origin: 'services-crm:docs/milestones/CRM-M8-activities-and-tasks.md#D-43'
  - id: BR-CAP-CRM-ACT-001-12
    proved_by: [ATS-CRM-ACT-001-12, TST-CRM-ACT-003, TST-CRM-ACT-007]
    statement: >-
      Rhythm supplies the activity’s origin, and an imported external reference
      must be unique within its workspace.
    kind: invariant
    enforced_at:
      - service
      - database
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/activities/tests/integration/services/persistence/activities.integration.spec.js#records
      sys_origin and sys_external_id from the source it is given
    origin: none
  - id: BR-CAP-CRM-ACT-001-13
    proved_by: [ATS-CRM-ACT-001-13, TST-CRM-ACT-002]
    statement: >-
      Activity saves reject custom field values because activities do not
      support custom fields.
    kind: validation
    enforced_at:
      - api
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/activities/tests/e2e/activities.e2e.spec.js#refuses a
      custom field value, an activity having no custom fields (R2-ARCH-001): 400
      naming custom_fields
    origin: >-
      R2-ARCH-001 D1 (Andrew 2026-10-19), recorded at
      services-crm:docs/migrations/activities/README.md#9. Decisions
  - id: BR-CAP-CRM-ACT-001-14
    proved_by: []
    statement: >-
      For a future-dated new activity, the console selects Scheduled unless the
      user already chose a status; editing preserves the saved status.
    kind: policy
    enforced_at:
      - console
    must_prove: false
    provenance: test
    source: >-
      react-console:lib/activities/schedule-status.test.ts#keeps a status the
      person chose
    origin: none
  - id: BR-CAP-CRM-ACT-001-15
    proved_by: [ATS-CRM-ACT-001-16]
    statement: >-
      A field-mode refusal reports the detected field problems together, and a
      rejected transaction keeps neither its activity nor its follow-up.
    kind: invariant
    enforced_at:
      - service
    must_prove: true
    provenance: observed
    source: >-
      services-crm:modules/activities/services/persistence/activities.ts:saveNewActivity
    origin: none
  - id: BR-CAP-CRM-ACT-001-16
    proved_by: [ATS-CRM-ACT-001-05, TST-CRM-ACT-002, TST-CRM-ACT-007, TST-CRM-ACT-013]
    statement: Hiding the attendee control leaves additional contact links valid.
    kind: validation
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/activities/tests/unit/services/activities/field-modes.spec.js#refuses
      nothing about the links when attendees are hidden
    origin: none
  - id: BR-CAP-CRM-ACT-001-17
    proved_by: [ATS-CRM-ACT-001-15]
    statement: >-
      A user create replaces absent or empty ownership with the caller, while a
      subsequent edit can retain empty ownership.
    kind: calculation
    enforced_at:
      - service
    must_prove: true
    provenance: observed
    source: >-
      services-crm:modules/activities/services/persistence/activities.ts:activityValuesOf
    origin: none
    note: >-
      The observed creation default conflicts with the recorded permission to
      choose No Owner.
scenarios:
  - id: SC-CAP-CRM-ACT-001-01
    given: The coordinator opens a member record with Meeting available.
    when: >-
      Record a meeting linked to the member, an attending colleague and that
      colleague’s organization.
    then: The same meeting appears on all three timelines with the member primary.
    provenance: test
    source: >-
      react-console:acceptance/crm/playwright/journeys/cross-application/CRM-ACC-0690.spec.ts#CRM-ACC-0690
  - id: SC-CAP-CRM-ACT-001-02
    given: Call requires direction and an outcome for completed work.
    when: Submit a completed call with both details missing.
    then: The refusal identifies both fields and leaves no saved call.
    provenance: test
    source: >-
      services-crm:modules/activities/tests/integration/services/persistence/activities.integration.spec.js#refuses
      a call with no direction and no outcome, naming both
edge_cases:
  - id: SC-CAP-CRM-ACT-001-03
    given: The browser receives no answer to an activity create.
    when: Retry the identical create from the console.
    then: Rhythm returns the existing activity without another entry.
    provenance: test
    source: >-
      react-console:lib/activities/retry-safe-writes.test.ts#answers a lost
      response with the activity it logged, and logs no second one
constraints: []
depends_on:
  - CAP-CRM-ACT-008
related:
  - CAP-CRM-ACT-002
  - CAP-CRM-ACT-003
  - CAP-CRM-ACT-005
journeys:
  - JNY-CRM-ACT-001
  - JNY-CRM-ACT-004
open_questions: []
evidence:
  - >-
    services-crm at a8d1364f1dd44b723bb5f9acba12f983e9ec8d69: Independently
    reviewed declarations, behavior and assertions identified in this
    capability’s row citations.
  - >-
    react-console at baf6dae382d6f41dce976c7f848667ccc37e076f: Independently
    reviewed declarations, behavior and assertions identified in this
    capability’s row citations.
controls: [TC-AZ-001, TC-AZ-002, TC-AZ-003, TC-AZ-004, TC-AZ-006, TC-DAT-001, TC-DAT-002, TC-DAT-003, TC-LOG-004, TC-LOG-005, TC-PI-001, TC-PI-002, TC-PI-003, TC-PRV-001, TC-PRV-002, TC-TEN-001, TC-TEN-002, TC-TEN-003]
exercised_by: [FAS-CRM-ACT-01-01, FAS-CRM-ACT-01-02, FAS-CRM-ACT-01-04, FAS-CRM-ACT-01-05, FAS-CRM-ACT-01-06, FAS-CRM-ACT-02-02, FAS-CRM-ACT-02-03]
suites: [TST-CRM-ACT-002, TST-CRM-ACT-003, TST-CRM-ACT-006, TST-CRM-ACT-007, TST-CRM-ACT-012, TST-CRM-ACT-013, TST-CRM-ACT-035, TST-CRM-ACT-069, TST-CRM-ACT-082]
---

# Log one interaction for all related people and organizations

## Purpose

Staff capture an interaction once and connect it to every CRM record involved.

## How It Works

A coordinator starts from a contact or organization and chooses Log Activity. They select the activity type and enter the details the form presents. Linked records describe everyone involved, while the primary selection establishes the preferred record context. Rhythm applies the type’s defaults to omitted details. An activity can have empty ownership after editing, but an initial user save fills it from the caller even when No Owner is selected. Formatted notes pass through cleaning before storage. A refused save returns the detected problems and does not retain the activity. A repeated create can recover the original record after a lost response. The customer’s migration and integration processes can also supply activities through their separate import paths.

## Relationships

- Activity configuration supplies the available types and form choices.
- Record timelines present saved interactions in their related contexts.
- Follow-up creation adds work to an initial activity save.
