Messaging Data Model
In a healthcare context, messages are sent all the time and can include many scenarios (patient to physician, physician to physician, and more), so ensuring they are well organized is important. This guide explains how to model and organize threads in Medplum.
Medplum messaging uses a two-level hierarchy of FHIR Communication resources: a thread header that represents the conversation, and child messages that hold the actual content. The same pattern supports provider-to-provider chat, patient-to-care-team messaging, and internal coordination.
This page covers:
- Representing individual messages and key
Communicationelements - Thread architecture and how to structure thread headers and messages
- Tagging and grouping threads with
category
For a working reference app, see the Contact Center Demo.
Thread Architecture
A thread header has no payload and no partOf. It exists to group messages. Each message has both: payload with content and partOf pointing at the header. That distinction is how you tell headers apart from messages when querying the API (for example part-of:missing=true matches thread headers). For more query patterns and live updates, see Searching and Querying Threads.
Representing Individual Messages
The FHIR Communication resource represents any healthcare message regardless of channel (email, SMS, in-app chat, and more).
| Element | Description | Relevant Valueset | Example |
|---|---|---|---|
payload | Text, attachments, or resources communicated to the recipient. On messages this is often contentString, contentAttachment, or contentReference. Omit on thread headers. | You have an appointment scheduled for 2pm. | |
sender | The person or team that sent the message. | Practitioner/doctor-alice-smith | |
recipient | The person or team that receives the message; can list multiple for group threads. On the thread header, include the full participant list (see Building and Structuring Threads). | Practitioner/doctor-gregory-house | |
topic | The main focus of the message, similar to an email subject line. Use a specific topic text. Set on both the thread header and every child message. | Custom internal text | In person physical with Homer Simpson on April 10th, 2023 |
category | The type of message being conveyed, like a tag applied to the thread or message. | HL7 Communication Category, SNOMED, custom | See below |
reasonCode | The specific reason the message was sent. Define a medical reason and/or a workflow reason: use the clinical findings subset of SNOMED for medical reasons and custom internal coding for workflow reasons. | SNOMED Clinical Findings, custom internal | 301180005 — Cardiovascular system normal (finding) |
partOf | On a message, reference to the thread header Communication. Empty on the thread header. | See Building and Structuring Threads | |
inResponseTo | Optional link to a specific prior message when the user explicitly replies to that message (for example a reply action). For linear chronological chat, partOf plus sorting by sent is usually enough. | Communication/previous-communication | |
medium | Channel or channels used; can be an array so one resource reflects multiple modalities. | Participation Mode Codes | |
subject | Patient or group the conversation is about. | Patient/homer-simpson | |
encounter | Optional link from the thread header to a session Encounter. See Representing Asynchronous Encounters. | Encounter/example-appointment | |
sent / received | When the message was sent or received. | 2023-04-10T10:00:00Z | |
status | Transmission and lifecycle state on the resource. Draft, sent, retracted, and related patterns are summarized in Communication Lifecycle. For read tracking via Tasks, see Read Receipts and Message Status. | Event Status Codes | in-progress |
category vs. reasonCodecategory classifies the message at a broad level (for example notification versus alert). reasonCode explains why it was sent in more detail (for example appointment reminder versus abnormal lab result). A message can carry both, such as category for notification and reasonCode for appointment reminder.
For every search parameter, see the Communication API reference.
Communication Lifecycle
| Stage | FHIR representation |
|---|---|
| Draft | Communication.status is preparation. |
| Sent | Communication.status is in-progress and Communication.sent is populated when applicable. |
| Read | Varies based on requirements; see Read Receipts and Message Status. |
| Retracted | Communication.status is entered-in-error for retract-and-correct or similar workflows. |
Building and Structuring Threads
Beyond single messages, most products group messages into threads. In FHIR, use a two-level hierarchy: one parent Communication as the thread header and child Communication resources as messages. Children link to the header with partOf.
The header represents the thread, not a specific message. It has no payload and no partOf. Child messages include payload and partOf pointing at the header.
Use a specific topic on both the header and every child so thread lists and message-level searches stay aligned.
sender and recipientSet sender to whoever created or opened the thread when that applies. Include that same actor in recipient, together with every other participant. Treat recipient on the header as the full participant list for routing and UI, not only others who received a message.
FHIR search cannot express recipient OR sender in one parameter. Inbox-style filters such as recipient=Practitioner/{id} only match headers where that practitioner appears in recipient, so omitting the thread creator breaks threads the user started. See Searching and Querying Threads for query patterns that rely on this convention.
Example of a thread grouped using a Communication resource
{
resourceType: 'Communication',
id: 'example-thread-header',
// Thread header: no partOf or payload
// Include the thread creator in recipient so recipient-based inbox search finds threads they started
sender: { reference: 'Practitioner/doctor-alice-smith' },
recipient: [{ reference: 'Practitioner/doctor-alice-smith' }, { reference: 'Practitioner/doctor-gregory-house' }],
topic: {
text: 'Homer Simpson April 10th lab tests',
},
},
// The initial message
{
resourceType: 'Communication',
id: 'example-message-1',
payload: [
{
id: 'example-message-1-payload',
contentString: 'The specimen for your patient, Homer Simpson, has been received.',
},
],
topic: {
text: 'Homer Simpson April 10th lab tests',
},
// ...
partOf: [
{
resource: {
resourceType: 'Communication',
id: 'example-thread-header',
status: 'completed',
},
},
],
},
// A response directly to `example-message-1` but still referencing the parent communication
{
resourceType: 'Communication',
id: 'example-message-2',
payload: [
{
id: 'example-message-2-payload',
contentString: 'Will the results be ready by the end of the week?',
},
],
topic: {
text: 'Homer Simpson April 10th lab tests',
},
// ...
partOf: [
{
resource: {
resourceType: 'Communication',
id: 'example-thread-header',
status: 'completed',
},
},
],
inResponseTo: [
{
resource: {
resourceType: 'Communication',
id: 'example-message-1',
status: 'completed',
},
},
],
},
// A second response
{
resourceType: 'Communication',
id: 'example-message-3',
payload: [
{
id: 'example-message-2-payload',
contentString: 'Yes, we will have them to you by Thursday.',
},
],
topic: {
text: 'Homer Simpson April 10th lab tests',
},
// ...
partOf: [
{
resource: {
resourceType: 'Communication',
id: 'example-thread-header',
status: 'completed',
},
},
],
inResponseTo: [
{
resource: {
resourceType: 'Communication',
id: 'example-message-2',
status: 'completed',
},
},
],
},
How to Tag or Group Threads
Tagging helps users interpret threads at a glance (for example a thread owned by nursing). Use Communication.category for that classification across purpose, audience, or nature.
Put category on both the thread header and child Communication resources. category is an array, so each resource can carry multiple tags.
| Type of tag | Codesystem |
|---|---|
| Level of credential | SNOMED Care Team Member Function valueset |
| Clinical specialty | SNOMED Care Team Member Function valueset |
| Product offering | SNOMED, LOINC, custom internal coding |
Example of multiple categories
{
resourceType: 'Communication',
id: 'example-communication',
status: 'completed',
category: [
{
text: 'Doctor',
coding: [
{
code: '158965000',
system: SNOMED,
},
],
},
{
text: 'Endocrinology',
coding: [
{
code: '394583002',
system: SNOMED,
},
],
},
{
text: 'Diabetes self-management plan',
coding: [
{
code: '735985000',
system: SNOMED,
},
],
},
],
};
You can use multiple category entries (for example separate entries for specialty and credential level). That tends to be self-documenting and easier to maintain, but queries may need to match several categories.
You can also use a single combined category code that encodes several dimensions. That can simplify search with one parameter and keep payloads smaller, but combinations explode over time and the app may need extra parsing.
See Also
- Searching and Querying Threads — queries, filters, subscriptions, and UI patterns
- Representing Asynchronous Encounters — linking thread headers to
Encounter - Sending Messages and Attachments —
payloadand attachments - Read Receipts and Message Status — unread and read state with Tasks
- Communication FHIR resource API