medOS ultra

Imaging Acknowledgement Shapes

Three imaging ack-row shapes: urgent dispatch, appointment prep-confirm, NM safety alert.

4 min read diagramsUpdated 2026-05-05docs/architecture/imaging-acknowledgement-shapes.md

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 the escalation_chain field.


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

Ask Anything