HL7 FHIR CDS Hooks - A Practical Guide

Sidharth Ramesh

Sidharth Ramesh

Founder and CEO

A simple alert in the EHR about a patient's allergy can be the difference between a healthy patient death.

Clinical Decision Support is one of the key reason why many organisations adopt an Electronic Health Record. It is safe to say that clinical decision support is a key pillar improving patient outcomes and avoiding fatal errors.

There are a few options to embed clinical decision support within EHR today:

  1. Proprietary rules built-in to the EHR: This has been the status quo for a while. You depend on what comes built in.
  2. Executable rules that can be utilised by the EHR: Rules written in a specific domain language like FHIR's Clinical Quality Language (CQL) , Health Quality Measurement Framework (HQMF) or openEHR GDL can be interpreted by the EHRs to provide alerts. The NCQA has been releasing their popular HEDIS Digital Quality Measures in FHIR's CQL format for a while now (but there so far been no CQL engine capable of compiling their CQL releases). There are more than 660 free openEHR GDL2 rules available today covering common quality measures. In the very near future, I won't be surprised if we may see LLM-based "natural language format" executable rules that can be interpreted by EHRs.
  3. API based rules: The EHR calls APIs outside every time something important happens. These external APIs then decide which alerts to show. This option provides the most versatility. By delegating the logic of which alerts to trigger to a third party services via an API, you can support both 1, and 2 described above. To make this possible, the EHRs and the CDS API vendors need to agree on a standard.

In this article, we'll be looking at one such standard that makes it possible to define API based rules - The HL7 CDS Hooks Specification.

How CDS Hooks Work

Every time a key moment happens in an EHR, like opening up a patient, or when selecting an order, a web-hook is sent to various configured HTTP endpoints. These endpoints then respond back with information about which cards to show or actions to perform.

The below illustration from the CDS Hooks website captures what this interaction look like overall.

How does FHIR CDS Hooks work

We can define a few CDS Hooks terminologies based on what we now know:

  1. The EHR is also called the CDS Client.
  2. The act of opening up a patient is called the Hook - specifically patient-view.
  3. The configured HTTP endpoint is called the CDS Service.
  4. The cards to show are just called cards.
  5. And the actions to perform are systemActions.

Discovery of Supported CDS Hooks

After a CDS Service (HTTP Endpoint) is configured with a CDS Client (EHR), the CDS Client somehow needs to know all the different Hooks that the CDS Service supports. This is possible through the discovery process.

Let's assume the CDS Service is hosted on https://cds-service.com. During the discovery process, the CDS Client will make a request to this URL:

curl 'https://cds-service.com/cds-services'

And the CDS Service is expected to respond with something looking like

{
  "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}}"
      }
    }
  ]
}

In this response, the hook tells the CDS Client that both the patient-view and the order-select hooks are supported and the EHR can call it by the id appended to the base URL. For example, if there was a patient-view event, in the EHR, the request go to:

curl -X POST 'https://cds-service.com/cds-services/static-patient-greeter'

Avoid this gotcha: You cannot host your cds service discovery and endpoints on just the base URL https://cds-service.com. It always has to have a /cds-services path after the base URL for discovery as well as service hook requests.

✅ Good: https://cds-service.com/cds-services/static-patient-greeter

❌ Bad: https://cds-service.com/static-patient-greeter

You can still host your services on another base URL like https://cds-service.com/foo, but the CDS Service Discovery and CDS Service Hook will happen on https://cds-service.com/foo/cds-services and https://cds-service.com/foo/cds-services/static-patient-greeter respectively.

✅ Good: https://cds-service.com/foo/cds-services/static-patient-greeter

❌ Bad: https://cds-service.com/foo/static-patient-greeter

Requesting the EHR to Pre-Fetch Data

Notice the prefetch property inside the static-patient-greeter service?

{
    "patientToGreet": "Patient/{{context.patientId}}"
}

This instructs the EHR to pull additional information and include it in the POST request to the CDS Service endpoint https://cds-service.com/cds-services/static-patient-greeter .

In this case Patient/{{context.patientId}} just represents a REST API call to the FHIR server to retrieve the Patient FHIR resource of the current patient in context. For understanding FHIR REST APIs in depth, I suggest you take a look at the official REST API guide.

