Imaging Acknowledgement Shapes
Three imaging ack-row shapes: urgent dispatch, appointment prep-confirm, NM safety alert.
Status: Design reference. Closes bug-041 from the nuclear-medicine loop. Drives the bug-022 orchestrator-handler implementation.
Date: 2026-05-03
Source-of-truth schema: infrastructure/medbase/migrations/035_acknowledgement_requests.sql
Why this exists. The acknowledgement system has no separate “templates” table — templates are encoded into the orchestrator handler’s INSERT statement. This doc fixes the canonical shape of each imaging-related ack row so that (a) the bug-022 orchestrator change has a clear spec to implement against, (b) the AcknowledgementInbox UI can recognise + group these by
workflow_id, and © ack-escalator handles them correctly via theescalation_chainfield.
Shape 1 — imaging.urgent.dept_notify (covers bug-022 / TOR §4.1.5)
Trigger event. imagingRequest.accepted AND metadata.priority='URGENT' (at orchestrator handler handleImagingStatusUpdate)
{
"subject_order_type": "imaging",
"subject_order_id": "<imagingRequest._id-as-string>",
"subject_display": "URGENT imaging — <modality.name> for <patient.full_name> (HN <patient.hn>)",
"requester_user_id": "<accepting_user_id-uuid-or-null>",
"requester_display": "<accepting_user.display_name>",
"recipient_type": "department",
"recipient_department_id": "imaging",
"recipient_display": "Imaging department",
"reason": "Urgent imaging request requires departmental acknowledgement before queue dispatch.",
"priority": "urgent", // matches the ack-row priority enum
"status": "pending",
"deadline_at": "<now + INTERVAL '30 minutes'>",
"reminder_rule": "FREQ=MINUTELY;INTERVAL=10;COUNT=3", // remind every 10 min, max 3
"next_reminder_at": "<now + INTERVAL '10 minutes'>",
"escalation_chain": [
{ "after_minutes": 10, "to_role": "imaging_supervisor" },
{ "after_minutes": 25, "to_role": "radiologist_on_call" }
],
"escalation_step": 0,
"channels": ["app", "email", "push"],
"encounter_ref": "<encounter._id-uuid-or-null>",
"patient_ref": "<patient._id-uuid-or-null>",
"workflow_id": "imaging_urgent_dispatch",
"workflow_step": "department_notify"
}
Idempotency key (orchestrator-side): (subject_order_type='imaging', subject_order_id=<reqId>, workflow_id='imaging_urgent_dispatch', status='pending'). If a pending row already exists, skip insert.
Resolution. Imaging dept staff acknowledges → status='acknowledged', response_user_id=<staff>, response_at=now(). The escalator moves on if not acknowledged within escalation_chain[i].after_minutes.
Shape 2 — imaging.appointment.prep_confirm (covers TOR §3.15.5)
Trigger event. imagingRequest.scheduled AND the linked nuclear_med_procedure_template.preparation_instructions is non-null
{
"subject_order_type": "imaging",
"subject_order_id": "<imagingRequest._id-as-string>",
"subject_display": "Appointment prep — <modality.name> on <slot.start_at>",
"requester_user_id": "<scheduling_user_id>",
"requester_display": "<scheduling_user.display_name>",
"recipient_type": "user",
"recipient_user_id": "<patient.linked_user_id-or-null>",
"recipient_display": "<patient.full_name>",
"reason": "Confirm patient has received prep instructions for upcoming imaging exam.",
"priority": "routine",
"status": "pending",
"deadline_at": "<slot.start_at - INTERVAL '24 hours'>",
"reminder_rule": "FREQ=DAILY;INTERVAL=1;UNTIL=<slot.start_at - INTERVAL '12 hours'>",
"next_reminder_at": "<min(now + INTERVAL '24 hours', deadline_at)>",
"escalation_chain": [
{ "after_hours": 24, "to_role": "scheduling_nurse" }
],
"escalation_step": 0,
"channels": ["app", "sms"], // patient-facing, so SMS is in the channel list
"encounter_ref": "<encounter._id>",
"patient_ref": "<patient._id>",
"workflow_id": "imaging_appointment_confirm",
"workflow_step": "prep_received"
}
Idempotency key: (subject_order_type='imaging', subject_order_id=<reqId>, workflow_id='imaging_appointment_confirm').
Resolution. Patient acknowledges via patient portal → status='acknowledged', response_outcome='prep_received'. The scheduling nurse follows up if no response by deadline (escalation step 1).
Shape 3 — nuclear_medicine.safety.alert_critical (covers cross-cutting follow-up #2)
Trigger event. Postgres trigger on dose_safety_alerts insert with alert_level='critical' (table is created by nuclear_medicine_schema.sql; trigger logic is part of the dose-administration flow per 03-data-model.md §2.2)
{
"subject_order_type": "medication", // closest match in the ack enum
"subject_order_id": "nm_dose_safety_alert:<alert.id>",
"subject_display": "Critical dose-limit alert — <patient.full_name> (HN <patient.hn>) <isotope_name>",
"requester_user_id": null, // system-generated (no user actor)
"requester_display": "Dose-safety system",
"recipient_type": "role",
"recipient_role_id": "radiopharmacist",
"recipient_display": "Radiopharmacist + supervisor",
"reason": "<alert.alert_message_th> | <alert.alert_message_en>",
"priority": "stat", // highest level
"status": "pending",
"deadline_at": "<now + INTERVAL '2 hours'>",
"reminder_rule": "FREQ=MINUTELY;INTERVAL=15;COUNT=4",
"next_reminder_at": "<now + INTERVAL '15 minutes'>",
"escalation_chain": [
{ "after_minutes": 30, "to_role": "nuclear_medicine_supervisor" },
{ "after_minutes": 90, "to_role": "radiation_safety_officer" }
],
"escalation_step": 0,
"channels": ["app", "email", "push", "sms"],
"encounter_ref": "<dose.encounter_ref-or-null>",
"patient_ref": "<dose.patient_id>",
"workflow_id": "nuclear_medicine_safety_alert",
"workflow_step": "<alert.alert_type>" // e.g. 'dose_limit_exceeded', 'cumulative_warning'
}
Idempotency key: (subject_order_type='medication', subject_order_id='nm_dose_safety_alert:<alert.id>'). Each safety-alert row gets at most one ack row.
Resolution. Radiopharmacist or supervisor acknowledges + records the resolution path (override / hold / cancel future round) on the underlying dose_safety_alerts row. The ack closes; further alerts on the same patient generate new ack rows.
Implementation pointers (for the bug-022 orchestrator change)
The orchestrator handler that owns these shapes is at:
infrastructure/medbase/functions/encounter-orchestrator/index.ts
Suggested per-shape branch:
// Inside handleImagingStatusUpdate(event: HospitalEvent)
if (event.payload.status === 'accepted' &&
event.payload.metadata?.priority === 'URGENT') {
await ensureAckRow(supabase, {
subject_order_type: 'imaging',
subject_order_id: event.payload._id,
workflow_id: 'imaging_urgent_dispatch',
// … the shape from Shape 1 above, with values pulled from event.payload
});
}
if (event.payload.status === 'scheduled') {
const tpl = await loadProcedureTemplate(event.payload.procedure_template_id);
if (tpl?.preparation_instructions) {
await ensureAckRow(supabase, /* Shape 2 */);
}
}
// Shape 3 is triggered separately — by a Postgres trigger on dose_safety_alerts,
// not by the orchestrator.
ensureAckRow checks the idempotency key with SELECT 1 FROM acknowledgement_requests WHERE … before INSERT. The cron-driven nm-dose-round-reminder (registered in 20260503b_imaging_nm_phase3_followups.sql) emits a similar shape for multi-round dose reminders — that is a fourth shape but outside the orchestrator path.
Cross-references
- docs/architecture/acknowledgement-system.md — the canonical AcknowledgementRequest design (the universal entity these shapes plug into)
- docs/architecture/encounter-orchestrator-triggers.md — handler dispatch patterns
- infrastructure/medbase/migrations/035_acknowledgement_requests.sql — table schema
- infrastructure/medbase/migrations/036_cron_jobs_registry.sql — cron pattern (used by the dose-round-reminder)
- docs/nuclear-medicine-loop/03-data-model.md §7 — fan-out registry that surfaces these flows