Pathology Specimen State Machine
Canonical specimen states, legal transitions, and the actor + UI surface that drives each.
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 |
Legal Transitions
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_specimenandsend_specimen(already wired inDialogSpecimenCollect.tsx) sit upstream ofpending— they govern the collection-side workflow, not the lab-side state machine.
Invariants
- Only one active state per specimen at a time — there is no parallel pathology workflow on a single specimen
releasedis terminal unless explicitly re-opened — re-opening creates an audit trail (amendment_reason,amended_by,amended_at)rejectedandcancelledare terminal — no transitions out- A specimen cannot skip backwards without an explicit
re_open— i.e.sign_outcannot return togrossing; you re-open all the way and re-walk - All transitions emit a
pathology.state_changedMoleculer event with{ specimenId, fromState, toState, actorId, timestamp }so the read-model projector can updatedepartment_queuesaccordingly
Edit Surface
The state machine itself is declarative — encoded in the workflow_templates row for pathology. To add a new state or transition:
- Open
/admin/pathology-workflow - Drag a new node from the palette
- Connect it with an edge
- 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”):
- Open
/admin/policy-gates - New rule,
trigger_action = pathology_release_report - Predicate:
actor.last_2fa_at < now() - interval '4 hours'→ action:block - Save — the Release button in the footer disables within ~300ms in every running session
No code change. No deploy.