---
id: STATE-CRM-ACT-003
title: Task holding and routing
object: DOM-CRM-ACT-003
product_area: crm
module: activities-tasks
revision: 1
status: draft
design_intent: proposed
implementation_evidence: observed
source_sha:
  services-crm: a8d1364f1dd44b723bb5f9acba12f983e9ec8d69
  react-console: baf6dae382d6f41dce976c7f848667ccc37e076f
states:
  - name: unrouted
    meaning: 'The task has no holder, queue or candidate role.'
    provenance: observed
    source: 'services-crm:modules/tasks/services/persistence/tasks.ts'
  - name: held
    meaning: A user holds the task without a queue or candidate role.
    provenance: observed
    source: 'services-crm:modules/tasks/services/persistence/tasks.ts'
  - name: queued_waiting
    meaning: The task waits unheld in a queue and has no candidate role.
    provenance: observed
    source: 'services-crm:modules/tasks/services/persistence/tasks.ts'
  - name: queued_claimed
    meaning: A user holds queued work without a candidate role.
    provenance: observed
    source: 'services-crm:modules/tasks/services/persistence/tasks.ts'
  - name: offered
    meaning: Unheld work has a candidate role and no queue.
    provenance: observed
    source: 'services-crm:modules/tasks/services/persistence/tasks.ts'
  - name: offered_claimed
    meaning: 'A user holds role-offered work, with its candidate role retained and no queue.'
    provenance: observed
    source: 'services-crm:modules/tasks/services/persistence/tasks.ts'
  - name: queued_offered_waiting
    meaning: Unheld work has both a queue and a candidate role; its claim uses queue eligibility.
    provenance: observed
    source: 'services-crm:modules/tasks/services/persistence/tasks.ts'
  - name: queued_offered_claimed
    meaning: A user holds work with both a queue and a retained candidate role.
    provenance: observed
    source: 'services-crm:modules/tasks/services/persistence/tasks.ts'
