---
id: CAP-CRM-ENT-002
title: Establish an organization and its hierarchy
product_area: crm
module: entities
revision: 1
status: approved
product_intent: confirmed
implementation_evidence: partially-observed
source_sha:
  services-crm: 7ea48a414c42eade10d60753b8bcc323b1dd726f
  react-console: da63075876b2c5f4a6af07d457bb24ea8bf4c0c7
intent_sources:
  - 'services-crm:docs/milestones/CRM-M1-contacts-and-organizations.md#M1-D3'
  - 'services-crm:docs/milestones/CRM-M1-contacts-and-organizations.plan.md#A16'
  - 'services-crm:docs/milestones/CRM-M1-contacts-and-organizations.md#D-07'
purpose: >-
  Staff establish an organization record and position it within the tenant’s
  organizational structure.
outcomes:
  - >-
    An organization exists with its number, id, type and parent, and its
    ancestry is returned on every read.
  - A loop in the hierarchy can never be saved.
  - A rejected create writes nothing and reports every problem at once.
actors:
  - ACT-CRM-001
  - ACT-CRM-003
  - ACT-CRM-004
surfaces:
  - console
  - api
preconditions: []
postconditions: []
permissions:
  - key: crm.organizations.create
    where_checked: api
    scope: tenant
  - key: crm.organizations.create-despite-duplicate-block
    where_checked: api
    scope: tenant
  - key: crm.organization-types.read
    where_checked: api
    scope: tenant
