Skip to main content

Publish and Subscribe

In a healthcare setting, Publish and Subscribe is a common pattern. For example, an everyday workflow in a laboratory setting is for a physician to create a lab order for a patient, and then receive a finalized lab report once the sample has been collected from the patient and processed.

In the past, this would have been a scenario where the patient is sent to a lab to get a blood test, and the lab report is faxed back to the doctor who ordered it.

This example will walk through how to technically using FHIR objects and FHIR Subscriptions. For those unfamiliar with FHIR Subscriptions, it is helpful to think of them a kind of webhook that is triggered on changes to a FHIR search filter.

This tutorial will walk through an common clinical example, to demonstrate both the functionality and a use case.

  • Create a ServiceRequest that "orders" the lab test
  • Update the ServiceRequest it moves through a workflow, for example - the sample is collected, the sample is analyzed, diagnostic report is created etc.
  • Create the Observations and DiagnosticReport that corresponds to the ServiceRequest above
  • Send a notification to another web application as the ServiceRequest is updated

Prerequisites

You will need to have a ClientApplication and ClientSecret in order to get this sample to work via the API. You can find your ClientApplications on Medplum.

If you just want to set up the FHIR notifications, you can drive the full workflow through the Medplum webapp by editing the objects from the web application.

Setting up the "Subscription"

In this example, we will set up the system to send a FHIR Subscription (webhook) to another application every time there is a change to a ServiceRequest.

To set up your Subscription page in Medplum and create a new subscription.

  • Make sure to set the status of the Subscription to "active" to ensure that the webhook is running
  • The Criteria section in the setup is what determines the triggering event for notifications. For example you put "ServiceRequest" in the Criteria section, all changes to ServiceRequests will generate a notification.
  • The Endpoint is the place where the subscribing web application URL should be placed. A full JSON representation of the object will be posted to the URL provided.
Subscriptions on AuditEvents

The Criteria of a subscription cannot be set to an AuditEvent resource. When a subscription is triggered it creates an AuditEvent, so using it as criteria would create a notification spiral.

You can find more instructions on setting up a subscription in the Medplum Bots documentation.

Before moving on to the rest of the tutorial, we recommend testing your subscription by attempting to trigger the webhook and inspect the data. If you have set up your webhook correctly you should see events when you create a new ServiceRequest or edit an existing ServiceRequest. You will also see AuditEvents created for the Subscription.

You can use any endpoint you like, and there are free services like Pipedream that you can use to set up an endpoint for testing purposes.

Creating the "Order" or ServiceRequest

This section shows how to create a ServiceRequest for a lab test needs that belongs to a Patient using the API. Notably, the snippet below conditional creates (only if patient does not exist) and creates a service request for a lab panel.

import { createReference, getReferenceString, MedplumClient, UCUM } from '@medplum/core';
import fetch from 'node-fetch';

/**
* Creates an order by creating Patient and ServiceRequest resources.
*
* We will use this in the "conditional create".
* When creating an order, and if you don't know if the patient exists,
* you can use this MRN to check.
* @param patientMrn - The patient medical record number (MRN).
*/
async function createServiceRequest(patientMrn: string): Promise<void> {
// First, create the patient if they don't exist.
// Use the "conditional create" ("ifNoneExist") feature to only create the patient if they do not exist.
const patient = await medplum.createResourceIfNoneExist(
{
resourceType: 'Patient',
name: [{ given: ['Batch'], family: 'Test' }],
birthDate: '2020-01-01',
gender: 'male',
identifier: [
{
system: 'https://namespace.example.health/',
value: patientMrn,
},
],
},
'identifier=' + patientMrn
);

const serviceRequest = await medplum.createResource({
resourceType: 'ServiceRequest',
status: 'active',
intent: 'order',
subject: createReference(patient),
code: {
coding: [
{
system: 'https://samplelab.com/tests',
code: 'A1C_ONLY',
},
],
},
});

// Should print "Patient/{id}"
console.log(getReferenceString(patient));

// Should print "ServiceRequest/{id}"
console.log(getReferenceString(serviceRequest));
}

Using this code snippet ServiceRequest was created and linked to a Patient. You should be able to see Patient created here and the ServiceRequest created here.

