Skip to main content

Messaging & Communications Decision Guide

Companion to the Messaging & Communications docs.

Section 1: Use Case & Participants

These questions establish the "who and why" before scoping any features. The answers should shape how you frame the rest of the conversation.

1.1 Who is messaging whom?

  • Patient ↔ provider
  • Provider ↔ provider
  • Care team internally (no patient in the thread)
  • System/automated → patient (one-way notifications)
  • Some combination of the above

Why it matters: different participant types affect access control, thread structure, and whether a patient subject is always required on a thread.

1.2 What is the primary purpose of messaging in their product?

  • Clinical care coordination (e.g. async visits, follow-ups)
  • Patient engagement / support
  • Internal operational communication
  • Outbound notifications only (reminders, results)

Why it matters: determines how much of the feature set is actually needed and whether async encounters/billing will come up.

1.3 Is this a new build or replacing something they have today?

  • If replacing: what are they replacing, and what are the gaps that drove this?
  • If new: is messaging central to the product or additive?

Why it matters: existing systems often carry assumptions about data model or UX that will surface as requirements. Knowing this early prevents surprises.


Section 2: Feature Scoping

Go through each together. For each: Yes / No / Nice-to-have / Not sure.

#FeatureYesNoNice-to-haveNot sure
1Live / real-time updates (new messages appear without refresh)
2Message response tracking and routing (assigning threads to providers or queues)
3Read receipts and unread counts
4File and image attachments
5Message editing and drafts
6Automated messaging (reminders, out-of-office, SLA escalations)
7External channel delivery (SMS, email — beyond in-app)
8Async encounters / billing

Section 3: Feature Deep Dives

Cover each feature flagged Yes or Nice-to-have. The goal is to land on a clear recommended approach by the end of each section.


3.1 Thread Structure (always covered)

Questions:

  • Are threads always tied to a specific patient, or do you need internal/admin threads with no patient context?
  • Are threads 1:1 only, or do you need group conversations with more than two participants?
  • Do threads need to be tagged or categorized (e.g. by specialty, product line, urgency)? If so, do tags need to be combinable (e.g. a thread tagged both "cardiology" and "urgent")?

What the answers drive:

SituationApproach
Threads always tied to a patientSet subject on every thread header — enables filtering by patient across the inbox
Internal/admin threads neededsubject is optional — don't assume it's always present in queries or UI
1:1 onlySimpler participant model; simplifies read receipt implementation (see 3.4)
Group threads neededRecipient list on thread header must include all participants including the thread creator
Tags/categories needed, single dimensionUse a single category entry on both the thread header and child messages
Tags/categories needed, multiple combinable dimensionsUse multiple category entries (e.g. one for specialty, one for credential level) — more maintainable but queries may need to match several categories

3.2 Live Updates

Questions:

  • Do new messages need to appear in real time without a page refresh?
  • Is this needed for the patient-facing app, the provider-facing app, or both?

What the answers drive:

SituationApproach
Real-time updates neededWebSocket subscriptions on the thread — requires this to be scoped and enabled for the project
Polling is acceptableSimpler implementation; periodic re-fetch of thread messages
Notification only (e.g. badge, push) without live message renderingCan use subscription to trigger a notification without requiring in-place message updates

3.3 Routing & Assignment

Questions:

  • Do messages get assigned to specific providers, to a pool/queue by role, or both?
  • Can threads be reassigned or escalated after initial assignment?
  • After reassignment, does the previous owner need to retain visibility of the thread?
  • Do you need a structured audit trail of who owned a thread and why it was rerouted?

What the answers drive:

SituationApproach
Individual assignment onlySet Task.owner to a specific provider; remove performerType
Pool/queue routingSet Task.performerType by role type; clear Task.owner; providers claim from pool
BothUse performerType for initial pool routing; set owner and clear performerType when claimed
Reassignment needed, previous owner loses accessUpdate Task.owner in place; update Communication.recipient to match
Reassignment needed, previous owner retains visibilityCreate a new Task for the new owner; mark original as cancelled with a note pointing to the new Task
Audit trail needed (free text)Append to Task.note on each reroute with author and timestamp
Audit trail needed (structured/queryable)Create a Provenance resource on each reroute with a coded reason — queryable via Provenance?target=Task/{id}

3.4 Read Receipts & Unread State

Questions:

  • Are threads ever group conversations (more than 2 participants)?
  • Do you need to query unread counts via API — e.g. a badge count or a list of all unread threads for a user?
  • Do unread counts need to be tracked per-recipient in a group thread?

What the answers drive:

SituationApproach
1:1 threads onlyOption A: mark message status = completed when read; query unread via status filter. Simplest model.
Group threads, no API unread query neededOption B: store per-participant read state as an extension on the thread header. Shows unread dot in UI; not searchable via API.
Group threads, API unread query neededOption C: create a read-receipt Task per recipient per message; complete it when read. Supports badge counts and unread queries. Most complex.

3.5 Attachments

Questions:

  • What file types need to be supported?
  • Do attachments need to be searchable or referenceable as clinical documents — e.g. queryable by patient or document type — or are they just files to download within a message?