rules:
  - id: BR-CAP-CRM-ENT-002-01
    proved_by: [ATS-CRM-ENT-002-01, ATS-CRM-ENT-002-14, ATS-CRM-ENT-002-15, ATS-CRM-ENT-005-13, ATS-CRM-ENT-008-09, TST-CRM-ENT-786, TST-CRM-ENT-787]
    statement: >-
      Organization saves reject blank names, names above 200 characters and
      notes above 10,000 characters, identifying the invalid field.
    kind: validation
    enforced_at:
      - api
      - service
      - database
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/organizations.field-rules.integration.spec.js#refuses
      a name of 201 characters, naming name
    origin: 'services-crm:docs/milestones/CRM-M1-contacts-and-organizations.md#M1-D3'
    examples:
      positive: 'Accepted example: a name of exactly 200 characters is accepted.'
      negative: 'Rejected example: a name of spaces is rejected 400 naming name.'
  - id: BR-CAP-CRM-ENT-002-02
    proved_by: [ATS-CRM-ENT-002-02, TST-CRM-ENT-786]
    statement: >-
      Organization creation preserves an unused valid client id within the
      tenant or generates a time-ordered UUID when none is provided.
    kind: validation
    enforced_at:
      - api
      - database
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/entities/tests/e2e/organizations.e2e.spec.js#keeps
      the id the body named, which is no UUIDv7
    origin: none
  - id: BR-CAP-CRM-ENT-002-03
    proved_by: [ATS-CRM-ENT-002-03, TST-CRM-ENT-787]
    statement: >-
      Organization numbers use a tenant counter separate from contact numbers,
      initially 10000, ignore client numbers, may leave gaps after refusal and
      recover collisions above the highest existing organization number.
    kind: calculation
    enforced_at:
      - service
      - database
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/organizations.integration.spec.js#renumbers
      after the tenant's highest organization number, never its contacts'
    origin: none
  - id: BR-CAP-CRM-ENT-002-04
    proved_by: [ATS-CRM-ENT-002-04, ATS-CRM-ENT-002-16, ATS-CRM-ENT-002-18, ATS-CRM-ENT-012-19, ATS-CRM-ENT-018-10, TST-CRM-ENT-786]
    statement: >-
      A supplied organization type must exist in the tenant or the save returns
      a conflict.
    kind: validation
    enforced_at:
      - database
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/entities/tests/e2e/organization-types.e2e.spec.js#refuses
      an organization of a type the tenant does not have (409), and the delete
      of a type an organization has (409)
    origin: none
  - id: BR-CAP-CRM-ENT-002-05
    proved_by: [ATS-CRM-ENT-002-05, ATS-CRM-ENT-002-17, TST-CRM-ENT-786, TST-CRM-ENT-787]
    statement: >-
      A parent organization must belong to the same tenant or the save returns a
      conflict.
    kind: validation
    enforced_at:
      - database
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/entities/tests/e2e/organizations.e2e.spec.js#answers
      409 for a parent the tenant does not have
    origin: none
  - id: BR-CAP-CRM-ENT-002-06
    proved_by: [ATS-CRM-ENT-002-06, TST-CRM-ENT-775, TST-CRM-ENT-787]
    statement: >-
      An organization cannot parent itself or be moved beneath a descendant;
      when opposite moves race, the move following the first commit is rejected.
    kind: invariant
    enforced_at:
      - service
      - database
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/entities/tests/integration/db/organizations.table.integration.spec.js#refuses
      the second move by organizations_parent_not_descendant once the first
      commits
    origin: none
    examples:
      positive: 'Accepted example: a under B under a top: saved.'
      negative: >-
        Rejected example: the top moved under its grandchild: 400 naming the
        parent field.
  - id: BR-CAP-CRM-ENT-002-07
    proved_by: [ATS-CRM-ENT-002-07, ATS-CRM-ENT-002-13, ATS-CRM-ENT-003-14, ATS-CRM-ENT-004-24, TST-CRM-ENT-787]
    statement: >-
      Organization reads compute the nearest-first ancestor chain, its length
      and its top-level organization, so descendant reads reflect a parent move
      immediately and supplied ancestry does not control the result.
    kind: calculation
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/organizations.integration.spec.js#answers
      the descendant, never saved again, its ancestry under the new parent
    origin: none
  - id: BR-CAP-CRM-ENT-002-08
    proved_by: [ATS-CRM-ENT-002-08, ATS-CRM-ENT-002-12, ATS-CRM-ENT-004-22, TST-CRM-ENT-775, TST-CRM-ENT-787]
    statement: >-
      Organization status defaults to active and permits active, inactive or
      duplicate.
    kind: policy
    enforced_at:
      - api
      - database
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/entities/tests/integration/db/organizations.table.integration.spec.js#refuses
      a status outside the closed list by organizations_status_check
    origin: none
  - id: BR-CAP-CRM-ENT-002-09
    proved_by: [ATS-CRM-ENT-002-09, TST-CRM-ENT-786, TST-CRM-ENT-788]
    statement: >-
      An unaccompanied website derives a lowercase domain host without protocol,
      www, port, path or query, an unaccompanied domain derives an http www
      website, and an unrecognizable value is preserved without deriving the
      other field.
    kind: calculation
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/entities/tests/unit/db/organization-domain-name.spec.js#derives
      the domain name without the protocol or the www prefix
    origin: none
  - id: BR-CAP-CRM-ENT-002-10
    proved_by: [ATS-CRM-ENT-002-10, TST-CRM-ENT-787]
    statement: >-
      An organization logo becomes permanent from a tenant upload, tenant file
      or public image before the record is written; failure rejects the save and
      preserves the previous logo.
    kind: validation
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/organizations.integration.spec.js#refuses
      the update as ObjectInvalid, naming the logo field
    origin: none
  - id: BR-CAP-CRM-ENT-002-11
    proved_by: [ATS-CRM-ENT-002-11]
    statement: >-
      A blocked organization create requires duplicate-block override authority
      and a reason rather than confirmation alone.
    kind: policy
    enforced_at:
      - api
      - service
    must_prove: true
    provenance: unknown
    source: FND-CRM-ENT-029
    origin: >-
      D-32 (cited in
      services-crm:aws/crm.service.ts:duplicateBlockOverrideAction)
  - id: BR-CAP-CRM-ENT-002-12
    proved_by: []
    statement: >-
      Staff can create organizations in the console, but editing an existing
      organization uses the API until the Organization 360 milestone delivers
      its editor.
    kind: policy
    enforced_at:
      - console
    must_prove: false
    provenance: documented
    source: >-
      services-crm:docs/milestones/CRM-M1-contacts-and-organizations.plan.md#2.
      Scope
    origin: >-
      services-crm:docs/milestones/CRM-M1-contacts-and-organizations.plan.md#2.
      Scope
