Skip to main content

Digging Deeper into CDS Hooks for Personal Health Records

· 13 min read
Christopher Fu

This post will dive deeper into how personal health records (PHRs) can adopt the Clinical Decision Support (CDS) hooks standard to enable third-party extensibility. We're primarily focusing on CDS hooks because it is a more accessible standard for PHRs and third-party developers to adapt. For a high level context of why we care about this, please read Part 1.

The point of this post is to lay the groundwork and discuss how the CDS hook standard may need to be updated to handle PHR use cases. The great thing about building off a current standard like CDS hooks is that CDS hooks built for one PHR could be used for any PHR. For a deeper dive on CDS Hooks, read the HL7 Clinical Decision Support (CDS) hooks documentation.

Overview of CDS Hooks and their Limitations in Personal Health Records

To quickly summarize CDS hooks - they're HTTP webhooks that get called whenever a user of a clinical client, such as an Electronic Medical Record (EMR), takes a specific action. Examples of these actions include when a provider opens a patient's chart or when a provider starts a medication order for a patient. The idea is that registered third-party apps can be called whenever one of these actions is triggered and provide real-time feedback to the user about their actions. So, for example, a CDS application that can listen for when a doctor is entering a medication order for a specific drug in the EMR can suggest a different medication via a card in the UI of the EMR:

CDS hook flow

The CDS hook standard was primarily built for EMR systems and makes several assumptions:

  • There is a single FHIR store from which the third-party CDS hook can fetch the current patients records from
  • The hook trigger types are related to a clinical or EMR action.

There are a few reasons why these assumptions cause issues for PHRs:

  • PHRs may pull data from multiple sources, not a single FHIR source
  • Most of the currently specified hook types, e.g. the patient-view hook, don't make sense in PHRs as every view is a patient view. Other hooks like medication-prescribe and order-review are not actions users of a PHR would make.
  • PHRs may not expose an FHIR endpoint, especially if they are local-first like Mere is. Expecting PHRs to implement the entire FHIR server standard is unreasonable.

These assumptions limit the ability of PHRs to use CDS hooks in their current state.

Existing CDS Hooks Specification Technical Overview

If you're already familiar with the CDS hooks technical implementation, feel free to jump to my proposed updates to the CDS spec.

Let us dig deeper into how CDS hooks work technically.

First, all CDS hooks offered by a service are exposed via a discovery endpoint. The discovery endpoint reveals metadata that lets a clinical client know which CDS hooks are available from that service. The discovery endpoint is typically found at the url {baseURL}/cds-services and would return an array of hook metadata objects. Each object would have the following properties:

FieldOptionalityTypeDescription
hookREQUIREDstringThe hook this service should be invoked on. See Hooks.
titleRECOMMENDEDstringThe human-friendly name of this service.
descriptionREQUIREDstringThe description of this service.
idREQUIREDstringThe {id} portion of the URL to this service which is available at{baseUrl}/cds-services/{id}
prefetchOPTIONALobjectAn object containing key/value pairs of FHIR queries that this service is requesting the CDS Client to perform and provide on each service call. The key is a string that describes the type of data being requested and the value is a string representing the FHIR query.See Prefetch Template.
usageRequirementsOPTIONALstringHuman-friendly description of any preconditions for the use of this CDS Service.

A call to the endpoint would look something like this:

{
"services": [
{
"hook": "patient-view",
"title": "Static CDS Service Example",
"description": "An example of a CDS Service that returns a static set of cards",
"id": "static-patient-greeter",
"prefetch": {
"patientToGreet": "Patient/{{context.patientId}}"
}
},
{
"hook": "order-select",
"title": "Order Echo CDS Service",
"description": "An example of a CDS Service that simply echoes the order(s) being placed",
"id": "order-echo",
"prefetch": {
"patient": "Patient/{{context.patientId}}",
"medications": "MedicationRequest?patient={{context.patientId}}"
}
},
{
"hook": "order-sign",
"title": "Pharmacogenomics CDS Service",
"description": "An example of a more advanced, precision medicine CDS Service",
"id": "pgx-on-order-sign",
"usageRequirements": "Note: functionality of this CDS Service is degraded without access to a FHIR Restful API as part of CDS recommendation generation."
}
]
}

Now let us dig into a specific hook, which would be called at {baseURL}/cds-services/{id-from-discovery-url-metadata}

Currently, CDS hooks expect the following properties in the JSON body of a call :