What the answers drive:

SituationApproach
Files are just downloads within a messageStore as contentAttachment on the message payload — simple, no separate resource needed
Attachments need to be clinical documents (searchable, referenceable by patient/type)Create a DocumentReference and attach via contentReference on the payload — file becomes a first-class searchable clinical document. Note: this is the default behavior of Medplum's React ThreadChat component.
Attachments are references to existing clinical resources (e.g. a lab result)Use contentReference pointing at the relevant FHIR resource (e.g. DiagnosticReport)

3.6 Editing & Drafts

Questions:

  • Can sent messages be corrected or retracted after sending?
  • If so, does the original message content need to be discoverable via search (e.g. "show all edited messages"), or is it sufficient for the history to exist but not be directly queryable?
  • Do users need to save a draft and return to it across sessions or devices, or is per-device/browser draft storage acceptable?

What the answers drive:

SituationApproach
Sent messages cannot be editedNo special handling needed
Sent messages can be corrected; edits need to be searchableRetract-and-correct: mark original status = entered-in-error, create new message with corrected content linked via inResponseTo and tagged with a correction category. Corrections are discoverable via status and category filters.
Sent messages can be corrected; searchability of edits not requiredIn-place payload update via patchResource. Full version history still exists via _history but is not searchable.
Drafts only needed per browser/deviceStore in browser localStorage keyed by thread ID — no server resources needed, but lost if user clears storage or switches devices
Drafts need to persist across devices/sessionsStore as Communication with status = preparation; update as user types (debounced); promote to in-progress with a sent timestamp when sent. Filter drafts out of thread message queries with status:not=preparation.

3.7 Automations

Questions:

  • Are there events that should trigger automated messages (e.g. new lab result, appointment reminder)?
  • Are there SLA or response-time requirements (e.g. all messages responded to within 4 hours)?
  • Do providers have out-of-office or availability states that should affect routing?
  • Do you need to report on SLA compliance or response time metrics?

What the answers drive:

SituationApproach
Event-triggered automation (e.g. send message when lab result arrives)Bot triggered by a Subscription on the relevant resource type
Recurring automation (e.g. flag threads with no response after N days)Cron-scheduled Bot that scans for stale threads and creates reminder Tasks
Out-of-office / availability-based reroutingBot triggered on new message checks provider Schedule; reroutes Task to pool if provider has no available slots
SLA reporting neededTask data (authoredOn, status change timestamps, owner, priority) should be exported to an analytics platform — FHIR search is not designed for aggregate SLA metrics

3.8 External Channels

Questions:

  • Which channels — SMS, email, other?
  • Is this outbound-only (notify the patient) or does the patient reply back into the thread?
  • If inbound replies are needed, how is the patient identified from an incoming message (e.g. phone number, email address)?
  • Does the external provider (e.g. Twilio) have its own conversation ID that can be stored for thread matching?

What the answers drive:

SituationApproach
Outbound notifications onlyApp creates the Communication, then calls executeBot directly to send via the external channel — not Subscription-triggered, so errors surface synchronously
Inbound replies neededInbound webhook Bot receives the payload, identifies the patient, matches or creates a thread, creates a child Communication
External conversation ID available (e.g. Twilio conversation SID)Upsert thread header by that identifier on first message; use it as conditional partOf reference on all inbound messages — most reliable thread matching strategy
No external conversation IDMatch thread by patient phone/email and open thread status; ambiguous cases need a defined fallback strategy
Round-trip (provider replies in-app, patient receives via SMS/email)Store channel on thread header as medium; app creates the Communication then calls executeBot to dispatch outbound — Bot reads medium to route to the right channel
Webhook retry / duplicate messages likelyUse createResourceIfNoneExist with the provider's message ID as an identifier to prevent duplicate inbound Communications

3.9 Async Encounters / Billing

Questions:

  • Does the customer intend to bill for messaging-based care?
  • How do you define a "session" — one thread per session, grouped by day, or patient-initiated?
  • Can a single messaging session involve multiple patients (e.g. a parent asking about two children)?
  • Do you need to capture diagnosis codes, service type, or other clinical details per encounter?

What the answers drive:

SituationApproach
No billing intentNo Encounter resource needed; thread stands alone
Billing needed, one patient per sessionCreate an Encounter per session; link to the thread header via Communication.encounter
One thread = one sessionStraightforward 1:1 mapping of thread to Encounter
Rolling interaction model (e.g. continuous text thread, grouped by day)One Encounter spans multiple threads or a time window; multiple thread headers link to the same Encounter
Multiple patients can be in one sessionCreate a parent session Encounter + one child Encounter per patient via Encounter.partOf; clinical details live on child Encounters
Clinical documentation needed for billingPopulate Encounter.participant, Encounter.reasonCode, and Encounter.serviceType per patient Encounter

The external messaging channels Medplum integrates with first-party — both deliver through FHIR Communication resources, so they slot into the thread model above:

  • Twilio SMS — send and receive SMS via the $send-sms-twilio operation.
  • eFax — send and receive faxes via the $send-efax and $receive-efax operations.