The CDS Client gets this information and includes it in the request.

curl
  -X POST \
  -H 'Content-type: application/json' \
  --data @see-below
  "https://cds-service.com/cds-services/static-patient-greeter"

{
  "hookInstance": "d1577c69-dfbe-44ad-ba6d-3e05e953b2ea",
  "fhirServer": "http://hooks.smarthealthit.org",
  "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
    }
  }
}

Notice the prefetch property in the JSON above. That's what our CDS Service had requested through the discovery endpoint. You will of course, also see the fhirServer and fhirAuthorization properties, and yes, you will be able to authorise, and make REST API calls directly to the FHIR server using the bearer token if you want to fetch additional data. For example:

curl 
    -H 'Authorization: Bearer some-opaque-fhir-access-token' \
    "http://hooks.smarthealthit.org/Observation?patient=1288992"

Returning CDS Cards Back to the EHR

What can our CDS Service now return in response to the POST request?

Let's assume there is nothing the CDS Service wants to show the EHR. It would just return:

{ "cards": [] }

Not very educative now, is it?

Let's look at what a real CDS Service's response might looks like with a card:

{
  "cards": [
    {
      "summary": "Say hello to Daniel!",
      "indicator": "info",
      "detail": "This is a static card that greets the patient from Medblocks.",
      "source": {
        "label": "Medblocks CDS Service",
        "url": "https://medblocks.com",
        "icon": "https://cms.infra.medblocks.com/assets/2e0e885d-99dc-445f-8a89-fd910d3f7db8"
      },
      "links": [
        {
          "label": "Google",
          "url": "https://google.com",
          "type": "absolute"
        },
        {
          "label": "SMART Example App",
          "url": "https://smart.example.com/launch",
          "type": "smart",
          "appContext": "{\"session\":3456356,\"settings\":{\"module\":4235}}"
        }
      ]
    }
  ]
}

This would make the EHR show up a card that look something like this

Medblocks CDS Service

Let's break this down:

  1. cards return all the CDS Cards that are of relevance
  2. summary provides a title to the card and key information can be displayed here. Here, we're asking the practitioner to greet Daniel
  3. source provides a link in the card that attributes the card to a particular CDS Service.
  4. links are displayed as buttons that can open up static websites or launch SMART on FHIR Apps!

There's a lot more that can be done with CDS Cards. Let's explore what else is possible.

CDS Suggestions

Let's take a simple example. Let's say a doctor prescribes a drug for a patient. However, a cheaper version of this same drug (generic) is available. How can we nudge the behaviour of the doctor to save this patient thousands of dollars every year?

Of course, our card can intercept the order-select hook and display a nice little card to plead, "Please prescribe a generic and save this patient $5660 / year". But better yet, our cards can provide recommendations on what should be changed on-screen with a click of a button!

Take a look at this video below to see how this is possible:

Let's explore how this was made possible with the CDS hook response:

{
  "cards": [
    {
      "uuid": "e99db916-9940-4143-b3eb-6e1ebef855be",
      "summary": "Cost: $1274.38. Save $1218.02 with a generic medication.",
      "source": {
        "label": "CMS Public Use Files"
      },
      "indicator": "info",
      "suggestions": [
        {
          "label": "Change to generic",
          "uuid": "acac1f17-d22b-4c9d-bc08-7cd1c8d26de8",
          "actions": [
            {
              "type": "create",
              "description": "Create a resource with the newly suggested medication",
              "resource": {
                "resourceType": "MedicationOrder",
                "id": "order-123",
                "status": "draft",
                "patient": {
                  "reference": "Patient/smart-1288992"
                },
                "dateWritten": "2024-07-05",
                "dosageInstruction": [
                  {
                    "timing": {
                      "repeat": {
                        "frequency": 1,
                        "period": 1,
                        "periodUnits": "d"
                      }
                    },
                    "doseQuantity": {
                      "value": 1,
                      "system": "http://unitsofmeasure.org",
                      "code": "{pill}"
                    }
                  }
                ],
                "medicationCodeableConcept": {
                  "text": "Ondansetron 4 MG Disintegrating Oral Tablet",
                  "coding": [
                    {
                      "code": "104894",
                      "system": "http://www.nlm.nih.gov/research/umls/rxnorm",
                      "display": "Ondansetron 4 MG Disintegrating Oral Tablet"
                    }
                  ]
                }
              }
            }
          ]
        }
      ],
      "overrideReasons": [
        {
          "code": "patient-requested-brand",
          "system": "http://terminology.cds-hooks.org/CodeSystem/OverrideReasons",
          "display": "Patient Requested Brand Product"
        },
        {
          "code": "generic-drug-unavailable",
          "system": "http://terminology.cds-hooks.org/CodeSystem/OverrideReasons",
          "display": "Generic Drug Out of Stock or Unavailable"
        }
      ]
    }
  ]
}

