Skip to main content

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 and MedplumClient. If you still need a Medplum project, use Register.
  • Existing Patient and Practitioner resources in that project whose id values match the snippet references (or change every reference in 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 causes createResource to 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:

  1. 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-hosted baseUrl / token endpoint notes.
  2. Instantiate the client and call startClientLogin before any createResource or search calls. Use import { 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

  1. Thread header — A Communication with no payload and no partOf. It groups the conversation and carries topic, subject, participants, and optional category when you need tags (see Messaging Data Model).
  2. Each message — A Communication with payload (content) and partOf pointing at the same thread header, plus sender and recipient as needed. This walkthrough omits topic and category on 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 sets subject only on the thread header; add subject on 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:

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

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