medOS ultra

Pathology Specimen State Machine

Canonical specimen states, legal transitions, and the actor + UI surface that drives each.

4 min read diagramsUpdated 2026-05-01docs/pathology/state-machine.md

Canonical states for a pathology specimen, the legal transitions, and which actor + UI surface drives each one.

Canonical States

The state names align with EnumPathologyStatusVJRD in web/packages/diagnostics-kit/src/pathology/type.ts.

# State English Label Thai Label Description
0 pending Awaiting Specimen รอสิ่งส่งตรวจ Order placed, specimen not yet arrived at the lab
1 received Registered ลงทะเบียนรับแล้ว Specimen accepted into the lab; barcode scanned
2 grossing Gross Examination ตัดชิ้น (Grossing) Macroscopic exam, block selection
3 slide_prep Slide Preparation เตรียมสไลด์ H&E / IHC / special stain prep
4 slide_scan Slide Scanning สแกนสไลด์ Digital pathology scan (optional)
5 sign_out Pending Sign-out นำส่งพยาธิแพทย์ Pathologist review queue
6 released Released ตรวจสอบแล้วรายงานผลได้ Final report visible to ordering clinician
X rejected Rejected ปฏิเสธ Specimen rejected at registration
X cancelled Cancelled ยกเลิก Order cancelled by clinician
   pending ──── accept ────▶ received ────▶ grossing ────▶ slide_prep ────▶ slide_scan ────▶ sign_out ────▶ released
      │                          │              │                                                  │
      │                          │              │                                                  ▼
      └─── cancel ──────────▶ cancelled         │                                              amend_request
                                                ▼                                                  │
                                            rejected                                               ▼
                                                                                              re_open ──▶ sign_out
From To Trigger Actor UI Surface
pending received accept_specimen Specimen Handler Footer or Worklist row action
pending rejected reject_specimen Specimen Handler Footer (modal: rejection reason)
pending cancelled cancel_order Ordering Clinician Order details page
received grossing start_grossing Pathologist Assistant Footer
grossing slide_prep complete_grossing Pathologist Assistant Modal (block list)
slide_prep slide_scan complete_slide_prep Lab Tech Footer
slide_prep sign_out skip_scan Lab Tech Footer (if scan disabled)
slide_scan sign_out complete_scan Lab Tech Footer
sign_out released release_report Pathologist Modal (signature + diagnosis)
released sign_out re_open Pathologist Modal (amendment reason)

Trigger ↔ Policy Gate Mapping

Each transition has a corresponding policy_gates.trigger_action value the rule engine can target. An admin editing a row in /admin/policy-gates with one of these triggers will gate the corresponding button.

Transition trigger_action
Accept pathology_accept_specimen
Reject pathology_reject_specimen
Start Grossing pathology_start_grossing
Complete Grossing pathology_complete_grossing
Complete Slide Prep pathology_complete_slide_prep
Complete Scan pathology_complete_scan
Release pathology_release_report
Re-open / Amend pathology_amend_report

Existing triggers collect_specimen and send_specimen (already wired in DialogSpecimenCollect.tsx) sit upstream of pending — they govern the collection-side workflow, not the lab-side state machine.

Invariants

  1. Only one active state per specimen at a time — there is no parallel pathology workflow on a single specimen
  2. released is terminal unless explicitly re-opened — re-opening creates an audit trail (amendment_reason, amended_by, amended_at)
  3. rejected and cancelled are terminal — no transitions out
  4. A specimen cannot skip backwards without an explicit re_open — i.e. sign_out cannot return to grossing; you re-open all the way and re-walk
  5. All transitions emit a pathology.state_changed Moleculer event with { specimenId, fromState, toState, actorId, timestamp } so the read-model projector can update department_queues accordingly

Edit Surface

The state machine itself is declarative — encoded in the workflow_templates row for pathology. To add a new state or transition:

  1. Open /admin/pathology-workflow
  2. Drag a new node from the palette
  3. Connect it with an edge
  4. Save — the new transition is now legal everywhere the footer + modal renderer + aftersave hooks are consumed

To gate an existing transition (e.g., “Release report only if pathologist has 2FA verified within last 4 hours”):

  1. Open /admin/policy-gates
  2. New rule, trigger_action = pathology_release_report
  3. Predicate: actor.last_2fa_at < now() - interval '4 hours' → action: block
  4. Save — the Release button in the footer disables within ~300ms in every running session

No code change. No deploy.

Ask Anything