Searching and Querying Threads
How you populate sender and recipient on the thread header affects these queries—for recommended modeling (including listing the thread creator in recipient), see Organizing Communications — Building and Structuring Threads.
Search Query Cheat Sheet
| Goal | Query | Notes |
|---|---|---|
| All threads | Communication?part-of:missing=true | Returns only thread headers |
| Messages in a thread | Communication?part-of=Communication/{id}&_sort=sent | Sorted chronologically |
| Threads for a patient | Add &subject=Patient/{id} to any query above | Filter by patient context |
| Threads I participate in | Add &recipient=Practitioner/{id} | Header must list the creator in recipient; see note below and modeling tip |
| Active threads only | Add &status:not=completed,entered-in-error,stopped,unknown | Exclude closed threads |
Query for All Threads
- Typescript
- CLI
- cURL
// Search reference: find all thread headers, sorted by most recently active
await medplum.searchResources('Communication', {
'part-of:missing': true,
'status:not': 'completed,entered-in-error,stopped,unknown',
_sort: '-_lastUpdated',
});
medplum get 'Communication?part-of:missing=true&status:not=completed,entered-in-error,stopped,unknown&_sort=-_lastUpdated'
curl 'https://api.medplum.com/fhir/R4/Communication?part-of:missing=true&status:not=completed,entered-in-error,stopped,unknown&_sort=-_lastUpdated' \
-H 'authorization: Bearer $ACCESS_TOKEN' \
-H 'content-type: application/fhir+json'
The :missing modifier finds Communication resources that have no partOf reference — these are thread headers. Adding status:not=completed filters to only active threads. Sorting by -_lastUpdated (descending) puts the most recently active threads first.
Query for Messages in a Thread
- Typescript
- CLI
- cURL
// Retrieve all messages in a thread, sorted chronologically
await medplum.searchResources('Communication', {
'part-of': `Communication/${threadHeader.id}`,
_sort: 'sent',
});
medplum get 'Communication?part-of=Communication/{threadHeaderId}&_sort=sent'
curl 'https://api.medplum.com/fhir/R4/Communication?part-of=Communication/{threadHeaderId}&_sort=sent' \
-H 'authorization: Bearer $ACCESS_TOKEN' \
-H 'content-type: application/fhir+json'
This retrieves all child messages for a given thread header, sorted chronologically by the sent timestamp.
_revinclude with thread headersYou may encounter examples that use Communication?part-of:missing=true&_revinclude=Communication:part-of to return every thread header and every child message in one search response. That pattern tends to over-fetch: the bundle grows with the total number of messages across all matched threads and is expensive for the server. For typical UIs, load thread headers first, then run a separate query per thread (as above), using pagination where appropriate.
Add Filters
Filter threads by patient:
- Typescript
- CLI
- cURL
// Filter threads to a specific patient
await medplum.searchResources('Communication', {
'part-of:missing': true,
subject: 'Patient/homer-simpson',
});
medplum get 'Communication?part-of:missing=true&subject=Patient/homer-simpson'
curl 'https://api.medplum.com/fhir/R4/Communication?part-of:missing=true&subject=Patient/homer-simpson' \
-H 'authorization: Bearer $ACCESS_TOKEN' \
-H 'content-type: application/fhir+json'
Filter by Current User
To show threads the current user participates in, filter by recipient:
- Typescript
- CLI
- cURL
// Filter to only the current user's active threads
await medplum.searchResources('Communication', {
'part-of:missing': true,
recipient: getReferenceString(profile),
'status:not': 'completed,entered-in-error,stopped,unknown',
});
medplum get 'Communication?part-of:missing=true&recipient=Practitioner/{id}&status:not=completed,entered-in-error,stopped,unknown'
curl 'https://api.medplum.com/fhir/R4/Communication?part-of:missing=true&recipient=Practitioner/{id}&status:not=completed,entered-in-error,stopped,unknown' \
-H 'authorization: Bearer $ACCESS_TOKEN' \
-H 'content-type: application/fhir+json'
This works when thread headers list all participants — including the thread creator — in recipient. The Medplum ThreadInbox component follows this convention: when a new thread is created, the sender is added to recipient alongside other participants, so a recipient-based query returns threads the user started and threads they were added to.
Note: FHIR search does not support a single query for
recipient OR sender. If your thread headers don't follow the convention above, you'll need to run two searches (recipient=Practitioner/{id}andsender=Practitioner/{id}) and merge results client-side.
For how to model thread headers so this works, see the thread header sender / recipient recommendation in Organizing Communications.
For other items to filter on, see the Communication Search Parameters.
In Your UI
Rendering Thread Lists
The thread list query (part-of:missing=true) returns thread headers. Display topic.text as the thread title, recipient as participants, and subject as the patient context. Sort by _lastUpdated (descending) to show most recently active threads first.
For the message detail view, query with part-of=Communication/{id}&_sort=sent and render each message's payload, sender, and sent timestamp.
Live Updates
To receive new messages in real time, subscribe to Communications in the current thread. WebSocket subscriptions may need to be enabled for your project (for example, the websocket-subscriptions feature). The subscription criteria for a specific thread:
Communication?part-of=Communication/{threadId}
When a notification arrives, append the new message to your local state. The notification Bundle contains a SubscriptionStatus entry and the new Communication — find it by resourceType rather than by position:
const emitter = medplum.subscribeToCriteria(`Communication?part-of=Communication/${threadId}`);
emitter.addEventListener('message', (event) => {
const newMessage = event.payload.entry?.find(
(e) => e.resource?.resourceType === 'Communication'
)?.resource;
if (newMessage) {
// Show a toast or in-app notification for the new message
appendMessageToThread(newMessage);
}
});
If using @medplum/react, the useSubscription hook handles connection management:
useSubscription(
`Communication?part-of=Communication/${id}`,
(bundle: Bundle) => {
const newMessage = bundle.entry?.find(
(e) => e.resource?.resourceType === 'Communication'
)?.resource as Communication | undefined;
if (newMessage) {
// Show a toast or in-app notification for the new message
setMessages((prev) => [...prev, newMessage]);
}
}
);
See the useSubscription documentation for WebSocket connection setup and reconnection handling.
If using @medplum/react, the medplum-chat-demo provides working implementations of thread list and message detail views. The BaseChat component renders message lists with built-in attachment handling.