Because the ServiceRequest was created, the Subscription that was created in the previous section will trigger a web request to the provided endpoint.

Updating the status of the ServiceRequest as it moves through the workflow

After the ServiceRequest was created, it needs to be updated continuously as it moves through a workflow. It can be hard to visualize what is happening here, but the way to think about this from a perspective of a Patient getting a lab test.

  • A physician orders the test
  • The specimen is collected
  • The observation is determined by the analyzer and diagnostic report is created

Step 1 above was completed in the previous step, so the next step is to record a specimen collection and link it back to the ServiceRequest, and then update the ServiceRequest to indicate that the Specimen is available.

Step 2 can be accomplished using the below code snippet:

import { Specimen } from '@medplum/fhirtypes';

/**
* Creates a Specimen for a given ServiceRequest
* @param serviceRequestId - The ServiceRequest ID.
*/
async function createSpecimenForServiceRequest(serviceRequestId: string): Promise<void> {
// First, create the specimen resource
const specimen: Specimen = await medplum.createResource({
resourceType: 'Specimen',
status: 'available',
request: [
{
reference: `ServiceRequest/${serviceRequestId}`,
},
],
type: {
text: 'SERUM',
coding: [
{
system: 'https://namespace.specimentype.health/',
code: 'SERUM',
},
],
},
});

// Next, update the ServiceRequest to show that the specimen was collected.
const serviceRequest = await medplum.readResource('ServiceRequest', serviceRequestId);
serviceRequest.orderDetail = [
{
text: 'SAMPLE_COLLECTED',
},
];
const updatedServiceRequest = await medplum.updateResource(serviceRequest);

// Should print "Specimen/{id}"
console.log(getReferenceString(specimen));

// Should print "SAMPLE_COLLECTED"
console.log(updatedServiceRequest.orderDetail?.[0].text);
}

Creating an Observation and a DiagnosticReport

Now coming back to the core workflow, now that the specimen is collected, we need to run the samples on the lab instruments and produce the results.

  • A physician orders the test - COMPLETE
  • The specimen is collected - COMPLETE
  • The observation is determined by the analyzer and diagnostic report is created

Usually, this data is generated by a lab instrument or Laboratory Information System (LIS), or comes from a Laboratory provided FHIR interface. After the data is generated, it is important to update the status of the original ServiceRequest

import { DiagnosticReport, Observation } from '@medplum/fhirtypes';

async function createReport(patientId: string, serviceRequestId: string): Promise<void> {
// Retrieve the Patient and ServiceRequest
const patient = await medplum.readResource('Patient', patientId);
const serviceRequest = await medplum.readResource('ServiceRequest', serviceRequestId);

// Create the first Observation resource.
const observation: Observation = await medplum.createResource({
resourceType: 'Observation',
status: 'final',
basedOn: [createReference(serviceRequest)],
subject: createReference(patient),
code: {
coding: [
{
system: 'https://samplelabtests.com/tests',
code: 'A1c',
display: 'A1c',
},
],
},
valueQuantity: {
value: 5.7,
unit: 'mg/dL',
system: UCUM,
code: 'mg/dL',
},
});

// Create a DiagnosticReport resource.
const report: DiagnosticReport = await medplum.createResource({
resourceType: 'DiagnosticReport',
status: 'final',
basedOn: [
{
reference: serviceRequestId,
},
],
subject: {
reference: patientId,
},
code: {
coding: [
{
system: 'https://samplelab.com/testpanels',
code: 'A1C_ONLY',
},
],
},
result: [createReference(observation)],
});

// Next, update the ServiceRequest to show that the sample was processed and the report created.
serviceRequest.orderDetail = [
{
text: 'LAB_PROCESSED',
},
];
await medplum.updateResource(serviceRequest);

// Should print DiagnosticReport/{id}
console.log(getReferenceString(report));
}

In Conclusion

In this example we demonstrated the use of subscriptions and how to set them up for a simple lab workflow. FHIRPath subscriptions are very powerful and can be used to integrate systems with ease. This is similar to the workflow that large commercial labs use, and can be configured with additional features like advanced permissions (only get updates for very specific requests), multiples service types and more.