Let's take a look at how this works:

  1. suggestions clearly indicate what needs to be changed with the actions showing that a new MedicationOrder resource is to be created
  2. overrideReasons provide two different reasons as to why a practitioner may not want to accept this card. This will show up in the feedback that we give back to the CDS Service - we'll talk about this later.

And just like that, we've nudged the practitioner with a single click to save thousands of dollars for the patient! 🎉

Avoid this gotcha: The suggestions that are accepted change the state of the "scratchpad" or the draft resources on the practitioner's screen - i.e updating the medication order changes the draft prescription on-screen. It doesn't immediately change the state of the MedicationOrder resource on the FHIR Server.

✅ Your CDS Suggestions act on the practitioner's "scratchpad" or the local state of the EHR at any given time.

❌ Your CDS Suggestion does not act on the FHIR Server directly. Do not try to modify resources on the FHIR Server using the actions property of suggestions.

Automatic Actions and Autolaunch

There are a few other important things a CDS Response can make the CDS Client do. One of this is system actions - that allow the CDS Client to automatically apply changes to the "scratchpad" without requiring user intervention using the systemActions property of the response.

{
  "cards": [],
  "systemActions": [
    {
      "type": "update",
      "resource": {
        "resourceType": "ServiceRequest",
        "id": "example-MRI-59879846",
        "...": "<snipped for brevity"
      }
    }
  ]
}

You can also trigger the launch of SMART Apps and external links automatically without user intervention using the autolaunchable property of links:

{
  "cards": [
    {
      "uuid": "4e0a3a1e-3283-4575-ab82-028d55fe2719",
      "summary": "Lung cancer screening shared decision making",
      "detail": "Patient is a current smoker with a 20 pack/year history. Consider advising patient to complete lung cancer screening. The Lung Cancer Screening Shared Decision Making App (LCSSDM) has been proven to increase patient followthrough for screening.",
      "source": {
        "label": "Lung Cancer Screening Shared Decision Making App",
        "url": "https://example.com/LCS",
        "icon": "https://example.com/img/icon-100px.png"
      },
      "links": [
        {
          "label": "Github",
          "url": "https://github.com",
          "type": "absolute",
          "autolaunchable": true
        }
      ]
    }
  ]
}

With great power comes great responsibility. So use these features very carefully. It's finally up to the CDS Client (EHR) whether it accepts these automatic actions or not.

Feedback on CDS Cards

The CDS Client (EHR) can send feedback to the CDS Service on what was actually done with the cards. This includes the user accepting, rejecting or ignoring the card. This feedback comes in the form of a POST request. This can include the reason for rejection as well (or not).

If a user accepts a card, this is how the feedback to the CDS Service might look like:

POST {baseUrl}/cds-services/{serviceId}/feedback

{
  "feedback": [
    {
      "card": "4e0a3a1e-3283-4575-ab82-028d55fe2719",
      "outcome": "accepted",
      "acceptedSuggestions": [
        {
          "id": "e56e1945-20b3-4393-8503-a1a20fd73152"
        }
      ],
      "outcomeTimestamp": "2021-12-11T10:05:31Z"
    }
  ]
}

Avoid this gotcha: The feedback is specific to each CDS Service Hook. In the case of our patient greeter service on https://cds-service.com/cds-services/static-patient-greeter, the feedback on card acceptance / rejection will go to https://cds-service.com/cds-services/static-patient-greeter/feedback.

✅ Good: Implement a feedback endpoint for each service: https://cds-service.com/cds-services/static-patient-greeter/feedback or https://cds-service.com/cds-services/*/feedback.

