---
id: CON-CRM-ENT-003
name: Affiliation
feature: FEAT-CRM-ENT-04
product_area: crm
module: entities
revision: 1
status: approved
product_intent: proposed
implementation_evidence: observed
source_sha:
  services-crm: 7ea48a414c42eade10d60753b8bcc323b1dd726f
  react-console: da63075876b2c5f4a6af07d457bb24ea8bf4c0c7
definition: >-
  An affiliation records a dated connection between one contact and one organization, including the
  responsibilities held on that tie.
owned_by: MOD-CRM-ENT
acted_on_by:
  - CAP-CRM-ENT-003
  - CAP-CRM-ENT-006
  - CAP-CRM-ENT-009
  - CAP-CRM-ENT-010
  - CAP-CRM-ENT-011
  - CAP-CRM-ENT-013
  - CAP-CRM-ENT-014
visible_to:
  - ACT-CRM-001
  - ACT-CRM-002
  - ACT-CRM-003
  - ACT-CRM-004
  - ACT-CRM-005
relationships:
  - to: CON-CRM-ENT-001
    statement: The contact owns the dated affiliation.
    provenance: observed
    source: 'services-crm:modules/entities/db/contact-organization-affiliations.table.ts'
  - to: CON-CRM-ENT-033
    statement: Record safeguards preserve and explain changes to the owning record.
    provenance: observed
    source: 'services-crm:modules/entities/services/persistence/record-locks.ts'
facets:
  - name: Type and default links
    meaning: Relationship categories carry optional contact-information default mappings.
    used_for: Describing ties and filling empty contact-detail purposes.
  - name: Primary designation and title
    meaning: >-
      One eligible tie identifies the primary organization and coordinates its title with the
      contact.
    used_for: Presenting the main connection without erasing other ties.
  - name: Effective dates and capacity
    meaning: >-
      Workspace dates distinguish upcoming, current and past ties and bound simultaneous
      relationships.
    used_for: Recording concurrent and repeated organizational connections.
  - name: Role periods
    meaning: Catalogue roles describe dated responsibilities within the affiliation.
    used_for: Retaining role history and applying organization restrictions.
  - name: Automatic and notification roles
    meaning: >-
      Role settings support automatic responsibility assignment and notification recipient
      selection.
    used_for: Assigning eligible roles and identifying notice recipients.
  - name: Departure and reconciliation
    meaning: Explicit departure closes a tie; reconciliation processes date boundaries.
    used_for: 'Keeping roles, default links and compliance aligned with effective dates.'
evidence:
  - >-
    services-crm at 7ea48a414c42eade10d60753b8bcc323b1dd726f: docs/entities-api.md;
    docs/affiliations/README.md; docs/archive.md; docs/record-locks.md; docs/record-history.md;
    docs/disconnected-information.md; docs/milestones/CRM-M1-contacts-and-organizations.md and
    CRM-M2-affiliations-roles-compliance.md; modules/entities/db/contacts.table.ts,
    organizations.table.ts, contact-organization-affiliations.table.ts;
    services/persistence/record-locks.ts; services/affiliations/affiliation-date-reconciliation.ts;
    services/organizations/compliance/contact-role-compliance.ts.
  - >-
    react-console at da63075876b2c5f4a6af07d457bb24ea8bf4c0c7:
    components/forms/contacts/affiliation-components.tsx;
    components/contact-editor/contact-editor.tsx;
    acceptance/crm/playwright/journeys/affiliations/CRM-ACC-0201.spec.ts; existing capability and
    journey evidence bindings in this corpus.
approved_by: Andrew
approved_at: 2026-10-22T00:00:00.000Z
narrative:
  status: verified
  review: NARRATIVE-REVIEW-2026-10-23
