CDS Hooks are services called by CDS Clients (typically Electronic Health Record Systems (EHRs) or other health information systems). They implement a "hook"-based pattern for invoking decision support from within a clinician's workflow.
Smile CDR implements Version 1.1 of the CDS Hooks Specification.
The Smile CDR CDS Hooks Module simplifies the effort for creating CDS Hooks. All you need to do is create a method that accepts a CdsServiceRequestJson
parameter and returns a CdsServiceResponseJson
value and annotate this method with the @CdsService
annotation. This annotation and the Json classes and all their subcomponents are available in the open-source project called cdr-api-public
. Any FHIR resources in requests and responses are automatically serialized into hapi-fhir FHIR resource instances for you, so they are easy to work with within your code.
In addition to simplifying the effort to build CDS Hooks, the Smile CDR CDS Hooks module also provides the following:
The Smile CDR CDS Hooks module provides a couple of powerful Auto-Prefetch features:
allowAutoFhirClientPrefetch
is set to true
in the @CdsService
annotation on your CDS Service method, then
before calling your method, Smile CDR will compare the prefetch elements declared by your service method in
the @CdsService
annotation to the prefetch elements included within the CdsServiceRequestJson
REST request and if
it detects any are missing, then Smile CDR will use the FHIR endpoint authorization details included within
the fhirAuthorization
element in the request to automatically add them to the prefetch before calling your method.null
is not considered to be missing. CDS Hooks clients set a
prefetch value to null
to indicate that this prefetch data is known to not exist).fhirServer
is included in the request
@CdsService
annotation on the service method has allowAutoFhirClientPrefetch = true
, then Smile CDR will
perform a FHIR REST call to that fhirServer
endpoint to fetch the missing data.fhirServer
endpoint itself to retrieve the
missing data.source
for the @CdsServicePrefetch
.
Smile CDR will attempt to use the source
strategy for the query instead of following the order above.The diagram below shows how CDS Hooks work. The box in grey contains customer code, which is code that you write.
A CDS Hooks implementation is packaged as a Java JAR file that contains several key components:
A CDS Hooks class contains annotated service and feedback methods. One CDS Hooks class can contain any number of these methods. A CDS Hooks service method is annotated with the @CdsService
annotation and a CDS Hooks feedback method is annotated with the @CdsServiceFeedback
annotation. The "value" of these annotations corresponds to the id of the CDS Hooks service. For example:
A method annotated with @CdsService(value="example-service")
is accessed at a path like https://example.com:8888/cds-services/example-service
A method annotated with @CdsServiceFeedback(value="my-service")
is accessed ata path like https://example.com:8888/cds-services/my-service/feedback
.
A very basic example is shown below:
package ca.cdr.endpoint.cdshooks.module;
import ca.uhn.hapi.fhir.cdshooks.api.CdsService;
import ca.uhn.hapi.fhir.cdshooks.api.CdsServiceFeedback;
import ca.uhn.hapi.fhir.cdshooks.api.CdsServicePrefetch;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceAcceptedSuggestionJson;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceFeedbackJson;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceIndicatorEnum;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceRequestJson;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceResponseCardJson;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceResponseCardSourceJson;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceResponseJson;
import org.hl7.fhir.r4.model.Patient;
import java.util.List;
import java.util.UUID;
public class ExampleCdsService {
@CdsService(value = "example-service",
hook = "patient-view",
title = "Greet a patient",
description = "This service says hello to a patient",
prefetch = {
@CdsServicePrefetch(value = "patient", query = "Patient/{{context.patientId}}")
})
public CdsServiceResponseJson exampleService(CdsServiceRequestJson theCdsRequest) {
Patient patient = (Patient) theCdsRequest.getPrefetch("patient");
CdsServiceResponseJson response = new CdsServiceResponseJson();
CdsServiceResponseCardJson card = new CdsServiceResponseCardJson();
card.setSummary("Hello " + patient.getNameFirstRep().getNameAsSingleString());
card.setIndicator(CdsServiceIndicatorEnum.INFO);
CdsServiceResponseCardSourceJson source = new CdsServiceResponseCardSourceJson();
source.setLabel("Smile CDR");
card.setSource(source);
response.addCard(card);
return response;
}
@CdsServiceFeedback("example-service")
public CdsServiceFeedbackJson feedback(CdsServiceFeedbackJson theFeedback) {
theFeedback.setAcceptedSuggestions(List.of(new CdsServiceAcceptedSuggestionJson().setId(UUID.randomUUID().toString())));
return theFeedback;
}
}
Both of these example methods accept a single json instance parameter (CdsServiceRequestJson
and CdsServiceFeedbackJson
respectively). Alternatively, these methods can accept a single String parameter in which case the CDS Hooks module will string-encode the instance before calling the method.
This mandatory class is a Spring Framework Annotation-based Application Context Config class. It is characterized by having the @Configuration
annotation on the class itself, as well as having one or more non-static factory methods annotated with the @Bean
method, which create instances of your providers (as well as creating any other utility classes you might need, such as database pools, HTTP clients, etc.).
This class must instantiate a bean named cdsServices
:
cdsServices
bean method should return a List<Object>
of classes that contain @CdsService
and/or @CdsServiceFeedback
annotated methods.The following example shows a Spring Context Config class that registers the CDS Hooks example above.
@Configuration
public class TestServerAppCtx {
/**
* This bean is a list of CDS Hooks classes, each one
* of which implements one or more CDS-Hook Services.
*/
@Bean(name = "cdsServices")
public List<Object> cdsServices(){
List<Object> retVal = new ArrayList<>();
retVal.add(new ExampleCdsService());
// add other CDS Hooks classes...
return retVal;
}
}
The CDS Hooks specification introduces Extensions as a part of CDS hooks request and response.
Also, a CDS service should also be able to expose which Extension
it supports on the Discovery
endpoint. In order to support extensions for your implementation, follow the steps:
CdsHooksExtension
. A sample is shown below:public class MeasurementUnitExtension extends CdsHooksExtension {
@JsonProperty("age-measurement-unit")
private String myAgeMeasurementUnit;
public String getAgeMeasurementUnit() {
return myAgeMeasurementUnit;
}
public void setAgeMeasurementUnit(String theAgeMeasurementUnit) {
myAgeMeasurementUnit = theAgeMeasurementUnit;
}
}
Extension
your CDS service supports and the custom class. The custom class is used for deserialization of the incoming CDS hook request Extension
. A sample CDS service declaration is shown below alongside usage of CDS hooks request Extension
to manipulate the CDS hooks response:@CdsService(
value = SERVICE_ID,
hook = "patient-view",
title = "Age",
description = "Age calculated from patient birthday",
prefetch = {@CdsServicePrefetch(value = "patient", query = "Patient/{{context.patientId}}")},
allowAutoFhirClientPrefetch = true,
extension = """
{
"age-measurement-unit": "years or months"
}
""",
extensionClass = MeasurementUnitExtension.class)
public CdsServiceResponseJson calculateAge(CdsServiceRequestJson theCdsServiceRequestJson) {
Patient patient = (Patient) theCdsServiceRequestJson.getPrefetch(PATIENT_KEY);
if (patient == null) {
throw new InvalidRequestException("'patient' prefetch resource missing from request.");
}
String ageMeasurementUnit = YEARS;
// Get the extension passed in the request
MeasurementUnitExtension extension = (MeasurementUnitExtension) theCdsServiceRequestJson.getExtension();
if (!StringUtils.isEmpty(extension.getAgeMeasurementUnit())) {
ageMeasurementUnit = (MONTHS).equals(extension.getAgeMeasurementUnit()) ? MONTHS : YEARS;
}
CdsServiceResponseJson response = new CdsServiceResponseJson();
CdsServiceResponseCardJson card = new CdsServiceResponseCardJson();
Integer ageInRequestedMeasurement = calculateAge(patient, ageMeasurementUnit);
String name = getName(patient);
if (ageInRequestedMeasurement == null) {
card.setSummary(name + "'s age is unknown");
} else {
card.setSummary(name + " is " + ageInRequestedMeasurement + " " + ageMeasurementUnit + " old.");
}
card.setIndicator(CdsServiceIndicatorEnum.INFO);
CdsServiceResponseCardSourceJson source = new CdsServiceResponseCardSourceJson();
source.setLabel("ACME Age Calculator");
card.setSource(source);
response.addCard(card);
return response;
}
private String getName(Patient thePatient) {
if (thePatient == null) {
return null;
}
List<HumanName> names = thePatient.getName();
if (names == null || names.size() == 0 || names.get(0).isEmpty()) {
return "This patient";
}
HumanName nameItem = names.get(0);
return nameItem.getNameAsSingleString();
}
private Integer calculateAge(Patient thePatient, String theAgeMeasurementUnit) {
if (thePatient == null) {
return null;
}
Date birthDate = thePatient.getBirthDate();
if (birthDate == null) {
return null;
}
LocalDate localBirthDate =
birthDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
int ageDifferenceInYears = Period.between(
localBirthDate,
Instant.now().atZone(ZoneId.systemDefault()).toLocalDate())
.getYears();
return theAgeMeasurementUnit.equals(MONTHS) ? ageDifferenceInYears * 12 : ageDifferenceInYears;
}
This section outlines considerations when building your CDS Hooks classes.
In order to build your Smile CDR CDS Hooks, you will need the annotation and json classes on your path. Currently, this is accomplished by adding the cdr-api-public-*.jar file to your build classpath. This jar file can be found in your smilecdr/lib folder. In the future, this jar will be published to a central repository and available as a maven dependency.
See Library Support for details concerning libraries available on the classpath where your CDS Hooks classes are loaded.
The Spring Context Config class and your CDS Hooks classes must all be packaged up in a normal Java JAR file. No third party libraries should be packaged with your code.
If you are using Apache Maven as your build system, this just means you should use a normal project with a packaging of jar
.
Once you have created a JAR with your CDS Hooks in it, this JAR should be placed in the customerlib/
directory of the Smile CDR installation, and Smile CDR should be restarted. You will then want to create a new module configuration as follows:
IBaseResource
objects
according to this FHIR Version.CDS Hooks methods have access to the underlying authenticated user session if needed. See Security Attributes for more information.
See Exceptions for details concerning exceptions and error management within CDS Hooks classes.
Per Version 1.1 of the CDS Hooks Specification, a list of all registered services is available at a path like https://example.com:8888/cds-services
. As a convenience, swagger REST documentation is provided at the root of the endpoint: https://example.com:8888/
.
A sample CDS Hooks project is available at the following links:
To create CDS Services from PlanDefinitions the dependencies for a FHIR Storage Module, FHIR Endpoint and CQL module must be set. This will create a listener on the storage module so that any changes to PlanDefinition resources will update the CDS Service cache.
Any PlanDefinition resource with an action that has a trigger of type named-event will have a CDS Service created using the PlanDefinition.id as the service id and the name of the trigger as the hook that the service is created for per the CDS on FHIR Specification.
CDS Services created this way will show up as registered services and can be called just as other services are called. The CDS Service request will be converted into parameters for the $apply operation, the results of which are then converted into a CDS Response per the CDS on FHIR Specification.
These CDS Services will take advantage of the Auto Prefetch feature. Prefetch data is included as a Bundle in the data
parameter of the $apply call.
The $apply operation is running against the FHIR Storage Module, so it will also have access to any data stored there. Any CQL evaluation during the $apply operation that results in a retrieve will always pull from the Bundle and the FHIR Storage Module. This is done regardless of what data is passed into the prefetch of the service request.