❌ Bad: Implement a feedback route on the base URL directly: https://cds-service.com/cds-services/feedback

Practical Considerations

CDS Hooks transmits sensitive patient data outside of the EHR to a third party. Here are some important security considerations:

  1. Always use HTTPS for hosting your CDS Services to avoid man-in-the-middle attacks. For added security consider using mutual TLS between the CDS Client and the CDS Service.
  2. Many times, the CDS Client calls CDS Services from a browser environment. So it's important to enable Cross Origin Resource Sharing (CORS) on your CDS Service endpoints.

Beware of Alert Fatigue: CDS Hooks provide a great way to nudge the behaviour of doctors, it's important to understand Alert Fatigue and it's consequences. Provide alerts and cards on things that are crucial and can provide immediate value. Understand that the practitioner might see hundreds of alerts a day, and try to provide alerts that practitioners can trust and will look forward to seeing.

✅ Good: Provide high-value alerts that practitioners look forward to acting on.

❌ Bad: Spam a practitioner with trivial alerts just because you can.

What next?

So you've gotten the basics of how CDS Hooks works if you've made it this far. You may be tempted to checkout Epic's CDS Hooks documentation if you want to implement CDS Hooks today with a real-world EHR. I'd recommend that you try and tackle the original CDS Hooks specification first now that you have a better understanding of what the terminology means and how things work!

If you want to build your own CDS Hook Service with the sandbox, follow along with the video below.

Frequently Asked Questions

What are CDS Hooks?

CDS Hooks are a specification by HL7 that enables EHRs to integrate Clinical Decision Support from 3rd parties using RESTful HTTP APIs

What Is the Technology Behind CDS Hooks?

CDS Hooks is based on RESTful HTTP APIs. It utilizes the HL7 FHIR to model healthcare data and integrates well with the SMART on FHIR authorization framework to launch external applications as a link on a CDS Card.

What are the benefits of Implementing CDS Hooks in a Healthcare System?

By implementing CDS Hooks, Healthcare Systems obtain the ability to delegate the Clinical Decision Support logic to 3rd parties. This can be a powerful way to ensure that clinicians always receive the latest and most relevant alerts in their EHR, without having to constantly upgrade the EHR system. CDS Hooks can also integrate the provider's workflow with payers and enable point-of-care quality measures and pre-authorization.

What are the use cases of CDS Hooks in a clinical setting?

Some use cases of CDS Hooks in the clinical setting are to display Drug-Drug or Drug-Allergy Interaction alerts, Insurance Pre-Authorization alerts, Nudges to improve Quality Measures and Reduce the cost of services.

How to start Developing with CDS Hooks?

CDS Hooks development can be simple and straightforward if you know how to build a JSON REST Server. Watch our step-by-step guide at How To Create A FHIR CDS Hook Service

What are some challenges and limitations of CDS Hooks?

CDS Hooks are a relatively recent specification and have limited EHR support. The specification currently only has a few mature hooks like - patient-view, order-select, and order-sign. The specification also leaves a lot of the implementation details up to the CDS Client which might mean CDS Services might have to develop slightly different versions of their software for each EHR.

Further reading

  1. Original CDS Hooks Specification
  2. FHIR Clinical Decision Support Services
  3. CDS Hooks implementation by Davinci FHIR Profile for Payers
  4. Josh Mandel's Demo of CDS Hooks on YouTube
  5. Video and Guide on CDS Hooks by Kevin Maloy, MD
  6. Does Cerner Support CDS Hooks?
  7. Impact of integrating public health clinical decision support alerts into electronic health records on testing for gastrointestinal illness
  8. Persisted Resource vs ScratchPad on Github Issues
  9. Some Issues with CDS Hooks Dynamic Behaviour on Github
  10. FHIR Transaction Like behaviour on CDS Hooks Github Pull Request
  11. Leveraging CQL and CDS hook to enhance CDS - Talk at DevDays 2023 by Wylem Bars
  12. openEHR GDL2 with CDS Hooks and SMART apps on YouTube

Become a FHIR Expert Today — Join Our Free Webinar

Unlock the secrets to mastering FHIR in our exclusive, free live webinar. Discover why now is the prime time to excel and how you can become an expert.