narrative_sources:
  - section: Why It Exists
    provenance: inferred
    source: >-
      services-crm:modules/entities/tests/e2e/contact-affiliations.e2e.spec.js#answers 400 for an
      organization the tenant does not have;
      react-console:acceptance/crm/playwright/journeys/affiliations/CRM-ACC-0202.spec.ts#CRM-ACC-0202;
      react-console:acceptance/crm/playwright/journeys/affiliations/CRM-ACC-0177.spec.ts#CRM-ACC-0177;
      services-crm:modules/entities/tests/unit/services/persistence/contact-affiliations.spec.js#refuses
      two affiliations newly marked primary at once;
      services-crm:modules/entities/tests/unit/services/persistence/contact-affiliations.spec.js#refuses
      a primary affiliation that starts after today;
      services-crm:modules/entities/tests/integration/services/persistence/contact-affiliations.integration.spec.js#makes
      nothing primary in its place;
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#refuses
      an affiliation that ends before it starts;
      services-crm:modules/entities/tests/unit/services/persistence/contact-affiliations.spec.js#finds
      periods that share their last and first day overlapping;
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#refuses
      to move the affiliation to another organization under the same id (docs/affiliations/README.md
      §0b.2);
      services-crm:modules/entities/tests/unit/services/persistence/contact-affiliations.spec.js#counts
      only current and upcoming affiliations against the limit: ended history is unlimited (M2-D5);
      services-crm:modules/entities/tests/unit/services/persistence/contact-affiliations.spec.js#ends
      the previous primary the day before the effective date by default, and its open roles with it;
      react-console:lib/affiliations/section.ts:affiliationBeingLeft;
      services-crm:modules/entities/tests/unit/services/persistence/contact-affiliations.spec.js#gives
      the contact the primary affiliation's job title when the save changed only the affiliation's;
      services-crm:modules/entities/tests/e2e/contact-information-sources.e2e.spec.js#refuses 400 a
      second contact naming the same affiliation id, saying another affiliation has it;
      services-crm:modules/entities/tests/e2e/contact-affiliations.e2e.spec.js#answers 403
      permission_missing to a caller who may read organizations but not contacts;
      react-console:components/console/organization/affiliation-dialogs.tsx:ContactPrimaryInfo;
      react-console:acceptance/crm/playwright/journeys/affiliations/CRM-ACC-0172.spec.ts#CRM-ACC-0172;
      services-crm:modules/entities/tests/integration/services/persistence/contact-affiliations.business-dates.integration.spec.js#refuses
      a primary starting on the tenant's tomorrow, though it is already UTC's and Sydney's today;
      services-crm:modules/entities/tests/integration/services/persistence/contact-affiliations.business-dates.integration.spec.js#reads
      a tie ending on the tenant's today as current;
      react-console:acceptance/crm/playwright/journeys/affiliations/CRM-ACC-0186.spec.ts#CRM-ACC-0186;
      services-crm:modules/entities/tests/unit/services/affiliations/affiliation-date-window.spec.js#spans
      every day of a skipped weekend, so the next night makes them up;
      services-crm:modules/entities/tests/integration/services/affiliations/affiliation-date-reconciliation.integration.spec.js#takes
      the organization out of compliance the next morning, with no save of the contact;
      services-crm:modules/entities/tests/integration/services/affiliations/affiliation-date-reconciliation.integration.spec.js#finds
      the day settled and changes nothing, the organization’s version included;
      services-crm:modules/entities/tests/e2e/affiliation-date-reconciliation.e2e.spec.js#refuses a
      real run that names a day, 400 naming it; services-crm:docs/affiliations/README.md#5.6b The
      overnight date reconciliation (D-46; CRM-M2 A6 and A7);
      services-crm:modules/entities/services/persistence/contact-affiliations.ts:affiliationRefusal;
      services-crm:modules/entities/tests/unit/services/persistence/contact-affiliations.spec.js#refuses
      a role whose dates lie outside its affiliation's;
      services-crm:modules/entities/tests/unit/services/persistence/contact-affiliations.spec.js#refuses
      the same role overlapping itself on one affiliation;
      services-crm:modules/entities/tests/unit/services/persistence/contact-affiliations.spec.js#ends
      the open roles of an affiliation the save ends, on the same day;
      services-crm:modules/entities/tests/integration/services/persistence/contact-affiliations.integration.spec.js#refuses
      a role outside the list added to an existing affiliation;
      services-crm:modules/entities/tests/integration/services/contacts/role-assignment/contact-role-assignment.integration.spec.js#gives
      the organization's first contact the automatic and first-contact roles, besides the roles it
      states;
      services-crm:modules/entities/tests/integration/services/contacts/role-assignment/automatic-role-reconciliation.integration.spec.js#gives
      the automatic and first-contact roles on the start day in the tenant calendar;
      services-crm:modules/entities/tests/integration/services/contacts/role-assignment/automatic-role-reconciliation.integration.spec.js#does
      not give back a role a person removed after it was given;
      services-crm:modules/entities/tests/integration/services/contacts/role-assignment/contact-role-assignment.integration.spec.js#counts
      an inactive contact as already belonging to the organization, so the next contact is not its
      first;
      services-crm:modules/entities/tests/integration/services/affiliations/affiliation-date-reconciliation.follow-ups.integration.spec.js#skips
      the forbidden role on the start day;
      services-crm:docs/milestones/CRM-M2-affiliations-roles-compliance.md#M2-D12;
      services-crm:modules/entities/tests/integration/services/contacts/role-assignment/contact-role-assignment.integration.spec.js#gives
      the first-contact roles to exactly one of several contacts joining an empty organization at
      once; react-console:components/affiliations/affiliation-editor.tsx:Add Period;
      services-crm:docs/milestones/CRM-M2-affiliations-roles-compliance.md#M2-D13;
      services-crm:docs/disconnected-information.md#What changed;
      services-crm:modules/entities/tests/integration/services/persistence/contact-information-sources.integration.spec.js#refuses
      a link to a record of an organization other than the affiliation’s;
      services-crm:modules/entities/tests/integration/services/persistence/contact-information-sources.integration.spec.js#refuses
      a new link through an upcoming affiliation, naming the path;
      services-crm:modules/entities/tests/integration/services/persistence/contact-information-sources.integration.spec.js#refuses
      a value that differs from the organization’s in letter case, naming the field (A19);
      services-crm:modules/entities/tests/integration/services/persistence/contact-information-sources.integration.spec.js#writes
      nothing to the contact: its version is the one before;
      services-crm:modules/entities/tests/integration/services/persistence/contact-information-sources.integration.spec.js#turns
      the linked work address into a disconnected row holding its last value, not a copy (D-25);
      services-crm:modules/entities/tests/e2e/contacts-disconnected.e2e.spec.js#refuses a client
      that asks for a row to be disconnected, naming the field;
      services-crm:modules/entities/tests/e2e/contacts-disconnected.e2e.spec.js#is refused 409 on a
      row that is no longer disconnected;
      services-crm:modules/entities/tests/integration/services/persistence/contact-information-sources.integration.spec.js#makes
      an independent copy of the resolved value when the row is saved as copied with no value of its
      own;
      services-crm:modules/entities/tests/integration/services/persistence/contact-information-sources.integration.spec.js#refuses
      a save that removes the affiliation and keeps the rows linked through it;
      services-crm:modules/entities/tests/integration/services/persistence/contact-information-sources.integration.spec.js#links
      nothing when the choice is an empty list;
      services-crm:modules/entities/tests/integration/services/affiliations/affiliation-date-reconciliation.follow-ups.integration.spec.js#links
      every default of its type on the start day when the save stated no choice; FND-CRM-ENT-010
    note: 'Business need inferred from the sourced record behavior, without inventing product history.'
  - section: What It Controls
    provenance: observed
    source: >-
      services-crm:modules/entities/tests/e2e/contact-affiliations.e2e.spec.js#answers 400 for an
      organization the tenant does not have;
      react-console:acceptance/crm/playwright/journeys/affiliations/CRM-ACC-0202.spec.ts#CRM-ACC-0202;
      react-console:acceptance/crm/playwright/journeys/affiliations/CRM-ACC-0177.spec.ts#CRM-ACC-0177;
      services-crm:modules/entities/tests/unit/services/persistence/contact-affiliations.spec.js#refuses
      two affiliations newly marked primary at once;
      services-crm:modules/entities/tests/unit/services/persistence/contact-affiliations.spec.js#refuses
      a primary affiliation that starts after today;
      services-crm:modules/entities/tests/integration/services/persistence/contact-affiliations.integration.spec.js#makes
      nothing primary in its place;
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#refuses
      an affiliation that ends before it starts;
      services-crm:modules/entities/tests/unit/services/persistence/contact-affiliations.spec.js#finds
      periods that share their last and first day overlapping;
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#refuses
      to move the affiliation to another organization under the same id (docs/affiliations/README.md
      §0b.2);
      services-crm:modules/entities/tests/unit/services/persistence/contact-affiliations.spec.js#counts
      only current and upcoming affiliations against the limit: ended history is unlimited (M2-D5);
      services-crm:modules/entities/tests/unit/services/persistence/contact-affiliations.spec.js#ends
      the previous primary the day before the effective date by default, and its open roles with it;
      react-console:lib/affiliations/section.ts:affiliationBeingLeft;
      services-crm:modules/entities/tests/unit/services/persistence/contact-affiliations.spec.js#gives
      the contact the primary affiliation's job title when the save changed only the affiliation's;
      services-crm:modules/entities/tests/e2e/contact-information-sources.e2e.spec.js#refuses 400 a
      second contact naming the same affiliation id, saying another affiliation has it;
      services-crm:modules/entities/tests/e2e/contact-affiliations.e2e.spec.js#answers 403
      permission_missing to a caller who may read organizations but not contacts;
      react-console:components/console/organization/affiliation-dialogs.tsx:ContactPrimaryInfo;
      react-console:acceptance/crm/playwright/journeys/affiliations/CRM-ACC-0172.spec.ts#CRM-ACC-0172;
      services-crm:modules/entities/tests/integration/services/persistence/contact-affiliations.business-dates.integration.spec.js#refuses
      a primary starting on the tenant's tomorrow, though it is already UTC's and Sydney's today;
      services-crm:modules/entities/tests/integration/services/persistence/contact-affiliations.business-dates.integration.spec.js#reads
      a tie ending on the tenant's today as current;
      react-console:acceptance/crm/playwright/journeys/affiliations/CRM-ACC-0186.spec.ts#CRM-ACC-0186;
      services-crm:modules/entities/tests/unit/services/affiliations/affiliation-date-window.spec.js#spans
      every day of a skipped weekend, so the next night makes them up;
      services-crm:modules/entities/tests/integration/services/affiliations/affiliation-date-reconciliation.integration.spec.js#takes
      the organization out of compliance the next morning, with no save of the contact;
      services-crm:modules/entities/tests/integration/services/affiliations/affiliation-date-reconciliation.integration.spec.js#finds
      the day settled and changes nothing, the organization’s version included;
      services-crm:modules/entities/tests/e2e/affiliation-date-reconciliation.e2e.spec.js#refuses a
      real run that names a day, 400 naming it; services-crm:docs/affiliations/README.md#5.6b The
      overnight date reconciliation (D-46; CRM-M2 A6 and A7);
      services-crm:modules/entities/services/persistence/contact-affiliations.ts:affiliationRefusal;
      services-crm:modules/entities/tests/unit/services/persistence/contact-affiliations.spec.js#refuses
      a role whose dates lie outside its affiliation's;
      services-crm:modules/entities/tests/unit/services/persistence/contact-affiliations.spec.js#refuses
      the same role overlapping itself on one affiliation;
      services-crm:modules/entities/tests/unit/services/persistence/contact-affiliations.spec.js#ends
      the open roles of an affiliation the save ends, on the same day;
      services-crm:modules/entities/tests/integration/services/persistence/contact-affiliations.integration.spec.js#refuses
      a role outside the list added to an existing affiliation;
      services-crm:modules/entities/tests/integration/services/contacts/role-assignment/contact-role-assignment.integration.spec.js#gives
      the organization's first contact the automatic and first-contact roles, besides the roles it
      states;
      services-crm:modules/entities/tests/integration/services/contacts/role-assignment/automatic-role-reconciliation.integration.spec.js#gives
      the automatic and first-contact roles on the start day in the tenant calendar;
      services-crm:modules/entities/tests/integration/services/contacts/role-assignment/automatic-role-reconciliation.integration.spec.js#does
      not give back a role a person removed after it was given;
      services-crm:modules/entities/tests/integration/services/contacts/role-assignment/contact-role-assignment.integration.spec.js#counts
      an inactive contact as already belonging to the organization, so the next contact is not its
      first;
      services-crm:modules/entities/tests/integration/services/affiliations/affiliation-date-reconciliation.follow-ups.integration.spec.js#skips
      the forbidden role on the start day;
      services-crm:docs/milestones/CRM-M2-affiliations-roles-compliance.md#M2-D12;
      services-crm:modules/entities/tests/integration/services/contacts/role-assignment/contact-role-assignment.integration.spec.js#gives
      the first-contact roles to exactly one of several contacts joining an empty organization at
      once; react-console:components/affiliations/affiliation-editor.tsx:Add Period;
      services-crm:docs/milestones/CRM-M2-affiliations-roles-compliance.md#M2-D13;
      services-crm:docs/disconnected-information.md#What changed;
      services-crm:modules/entities/tests/integration/services/persistence/contact-information-sources.integration.spec.js#refuses
      a link to a record of an organization other than the affiliation’s;
      services-crm:modules/entities/tests/integration/services/persistence/contact-information-sources.integration.spec.js#refuses
      a new link through an upcoming affiliation, naming the path;
      services-crm:modules/entities/tests/integration/services/persistence/contact-information-sources.integration.spec.js#refuses
      a value that differs from the organization’s in letter case, naming the field (A19);
      services-crm:modules/entities/tests/integration/services/persistence/contact-information-sources.integration.spec.js#writes
      nothing to the contact: its version is the one before;
      services-crm:modules/entities/tests/integration/services/persistence/contact-information-sources.integration.spec.js#turns
      the linked work address into a disconnected row holding its last value, not a copy (D-25);
      services-crm:modules/entities/tests/e2e/contacts-disconnected.e2e.spec.js#refuses a client
      that asks for a row to be disconnected, naming the field;
      services-crm:modules/entities/tests/e2e/contacts-disconnected.e2e.spec.js#is refused 409 on a
      row that is no longer disconnected;
      services-crm:modules/entities/tests/integration/services/persistence/contact-information-sources.integration.spec.js#makes
      an independent copy of the resolved value when the row is saved as copied with no value of its
      own;
      services-crm:modules/entities/tests/integration/services/persistence/contact-information-sources.integration.spec.js#refuses
      a save that removes the affiliation and keeps the rows linked through it;
      services-crm:modules/entities/tests/integration/services/persistence/contact-information-sources.integration.spec.js#links
      nothing when the choice is an empty list;
      services-crm:modules/entities/tests/integration/services/affiliations/affiliation-date-reconciliation.follow-ups.integration.spec.js#links
      every default of its type on the start day when the save stated no choice; FND-CRM-ENT-010
