Creating Your First Thread: A Step-By-Step Guide
This walkthrough creates a thread through the FHIR API using MedplumClient. That matches how a real app (or Bot) writes messages. The Medplum App is useful as an inspector: confirm resources, references, and history—not as the primary authoring experience you ship to users.
Before you start, read how thread headers and messages differ in Messaging Data Model — Building and Structuring Threads.
The example below is a provider–patient conversation: the patient is subject and is included in recipient on the thread header and on messages where they should see or receive the thread, alongside the clinician.
Prerequisites
- An authenticated
MedplumClient. For a small local script or this exercise, client credentials below is usually the fastest path. For interactive login, bots, and other flows, see Auth andMedplumClient. If you still need a Medplum project, use Register. - Existing
PatientandPractitionerresources in that project whoseidvalues match the snippet references (or change everyreferencein the code to match your data). Create or import them in the Medplum App or another flow you use for test data. Any reference that does not resolve causescreateResourceto fail with a server error pointing at the bad reference. If that happens, confirm the targets exist using the checks in Verification below.
Authenticate With Client Credentials
MedplumClient can authenticate in several ways. For machine-to-machine access (typical for a one-off script that creates threads), use OAuth2 client credentials:
- In the Medplum App, open Project Admin → Clients and ensure you have a
ClientApplication. Create one there if needed (same place you would configure or invite a client for API access). You need the application ID and Secret; see Client Credentials for details and for self-hostedbaseUrl/ token endpoint notes. - Instantiate the client and call
startClientLoginbefore anycreateResourceor search calls. Useimport { MedplumClient } from '@medplum/core';at the top of your script (same as the thread snippets below).
// Client credentials before FHIR calls; use real id/secret from Project Admin → Clients.
const medplum = new MedplumClient({ baseUrl: 'https://api.medplum.com/' });
const profile = await medplum.startClientLogin('YOUR_CLIENT_ID', 'YOUR_CLIENT_SECRET');
console.log(profile);
Replace the placeholders with the id and secret from the App. In your own repo, prefer reading them from the environment (for example process.env.MEDPLUM_CLIENT_ID) so secrets are not committed. The ClientApplication must be allowed to create and read the resources you use (for example Communication, Patient, Practitioner) via access policies on its project membership—otherwise requests will fail with 401/403 before you get to reference errors.
Create the Thread Header and Messages
- Thread header — A
Communicationwith nopayloadand nopartOf. It groups the conversation and carriestopic,subject, participants, and optionalcategorywhen you need tags (see Messaging Data Model). - Each message — A
Communicationwithpayload(content) andpartOfpointing at the same thread header, plussenderandrecipientas needed. This walkthrough omitstopicandcategoryon child messages to keep the example minimal; duplicate those on children when you need message-level display, search, or subscriptions aligned with the header (Messaging Data Model). This walkthrough setssubjectonly on the thread header; addsubjecton child messages too if your searches or profiles require it.
The first snippet creates the thread header and the opening message from the clinician to the patient. The header lists both participants in recipient; see the modeling note in Messaging Data Model.
// Thread header (no payload, no partOf) plus the first child message from the clinician.
// Provider–patient thread: replace Patient and Practitioner references with real ids from your project.
// Fixed `sent` values match the April 10th topic and sort predictably in examples; use real timestamps in production.
const createdThreadHeader = await medplum.createResource({
resourceType: 'Communication',
status: 'in-progress',
topic: {
text: 'Lab results for Homer Simpson - April 10th',
},
subject: {
reference: 'Patient/homer-simpson',
display: 'Homer Simpson',
},
sender: {
reference: 'Practitioner/doctor-alice-smith',
display: 'Dr. Alice Smith',
},
// Thread header lists every participant in recipient (including the sender) so inbox-style queries work; see Messaging Data Model.
recipient: [
{ reference: 'Patient/homer-simpson', display: 'Homer Simpson' },
{ reference: 'Practitioner/doctor-alice-smith', display: 'Dr. Alice Smith' },
],
// Optional in FHIR; included here so the header aligns with message times when demonstrating _sort=sent.
sent: '2024-04-10T09:00:00.000Z',
});
const walkthroughFirstMessage = await medplum.createResource({
resourceType: 'Communication',
status: 'in-progress',
partOf: [{ reference: `Communication/${createdThreadHeader.id}` }],
sender: {
reference: 'Practitioner/doctor-alice-smith',
display: 'Dr. Alice Smith',
},
recipient: [{ reference: 'Patient/homer-simpson', display: 'Homer Simpson' }],
payload: [
{
contentString:
'Hi Homer — we received your lab specimen and processing has started. We will message you here when results are ready.',
},
],
sent: '2024-04-10T10:00:00.000Z',
});
Reply From Another Participant
The next message is from the patient. In a real deployment you would normally create it from another app or login (for example the patient’s session) with its own authenticated MedplumClient. The block below keeps it in one tutorial file so you can run the flow locally; it reuses createdThreadHeader from the previous step.
// In production this createResource call would run as another user (e.g. patient portal) with their own MedplumClient session.
// It is shown in the same file so you can try the thread end-to-end; use the same `createdThreadHeader.id` from the step above.
const walkthroughSecondMessage = await medplum.createResource({
resourceType: 'Communication',
status: 'in-progress',
partOf: [{ reference: `Communication/${createdThreadHeader.id}` }],
sender: {
reference: 'Patient/homer-simpson',
display: 'Homer Simpson',
},
recipient: [{ reference: 'Practitioner/doctor-alice-smith', display: 'Dr. Alice Smith' }],
payload: [
{
contentString: 'Thanks — will the results be ready by the end of the week?',
},
],
sent: '2024-04-10T10:05:00.000Z',
});
Optional: Reply to a Specific Message
For linear chat, partOf plus sorting by sent is usually enough. Add inResponseTo only when the user explicitly replies to one message (for example a Reply action). The following snippet continues the variables from the steps above (createdThreadHeader, walkthroughSecondMessage):
// Use when the user explicitly replies to one message (not required for linear chat).
// Continues createdThreadHeader and walkthroughSecondMessage from the header, first message, and patient reply steps above.
const walkthroughReplyInResponseTo = await medplum.createResource({
resourceType: 'Communication',
status: 'in-progress',
partOf: [{ reference: `Communication/${createdThreadHeader.id}` }],
sender: { reference: 'Practitioner/doctor-alice-smith', display: 'Dr. Alice Smith' },
recipient: [{ reference: 'Patient/homer-simpson', display: 'Homer Simpson' }],
payload: [{ contentString: 'Yes — we expect your results by Thursday. We will notify you here.' }],
sent: '2024-04-10T10:15:00.000Z',
inResponseTo: [{ reference: `Communication/${walkthroughSecondMessage.id}` }],
});
Atomic Creates With Transaction Bundles
The steps above use separate createResource calls so each part of the thread is obvious. In production, you may prefer a single FHIR transaction Bundle so the thread header and first message are created together (all entries succeed or none are committed). Use fullUrl and urn:uuid references so the first message’s partOf can point at the header inside the same bundle. This tutorial does not include a full bundle example on purpose: step-by-step transaction patterns, the executeBatch helper on MedplumClient, project feature flags, and current limitations are covered in FHIR Batch Requests.
Verification
In the Medplum App, open Communication and find the new resources. The thread header should have no partOf and no payload; each message should have both.
If createResource failed with a reference error, confirm the Patient and Practitioner from the snippet exist (for example open Patient/homer-simpson in the App), or read them with the client:
await medplum.readResource('Patient', 'homer-simpson');
await medplum.readResource('Practitioner', 'doctor-alice-smith');
To list messages in the thread (newest last when using _sort=sent), run a search with the header’s id:
- 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'
Replace threadHeader.id (TypeScript) or {threadHeaderId} (CLI/cURL) with your thread header’s id. If nothing appears, check access policies—your user must be allowed to read those Communication resources.
See Also
- Communication FHIR resource API
- Client Credentials — OAuth2 client credentials and
ClientApplicationsetup - FHIR Batch Requests — batch and transaction
Bundlerequests,executeBatch - Searching and Querying Threads — thread lists, filters,
_revinclude, subscriptions - Sending Messages and Attachments —
payload, files,Binary - Contact Center Demo — full UI reference