transitions:
  - from: unrouted
    to: held
    trigger: 'Assign a user directly, including through ordinary editing.'
    guard: >-
      The task is unfinished, updates are unlocked and the user reference has the accepted
      syntax.
    side_effects:
      - Set holder and assignment attribution; enabled attention updates its audience.
    who: A task-update caller or the workflow service.
    provenance: test
    source: >-
      services-crm:modules/tasks/tests/e2e/tasks.e2e.spec.js#stamps who holds the task,
      who gave it to them and when
  - from: held
    to: unrouted
    trigger: Clear the directly assigned user.
    guard: The task is unfinished and updates are unlocked.
    side_effects:
      - >-
        Remove assignment attribution; enabled attention falls back to workspace
        administrators.
    who: A task-update caller or the workflow service.
    provenance: observed
    source: 'services-crm:modules/tasks/services/persistence/tasks.ts:assignmentValues'
  - from: unrouted
    to: queued_waiting
    trigger: 'Place unheld work in a queue, including on creation.'
    guard: >-
      The queue exists locally; updates require unfinished and unlocked work. Inactive
      queues are accepted.
    side_effects:
      - Record arrival at the queue and use queue responsibility for enabled attention.
    who: A caller authorized for the corresponding task creation or update.
    provenance: test
    source: >-
      services-crm:modules/tasks/tests/integration/services/persistence/tasks.integration.spec.js#moves
      a task to another queue and re-stamps when it entered, so it joins the back of that
      one
  - from: queued_waiting
    to: queued_claimed
    trigger: Claim available queued work.
    guard: >-
      The conditional write requires an unfinished, unlocked, queued and unheld task, and
      a user caller.
    side_effects:
      - Set the caller as holder and assigner while retaining queue arrival.
    who: A user authorized to update tasks.
    provenance: test
    source: >-
      services-crm:modules/tasks/tests/integration/services/persistence/tasks.concurrency.integration.spec.js#lets
      exactly one of them have it
  - from: queued_claimed
    to: queued_waiting
    trigger: Release held queued work.
    guard: 'The task is unfinished, update-unlocked, held and queued.'
    side_effects:
      - Clear holder and assignment stamps without changing queue arrival.
    who: 'Any caller authorized to update tasks, including a service.'
    provenance: test
    source: >-
      services-crm:modules/tasks/tests/integration/services/persistence/tasks.integration.spec.js#leaves
      the task in its queue, in the place it had: a release is not a re-queue
  - from: queued_waiting
    to: queued_claimed
    trigger: Assign a user while retaining the queue.
    guard: The task is unfinished and updates are unlocked.
    side_effects:
      - Record assignment attribution without changing its retained queue position.
    who: A caller authorized to update tasks.
    provenance: observed
    source: 'services-crm:modules/tasks/services/persistence/tasks.ts:assignTask'
  - from: unrouted
    to: offered
    trigger: Create an offer or have the workflow service offer work to a role.
    guard: >-
      The role reference has valid syntax without an existence check; creation cannot also
      name a holder. A changed offer requires unfinished, unlocked work.
    side_effects:
      - Set the candidate role and clear any existing holder on service reassignment.
    who: A task-creation caller or the workflow service.
    provenance: test
    source: >-
      services-crm:modules/tasks/tests/integration/services/persistence/tasks.role-claim.integration.spec.js#refuses
      a create naming both an assignee and a role, naming the field, and writes nothing
  - from: offered
    to: offered_claimed
    trigger: Claim available work offered only to a role.
    guard: >-
      The locked task is unfinished, unlocked and unheld, and Platform confirms current
      role holding for the user.
    side_effects:
      - Set holder and assignment attribution while retaining the candidate role.
    who: A task-update user who currently holds the candidate role.
    provenance: test
    source: >-
      services-crm:modules/tasks/tests/integration/services/persistence/tasks.role-claim.integration.spec.js#is
      refused 403 with the code task_claim_not_role_holder, and the task stays offered
  - from: offered_claimed
    to: offered
    trigger: Release role-offered work or clear its holder by assignment.
    guard: The task is unfinished and update-unlocked; release additionally requires a holder.
    side_effects:
      - Clear assignment and retain the candidate role so work is offered again.
    who: A caller authorized to update tasks.
    provenance: test
    source: >-
      services-crm:modules/tasks/tests/integration/services/persistence/tasks.role-claim.integration.spec.js#keeps
      the candidate role, so the task is offered again
  - from: offered_claimed
    to: held
    trigger: Assign a different user to held role-offered work.
    guard: The task is unfinished and updates are unlocked.
    side_effects:
      - 'Replace the holder and assignment stamps, clearing the candidate role.'
    who: A task-update caller or the workflow service.
    provenance: test
    source: >-
      services-crm:modules/tasks/tests/integration/services/persistence/tasks.role-claim.integration.spec.js#clears
      the role when the task is given to somebody other than its holder
  - from: held
    to: queued_claimed
    trigger: Add a queue while retaining the holder.
    guard: 'The task is unfinished, unlocked and references a local queue.'
    side_effects:
      - Stamp arrival in the newly selected queue.
    who: A caller authorized to update tasks.
    provenance: observed
    source: 'services-crm:modules/tasks/services/persistence/tasks.ts:queueValues'
  - from: queued_waiting
    to: queued_offered_waiting
    trigger: Have the workflow service offer queued work to a role.
    guard: >-
      A changed request requires unfinished, unlocked work; an already-satisfied service
      request returns unchanged.
    side_effects:
      - 'Clear holder and assignment stamps, set the role and retain queue arrival.'
    who: The workflow service.
    provenance: observed
    source: 'services-crm:modules/tasks/services/persistence/tasks.ts:offerTask'
  - from: queued_claimed
    to: queued_offered_waiting
    trigger: Have the workflow service replace the holder with a role offer.
    guard: >-
      A changed request requires unfinished, unlocked work; an already-satisfied service
      request returns unchanged.
    side_effects:
      - 'Clear holder and assignment stamps, set the role and retain queue arrival.'
    who: The workflow service.
    provenance: observed
    source: 'services-crm:modules/tasks/services/persistence/tasks.ts:offerTask'
  - from: queued_offered_waiting
    to: queued_offered_claimed
    trigger: Claim work that has both queue and role routing.
    guard: >-
      A user makes a conditional claim of unfinished, unlocked, unheld work; role holding
      is not consulted.
    side_effects:
      - 'Set the caller as holder and assigner; retain the queue, arrival and role.'
    who: A user authorized to update tasks.
    provenance: observed
    source: 'services-crm:modules/tasks/services/persistence/tasks.ts:claimTask'
  - from: queued_offered_claimed
    to: queued_offered_waiting
    trigger: Release held work that retains both routing references.
    guard: 'The task is unfinished, unlocked and held.'
    side_effects:
      - Clear assignment stamps and holder while keeping queue arrival and candidate role.
    who: A caller authorized to update tasks.
    provenance: observed
    source: 'services-crm:modules/tasks/services/persistence/tasks.ts:releaseTask'