---

# Affiliation

## What It Is

An Affiliation is a dated connection between one contact and one organization, classified by an Affiliation Type. It can describe employment, a chapter connection, or another workspace-defined kind of tie. The contact owns the affiliation, including its title, notes, primary designation, and dated role periods, even when staff initiate work from the organization's page.

Each affiliation has its own identity and period. One person can have concurrent ties to different organizations or different kinds of connection to the same one. Separate ended and later-returned ties preserve distinct episodes rather than pretending the person never left. Current, upcoming, and past interpretation uses the workspace's business day.

## Why It Exists

Our reading is that a single Organization field cannot explain both current relationships and their history. A member can work at a firm and serve a chapter simultaneously, and later change employment without ending chapter work. Making each connection explicit preserves the organizations, dates, and responsibilities needed to understand those differences without copying organization names into the person record.

## What It Is Not

An Affiliation is not an organization record or a contact role. It connects the two identities, while role periods describe responsibilities within that connection. A primary designation is a prominent label and does not define all the organizations the person belongs to today. A live affiliation is also distinct from a linked address: the tie authorizes a source relationship, but contact-point behavior owns resolution and disconnection.

## What It Controls

Effective affiliations determine current organizational membership for the applicable person lists and descendant discovery. Primary selection supplies the contact's principal organization context and coordinates the job title, but a past or upcoming tie has a different meaning. Ending a primary removes that designation without inventing a replacement.

