Skip to main content

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

GoalQueryNotes
All threadsCommunication?part-of:missing=trueReturns only thread headers
Messages in a threadCommunication?part-of=Communication/{id}&_sort=sentSorted chronologically
Threads for a patientAdd &subject=Patient/{id} to any query aboveFilter by patient context
Threads I participate inAdd &recipient=Practitioner/{id}Header must list the creator in recipient; see note below and modeling tip
Active threads onlyAdd &status:not=completed,entered-in-error,stopped,unknownExclude closed threads

Query for All Threads

// 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',
});

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

// Retrieve all messages in a thread, sorted chronologically
await medplum.searchResources('Communication', {
'part-of': `Communication/${threadHeader.id}`,
_sort: 'sent',
});

This retrieves all child messages for a given thread header, sorted chronologically by the sent timestamp.

About _revinclude with thread headers

You 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:

// Filter threads to a specific patient
await medplum.searchResources('Communication', {
'part-of:missing': true,
subject: 'Patient/homer-simpson',
});

Filter by Current User

To show threads the current user participates in, filter by recipient:

// 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',
});

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} and sender=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.