invalid:
  - from: unrouted
    to: held
    why: >-
      Claim cannot take unrouted work; assignment is required. A service reference cannot
      be its holder.
    provenance: observed
    source: 'services-crm:modules/tasks/services/persistence/tasks.ts'
  - from: queued_claimed
    to: queued_claimed
    why: >-
      A competing queue claim cannot replace an existing holder; the failed conditional
      write returns a conflict.
    provenance: observed
    source: 'services-crm:modules/tasks/services/persistence/tasks.ts'
  - from: offered
    to: offered_claimed
    why: A role-only offer remains unclaimed when Platform denies the caller’s role holding.
    provenance: observed
    source: 'services-crm:modules/tasks/services/persistence/tasks.ts'
  - from: unrouted
    to: queued_waiting
    why: >-
      The workflow creation schema has no queue input; queue placement uses another
      supported creation or edit path.
    provenance: observed
    source: 'services-crm:modules/tasks/services/persistence/tasks.ts'
concurrency: >-
  Conditional queue claims and releases admit at most one committed claimant for an
  available task. Role-only claims lock the task before checking current role authority.
  Claims and releases do not compare a read version, so a stale release can remove the
  current holder. Closing preserves routing, and reopening exposes the retained routing
  again.
invariants:
  - INV-DOM-CRM-ACT-003-03
  - INV-DOM-CRM-ACT-003-04
  - INV-DOM-CRM-ACT-003-05
  - INV-DOM-CRM-ACT-003-06
evidence:
  - >-
    services-crm at a8d1364f: modules/tasks/services/persistence/tasks.ts (assignTask,
    offerTask, claimTask, claimOfferedTask, releaseTask, assignmentValues, queueValues,
    candidateRoleAfterAssignment, holdsRole); db/tasks.table.ts (tasks_queued_at_check,
    tasks_assignment_stamps_check, tasks_candidate_role_assignee_check);
    docs/workstreams/crm-tasks-role-claim.md.
  - >-
    react-console at baf6dae3: lib/tasks/types.ts (canClaim, canRelease);
    components/tasks/task-detail-sheet.tsx (taskMenuItems).
  - >-
    services-crm:modules/tasks/services/persistence/tasks.ts:queueValues (queue changes
    preserve candidate roles unless a different assignee clears them; routing states are
    derived combinations, not persisted labels)
---

# Task Holding and Routing

## The Lifecycle

```mermaid
stateDiagram-v2
  unrouted --> held: route work
  held --> unrouted: route work
  unrouted --> queued_waiting: route work
  queued_waiting --> queued_claimed: claim
  queued_claimed --> queued_waiting: release
  unrouted --> offered: route work
  offered --> offered_claimed: claim
  offered_claimed --> offered: release
  offered_claimed --> held: route work
  held --> queued_claimed: route work
  queued_waiting --> queued_offered_waiting: offer to role
  queued_claimed --> queued_offered_waiting: offer to role
  queued_offered_waiting --> queued_offered_claimed: claim
  queued_offered_claimed --> queued_offered_waiting: release
```

Routing labels summarize combinations of holder, queue and candidate role; they are not persisted lifecycle values. The eight combinations include queued role offers. Priority between queue and role eligibility matters whenever both references are present.

## Transitions Explained

The structured rows describe assignment, queue entry, role offering, claiming and release. Ordinary queue edits can also move these combinations into or out of a queue while retaining a role. A different assignee clears a retained role; clearing a holder retains it. Queue release preserves the arrival instant.

## What Cannot Happen

An unrouted task cannot be claimed. A service cannot hold work. Creation refuses simultaneous holder and role inputs. Changed routing is restricted to unfinished, unlocked work, although already-satisfied workflow requests return before those restrictions.

## Concurrency and Timing

The database arbitrates queue claims, and a locked role claim checks authority at claim time. Publishing failures can still prevent a winning write from committing. The offered list selects only open, unheld offers without a queue and consults the user’s currently listed roles.