scenarios:
  - id: SC-CAP-CRM-ENT-002-01
    given: A tenant with a company type.
    when: A staff member creates an organization from the console form.
    then: 'The API returns every field as entered, with a number of 10000 or more.'
    provenance: test
    source: >-
      react-console:acceptance/crm/playwright/journeys/organizations/CRM-ACC-0100.spec.ts#CRM-ACC-0100
  - id: SC-CAP-CRM-ENT-002-02
    given: 'A national body, a region below it and a chapter below that.'
    when: The region moves under another top.
    then: 'The chapter, never saved again, returns its new top level.'
    provenance: test
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/organizations.integration.spec.js#answers
      the descendant, never saved again, its ancestry under the new parent
edge_cases:
  - id: SC-CAP-CRM-ENT-002-03
    given: >-
      Two administrators move two organizations under each other at the same
      instant.
    when: Both save.
    then: The first move commits and the second is rejected.
    provenance: test
    source: >-
      services-crm:modules/entities/tests/integration/db/organizations.table.integration.spec.js#refuses
      the second move by organizations_parent_not_descendant once the first
      commits
constraints: []
depends_on:
  - CAP-CRM-ENT-018
related:
  - CAP-CRM-ENT-003
  - CAP-CRM-ENT-005
  - CAP-CRM-ENT-012
  - CAP-CRM-ENT-014
  - CAP-CRM-ENT-018
journeys:
  - JNY-CRM-ENT-005
open_questions: []
feature: FEAT-CRM-ENT-02
concepts:
  - CON-CRM-ENT-002
  - CON-CRM-ENT-001
evidence:
  - >-
    `services-crm` at `7ea48a414c42eade10d60753b8bcc323b1dd726f`: examined
    `aws/api/organizations.routes.ts`, `organization-api-mapper.ts`, `organizations.schemas.ts`,
    `services/persistence/organizations.ts`,
    `services/organizations/ancestry/organization-ancestry.ts`, `db/organizations.table.ts`,
    `db/organization-domain-name.ts`, `db/field-limits.ts`, `db/managed-assets.table.ts` (the logo's
    file record); tests `e2e/organizations.e2e.spec.js`,
    `e2e/organizations-domain-name.e2e.spec.js`,
    `integration/services/persistence/organizations.integration.spec.js`,
    `organizations.field-rules.integration.spec.js`,
    `integration/db/organizations.table.integration.spec.js`,
    `unit/db/organization-domain-name.spec.js`.
  - >-
    `react-console` at `da63075876b2c5f4a6af07d457bb24ea8bf4c0c7`: examined inventory
    RUL-CRM-ORG-001 to 006, 011 to 015, 020, 022, 042; journeys CRM-ACC-0100, 0101, 0107, 0112,
    0113, 0117, 0128, 0137.
approved_by: Andrew
approved_at: 2026-10-22
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]
narrative:
  status: verified
  review: NARRATIVE-REVIEW-2026-10-23
narrative_sources:
  - section: Why It Works This Way
    provenance: inferred
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/organizations.field-rules.integration.spec.js#refuses
      a name of 201 characters, naming name;
      services-crm:modules/entities/tests/e2e/organizations.e2e.spec.js#keeps
      the id the body named, which is no UUIDv7;
      services-crm:modules/entities/tests/integration/services/persistence/organizations.integration.spec.js#renumbers
      after the tenant's highest organization number, never its contacts';
      services-crm:modules/entities/tests/e2e/organization-types.e2e.spec.js#refuses
      an organization of a type the tenant does not have (409), and the delete
      of a type an organization has (409);
      services-crm:modules/entities/tests/e2e/organizations.e2e.spec.js#answers
      409 for a parent the tenant does not have;
      services-crm:modules/entities/tests/integration/db/organizations.table.integration.spec.js#refuses
      the second move by organizations_parent_not_descendant once the first
      commits;
      services-crm:modules/entities/tests/integration/services/persistence/organizations.integration.spec.js#answers
      the descendant, never saved again, its ancestry under the new parent;
      services-crm:modules/entities/tests/integration/db/organizations.table.integration.spec.js#refuses
      a status outside the closed list by organizations_status_check;
      services-crm:modules/entities/tests/unit/db/organization-domain-name.spec.js#derives
      the domain name without the protocol or the www prefix;
      services-crm:modules/entities/tests/integration/services/persistence/organizations.integration.spec.js#refuses
      the update as ObjectInvalid, naming the logo field; FND-CRM-ENT-029;
      services-crm:docs/milestones/CRM-M1-contacts-and-organizations.plan.md#2.
      Scope
    note: >-
      Operational rationale inferred from the cited behavior; no personal motive
      or historical decision is asserted.
