On this page

This walkthrough creates a thread through the FHIR API using [`MedplumClient`](/content/docs/sdk/core.medplumclient). That matches how a real app (or Bot) writes messages. The [Medplum App](https://app.medplum.com/) 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](/content/docs/communications/messaging-data-model#building-and-structuring-threads/index.html).

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](/content/docs/communications/creating-your-first-thread#authenticate-with-client-credentials/index.html) below is usually the fastest path. For interactive login, bots, and other flows, see [Auth](/content/docs/auth/index.html) and [`MedplumClient`](/content/docs/sdk/core.medplumclient). If you still need a Medplum project, use [Register](/content/docs/tutorials/register/index.html).
- Existing [`Patient`](/content/docs/api/fhir/resources/patient/index.html) and [`Practitioner`](/content/docs/api/fhir/resources/practitioner/index.html) 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](https://app.medplum.com/) 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](/content/docs/communications/creating-your-first-thread#verification/index.html) 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](https://app.medplum.com/), open Project Admin → [Clients](https://app.medplum.com/admin/clients) and ensure you have a [`ClientApplication`](/content/docs/api/fhir/medplum/clientapplication/index.html). 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](/content/docs/auth/client-credentials/index.html) for details and for self-hosted `baseUrl` / token endpoint notes.
2. Instantiate the client and call [`startClientLogin`](/content/docs/sdk/core.medplumclient.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).

```ts
// 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`](/content/docs/api/fhir/medplum/clientapplication/index.html) must be allowed to create and read the resources you use (for example [`Communication`](/content/docs/api/fhir/resources/communication/index.html), `Patient`, `Practitioner`) via [access policies](/content/docs/access/access-policies/index.html) 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`](/content/docs/api/fhir/resources/communication/index.html) 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](/content/docs/communications/messaging-data-model#how-to-tag-or-group-threads/index.html)).
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](/content/docs/communications/messaging-data-model#building-and-structuring-threads/index.html)). 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](/content/docs/communications/messaging-data-model#building-and-structuring-threads/index.html).

```ts
// 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.

```ts
// 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`](/content/docs/api/fhir/resources/communication/index.html) 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`):

```ts
// 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`](/content/docs/sdk/core.medplumclient), project feature flags, and current limitations are covered in [FHIR Batch Requests](/content/docs/fhir-datastore/fhir-batch-requests/index.html).

## Verification

In the [Medplum App](https://app.medplum.com/), 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:

```ts
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:

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

```bash
medplum get 'Communication?part-of=Communication/{threadHeaderId}&_sort=sent'
```

```bash
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](/content/docs/access/access-policies/index.html)—your user must be allowed to read those `Communication` resources.