FieldOptionalityTypeDescription
hookREQUIREDstringThe hook that triggered this CDS Service call. See Hooks.
hookInstanceREQUIREDstringA universally unique identifier (UUID) for this particular hook call (see more information below).
fhirServerCONDITIONALURLThe base URL of the CDS Client's FHIR server. If fhirAuthorization is provided, this field is REQUIRED. The scheme MUST be https when production data is exchanged.
fhirAuthorizationOPTIONALobjectA structure holding an OAuth 2.0 bearer access token granting the CDS Service access to FHIR resources, along with supplemental information relating to the token. See the FHIR Resource Access section for more information.
contextREQUIREDobjectHook-specific contextual data that the CDS service will need.For example, with the patient-view hook this will include the FHIR id of the Patient being viewed. For details, see the Hooks specific specification page (example: patient-view).
prefetchOPTIONALobjectThe FHIR data that was prefetched by the CDS Client (see more information below).

An example of a request to a CDS endpoint from a clinical client would currently look like this:

curl
-X POST \
-H 'Content-type: application/json' \
--data @hook-details-see-example-below \
"https://example.com/cds-services/static-patient-greeter"
{
"hookInstance": "d1577c69-dfbe-44ad-ba6d-3e05e953b2ea",
"fhirServer": "http://hooks.smarthealthit.org:9080",
"hook": "patient-view",
"fhirAuthorization": {
"access_token": "some-opaque-fhir-access-token",
"token_type": "Bearer",
"expires_in": 300,
"scope": "user/Patient.read user/Observation.read",
"subject": "cds-service4"
},
"context": {
"userId": "Practitioner/example",
"patientId": "1288992",
"encounterId": "89284"
},
"prefetch": {
"patientToGreet": {
"resourceType": "Patient",
"gender": "male",
"birthDate": "1925-12-23",
"id": "1288992",
"active": true
}
}
}

Which would return the following response to be rendered to the clinical client:

{
"cards": [
{
"uuid": "44a7eb84-ce90-45ac-a85b-04fdb4e76679",
"summary": "Now seeing: Daniel",
"source": {
"label": "Patient greeting service"
},
"indicator": "info"
}
]
}

What needs to change with CDS hooks to work with Personal Health Records

Based on the limitations discussed above, I'd suggest the following updates to the CDS specification:

Updating the hook Field to Handle PHR Actions and Use Cases

The hook field should be expanded to handle use cases commonly found in a PHR. For example, current hook types such as patient-view, medication-prescribe and order-review are very EMR centric and don't apply to PHRs.

It may make sense to introduce hooks that tie into the lifecycle events of the PHR, like a app-start hook that executes when the PHR starts, or a open-diagnostic-report/open-observation hook that is called whenever a patient decides to look at a specific set of lab values.

This limitation isn't a technical limitation of the current spec and is more because common PHR use cases have not been explored yet. As these use cases are discovered and converge across multiple PHR implementations, it may make sense to standardize several of them so that hooks can be transferrable across PHR implementations.

Updating the Hook Body to Handle Multiple FHIR Servers as Sources

The fhirServer and fhirAuthorization fields currently allow a hook to define a single FHIR server. Unless a PHR has also implemented its own FHIR server for external services to call, it is unlikely that this is a practical expectation for PHRs. Most PHRs that want to share FHIR resources with a hook would likely want to do so via one of two methods:

  1. Send a list of fhirServer/fhirAuthorization tuples that the PHR itself uses so that the hook can pull the resources from the FHIR servers itself or
  2. Send over just the required FHIR resources needed for the hook to execute as a property the context field of the hook with zero fhirServer/fhirAuthorization values provided.

One way to do this (while maintaining backward compatibility with existing hooks) would be to allow for a fhirSources array, where each array element contains a fhirServer element, a fhirAuthorization element.

So to summarize, in a CDS hook that does not require any external fhirServer to fetch data from, the properties fhirServer, fhirAuthorization, and fhirSources should be null or undefined. In a hook that requires exactly one external FHIR server, both the fhirServer and fhirAuthorization properties should be provided. In cases where the hook may expect one or more external FHIR server, the fhirSources array should be populated, and both fhirServer and fhirAuthorization should be null or undefined.

The context property would need to be updated as well. The context property technically can contain any data, but most hooks usually provide a userId, patientId, and encounterId as fields. These variables are primarily intended to be passed to the hook as "parameters" but can also be parameters used within the prefetch templates - essentially used to replace values in in the template strings. However, these three properties are usually specific to a single FHIR server. Therefore, if we want to pass multiple fhirServers as specified above, each will require its unique context to reference, especially when referring to prefetch templates.

To solve this problem in a backward-compatible way, we would introduce a new contexts root property that would act similarly to the current context property, however, the contexts object would contain key:value properties where the key would be the value of the fhirServer to apply the context to and the value would be a context values specific to the fhirServer specified. In addition, any objects provided inside the contexts object would act as an override for the more generic context object. For this reason, both the context and contexts root properties can co-exist.

An example of how the new fhirServices property and the new contexts property would work together is shown below:

Discovery Endpoint

{
"services": [
{
"hook": "patient-view",
"title": "Static CDS Service Example",
"description": "An example of a CDS Service that returns a static set of cards",
"id": "static-patient-greeter",
"prefetch": {
"patientsToGreet": "Patient/{{context.patientId}}"
}
}
]
}

CDS Hook Body

{
"hookInstance": "d1577c69-dfbe-44ad-ba6d-3e05e953b2ea",
"hook": "patient-view",
"fhirServices": [
{
"fhirServer": "http://hooks.smarthealthit.org:9080",
"fhirAuthorization": {
"access_token": "some-opaque-fhir-access-token",
"token_type": "Bearer",
"expires_in": 300,
"scope": "user/Patient.read user/Observation.read",
"subject": "cds-service4"
}
},
{
"fhirServer": "http://hooks.smarthealthit.org:9090",
"fhirAuthorization": {
"access_token": "some-opaque-fhir-access-token",
"token_type": "Bearer",
"expires_in": 300,
"scope": "user/Patient.read user/Observation.read",
"subject": "cds-service4"
}
}
],
"contexts": {
"http://hooks.smarthealthit.org:9080": {
"userId": "Practitioner/example",
"patientId": "1288992",
"encounterId": "89284"
},
"http://hooks.smarthealthit.org:9090": {
"userId": "Practitioner/example2",
"patientId": "1288994",
"encounterId": "89285"
}
},
"prefetch": {
"patientsToGreet": {
"resourceType": "Bundle",
"entry": [
{
"resourceType": "Patient",
"gender": "male",
"birthDate": "1925-12-23",
"id": "1288992",
"active": true
},
{
"resourceType": "Patient",
"gender": "male",
"birthDate": "1925-12-23",
"id": "1288993",
"active": true
}
]
}
}
}

Note that there are still some problems that need to be solved with this approach, like how a PHR will get access tokens to their sources that have a different scope provided to the PHR app itself (e.g. the PHR may have read access to all resource types but may only want to provide an access token to a hook with read access to AlleryIntolerance resources).

Make the Discovery Endpoint Expose what Properties are Expected in the Context

So admittedly, this next point is more of a nice-to-have instead of a need-to-have, but something I thought I would propose anyways.

The context field should be added to the CDS discovery endpoint and define the clinical client's expected properties. By doing this, we make the discovery of the required fields for a hook machine-parsable. This means that clinical clients can decide whether they can provide additional data into the context of the the hook automatically without an external developer needing to add explicit support for each additional context property. Each element in the context field of the discovery endpoint would be a key:value pair, where the key was the name of the context item, and the value was a CodableConcept representing the data type to be populated by the clinical client. See below for an example.

{
"services": [
{
"hook": "patient-view",
"title": "Static CDS Service Example",
"description": "An example of a CDS Service that returns a static set of cards",
"id": "static-patient-greeter",
"context": {
"userId": {
"coding": [
{
"system": "https://cds-hooks.org/hooks/patient-view/#context",
"code": "1",
"display": "The id of the current user. Must be in the format [ResourceType]/[id]"
}
],
"text": "The id of the current user. Must be in the format [ResourceType]/[id]"
},
"patientId": {
"coding": [
{
"system": "https://cds-hooks.org/hooks/patient-view/#context",
"code": "2",
"display": "The FHIR Patient.id of the current patient in context"
}
],
"text": "The FHIR Patient.id of the current patient in context"
},
"encounterId": {
"coding": [
{
"system": "https://cds-hooks.org/hooks/patient-view/#context",
"code": "3",
"display": "The FHIR Encounter.id of the current encounter in context"
}
],
"text": "The FHIR Encounter.id of the current encounter in context"
}
},
"prefetch": {
"patientsToGreet": "Patient/{{contexts.patientId}}"
}
}
]
}

What This Proposal Does Not Touch On

Adapting this standard to PHRs will open up some questions and considerations that don't necessarily apply to EHR clinical clients. For now I'll leave the handling of these specific questions down to implementers, but I'll mention a few considerations here:

  • What is the user flow for fetching and providing fhirAuthorization access tokens that have a narrower scope than the PHR parent app has? If a user has multiple FHIR sources, what is the user experience for granting access to only some of the sources to a hook?
  • How will PHR apps handle enabling and disabling of individual hooks? How can PHRs allow end users to add their own CDS discovery urls and hooks?

Proposed CDS Hooks For Personal Health Records

I'm still working on an initial spec for some PHR focused CDS hooks, which I'll explore in our next blog post. Stay tuned!

To Be Continued?

This post is still a work in progress and will likely evolve as PHR use cases are discovered and solidified. If you have more ideas about CDS hooks for PHR or want to start a discussion, please shoot me an email and I can edit this post.