repositories:
  - repo: services-crm
    role: owner
    paths:
      - modules/entities/
      - docs/milestones/CRM-M1-contacts-and-organizations.plan.md
      - modules/entities/tests/e2e/organization-types.e2e.spec.js
      - modules/entities/tests/e2e/organizations.e2e.spec.js
      - >-
        modules/entities/tests/integration/db/organizations.table.integration.spec.js
      - >-
        modules/entities/tests/integration/services/persistence/organizations.field-rules.integration.spec.js
      - >-
        modules/entities/tests/integration/services/persistence/organizations.integration.spec.js
      - modules/entities/tests/unit/db/organization-domain-name.spec.js
  - repo: react-console
    role: console
    paths:
      - 'app/(console)/console/[tenantId]/crm/organizations/'
      - acceptance/crm/playwright/journeys/organizations/CRM-ACC-0100.spec.ts
exercised_by: [FAS-CRM-ENT-02-01, FAS-CRM-ENT-02-02, FAS-CRM-ENT-02-03, FAS-CRM-ENT-02-04, FAS-CRM-ENT-02-05, FAS-CRM-ENT-05-01]
suites: [TST-CRM-ENT-775, TST-CRM-ENT-786, TST-CRM-ENT-787, TST-CRM-ENT-788]
---

# Establish an organization and its hierarchy

## Purpose

An association needs to recognize member firms, chapters, and other bodies independently of the people who represent them. This capability establishes each organization and records its parent relationship, giving affiliations and organizational work a stable reference and giving readers a current view of the wider hierarchy.

## How It Works

Staff supply a name, optional organization type and parent, and organizational information. Rhythm checks the workspace references and field limits, prepares a logo when supplied, and creates the record with its own organization number. Organization numbering is separate from person numbering, and a client-supplied number does not choose that sequence.

The saved parent is the basis for ancestry. Reads walk the current parent chain to derive nearest ancestors and the top-level organization; supplied ancestry does not dictate the result. A parent move therefore affects descendant reads immediately without resaving them. Self-parenting and descendant-parenting are refused, and concurrent opposing moves cannot both establish a loop.

Website and domain values can derive their counterpart when only one recognizable value is supplied. Status describes the organization separately from archive. Console creation is within the recorded milestone; the formal contract describes existing organization editing through the service pending the fuller organization editor.

## Example

A coordinator creates Harbor West Chapter beneath Harbor National, with the chapter's address and website. Reading the chapter shows the national body's ancestry relationship and a derived domain. Later a service update moves the chapter beneath Harbor Regional Council. The next chapter read reports that new chain; trying to place the council beneath its own chapter is refused without creating a cycle.

The coordinator reads Harbor West's assigned identity and number before the move and compares them afterwards. Those values still name the same chapter; the derived ancestor list changes. If she submits an unknown council identity, the move refuses the parent reference rather than disconnecting the chapter from the national body. Two callers attempting opposing moves cannot both establish their requested structure: after the first commits, the second move is refused as cyclic. The caller rereads the actual hierarchy before proposing another placement. This verifies the hierarchy result rather than counting a successful HTTP response as sufficient evidence.

## Why It Works This Way

Our reading is that independent organization identities let a representative change without turning the organization into a different body. Workspace-owned references prevent an accidental cross-customer parent or type. Cycle prevention protects both navigation and descendant queries; checking only the proposed record without coordinating concurrent moves would leave a race that could invalidate the hierarchy.

Deriving ancestry on read avoids stale descendant copies after a parent move. Separate counters keep person and organization numbering independent, while allowed gaps mean refused writes need not recycle an identity-like number. Logo preparation before persistence protects the previous image on failure. The console boundary matters because a service capability is not evidence that every staff editing action exists on a page.

## Relationships

Maintain a person’s organizational relationships connects people to these organizations. Locate people and organizations consumes their hierarchy for descendant discovery. Organization Type supplies classification and role policy; Review organization role compliance and prepare notices owns derived assessments. Platform owns file storage and definitions, and Data Quality owns duplicate decisions that creation may encounter.