The affiliation bounds its role periods and supplies the organizational context for allowed-role checks and effective compliance coverage. Eligible affiliations can authorize shared address and phone links and pending defaults. Their start boundaries can trigger automatic roles and shared defaults through reconciliation. Removing a tie must handle its supplied links, while ending it may leave them unresolved; historical context remains rather than becoming a new organization's connection.

## How It Is Used

The CRM Editor adds, ends, and inspects ties in the contact editor and sees the resulting rows on organization people views. Record an employer change while retaining the previous affiliation shows the departure question and the retained old connection. The CRM Reader chooses Current, Past, or a dated view to answer the intended relationship question.

Good use distinguishes a new tie from reassignment of an old one. Staff end employment and add its replacement, choose primary deliberately, and preserve separate role periods when responsibilities recur. Omitting affiliations in an unrelated save leaves them alone; an explicit collection expresses a complete intended result and receives aggregate validation.

## Example

Lena is employed by Harbor Engineering and volunteers for Harbor West Chapter. The employment tie is primary, but both are current. On a move to Bayview Manufacturing, staff add the new primary and confirm departure from Harbor Engineering. Its affiliation and responsibilities end, while the chapter connection remains current.

A later return to Harbor Engineering creates another affiliation rather than overwriting the ended employment. Staff can inspect both episodes, with their own titles and role periods. Current people discovery can include Lena through the chapter even when her primary subtitle names Bayview, because membership and prominent labeling answer different questions.

## How It Relates

Contact owns the affiliation and Organization supplies its other party. Affiliation Type describes the kind of connection and default sharing choices. Contact Role gives responsibility meaning while role periods remain facets of the tie. Organization Type can restrict those roles; Organization Compliance assesses their effective coverage. Shared contact details depend on eligible sources, and Record Protection and History explains locks, versioned edits, and retained changes.
