Using CQL with FHIR, published by HL7 International / Clinical Decision Support. This guide is not an authorized publication; it is the continuous build for version 2.0.0-ballot built by the FHIR (HL7® FHIR® Standard) CI Build. This version is based on the current content of https://github.com/HL7/cql-ig/ and changes regularly. See the Directory of published versions
This topic provides general guidance and best-practices for authors building FHIR-based Knowledge Artifacts that make use of Clinical Quality Language (CQL). The topics provide informative guidance to facilitate authoring CQL directly with the FHIR data model, including support for primitives, choices, slices, and extensions, as well as guidance for dealing with missing information, negation, the use of terminologies in FHIR, and some discussion of profile-informed authoring.
As an exchange specification, FHIR has a rich syntax for expressing the values of elements defined in FHIR
resources. In particular, FHIR data types for representing basic values such as integers, strings, and dates and
times allow for extensions. This means that a FHIR
string
is not just a string value, but has elements (specifically, id
, extension
, and
value
, where the value
element contains the actual string value). This means that to access
the actual value of a FHIR string
element in CQL, authors would need to reference the value
element:
define "Patient is Female":
Patient.gender.value = 'female'
To avoid this, a FHIRHelpers library defines implicit conversions for all the FHIR types, allowing authors to treat FHIR elements as integers, strings, etc. directly:
define "Patient is Female":
Patient.gender = 'female'
Note that these conversions are performed automatically by the CQL-to-ELM translator when they are used by CQL, resulting in a conversion error if the FHIRHelpers library is not included using an include declaration:
include FHIRHelpers version '4.0.1'
The version of the library is not required by CQL, but for the FHIRHelpers reference, because it is so closely tied to the FHIR ModelInfo, best-practice is to include the version of FHIRHelpers.
FHIR includes the notion of choice types, or elements that can be represented as any of a number of types. For example,
the Patient.deceased
element can be specified as a boolean
or as a dateTime
. CQL also supports choice types, so these elements are represented directly as Choice Types within the Model Info.
When authoring CQL using FHIR, logic must take into account the possible choice types of the elements involved. For example, the Observation.effective
element may be represented as a dateTime
or a Period
(among other types):
define "Blood Pressure Observations Within 30 Days":
[Observation: "Blood Pressure"] O
where O.status = 'final'
and (
(O.effective as dateTime).value 30 days or less before Today()
or (O.effective as Period) starts 30 days or less before Today()
)
Rather than requiring different representations to be considered in the logic each time they are encountered, a fluent function can be defined that accepts a choice type argument:
define fluent function toInterval(choice Choice<FHIR.dateTime, FHIR.Period>):
case
when choice is FHIR.dateTime then
Interval[FHIRHelpers.ToDateTime(choice as FHIR.dateTime), FHIRHelpers.ToDateTime(choice as FHIR.dateTime)]
when choice is FHIR.Period then
FHIRHelpers.ToInterval(choice as FHIR.Period)
else null as Interval<DateTime>
end
This can then be written as:
define "Blood Pressure Observations Within 30 Days (refined)":
[Observation: "Blood Pressure"] O
where O.status = 'final'
and O.effective.toInterval() starts 30 days or less before Today()
Another common pattern in FHIR is the use of slices to constrain list-valued elements into sub-lists and elements. Consider the Blood Pressure that defines "Systolic" and "Diastolic" elements:
define "Blood Pressure With Slices":
[Observation: "Blood Pressure"] BP
where (singleton from (BP.component C where C.code ~ "Systolic blood pressure")).value < 140 'mm[Hg]'
and (singleton from (BP.component C where C.code ~ "Diastolic blood pressure")).value < 90 'mm[Hg]'
To reuse slices, CQL fluent functions can be defined for each slice:
define fluent function systolic(observation Observation):
singleton from (observation.component C where C.code ~ "Systolic blood pressure")
define fluent function diastolic(observation Observation):
singleton from (observation.component C where C.code ~ "Diastolic blood pressure")
These fluent functions can then be used to access the slices:
define "Blood Pressure With Slices (refined)":
[Observation: "Blood Pressure"] BP
where BP.systolic().value < 140 'mm[Hg]'
and BP.diastolic().value < 90 'mm[Hg]'
FHIR also supports defining extensions to allow additional information beyond what is available in the base FHIR resources to be specified. Profiles then make use of these extensions to establish how this additional information is exchanged in specific use cases. As a simple example, consider the birthsex extension in US Core:
define "Patient Birth Sex Is Male":
Patient P
let birthsex: singleton from (
P.extension E where E.url.value = 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-birthsex'
).value as FHIR.code
where birthsex = 'M'
In this example, a let clause is used to build a birthsex
element in the query that finds the birthsex extension value. As with slicing, fluent functions can be used to provide access to extensions:
define fluent function birthsex(patient Patient):
(singleton from (
patient.extension E where E.url = 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-birthsex'
)).value as FHIR.code
This function can then be used to easily access the birthsex extension:
define "Patient Birth Sex Is Male (refined)":
Patient P
where P.birthsex() = 'M'
As a more complex example, consider the race extension. This is a complex extension that define elements for ombCategory
, detailed
, and text
:
define "Patient With Race Category":
Patient P
let
race: singleton from (
P.extension E where E.url.value = 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-race'
),
ombCategory: race.extension E where E.url.value = 'ombCategory',
detailed: race.extension E where E.url.value = 'detailed'
where (ombCategory O return O.value as FHIR.Coding) contains "American Indian or Alaska Native"
and (detailed O return O.value as FHIR.Coding) contains "Alaska Native"
Again, these can be accessed directly using a let clause, or a fluent function can be defined to allow access:
define fluent function race(patient Patient):
(singleton from (patient.extension E where E.url = 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-race')) race
let
ombCategory: race.extension E where E.url = 'ombCategory' return E.value as Coding,
detailed: race.extension E where E.url = 'detailed' return E.value as Coding,
text: singleton from (race.extension E where E.url = 'text' return E.value as string)
return { ombCategory: ombCategory, detailed: detailed, text: text }
define "Patient With Race Category (refined)":
Patient P
where P.race().ombCategory contains "American Indian or Alaska Native"
and P.race().detailed contains "Alaska Native"
For common use cases, the CQF Common implementation guide provides a FHIRCommon library that defines many of these types of functions and declarations that are commonly used with CQL and FHIR. By including a reference to this implementation guide, content IGs can build CQL that refers to these common functions by including the FHIRCommon library:
include fhir.cqf.common.FHIRCommon
Note that rather than using FHIR directly, CQL also supports models derived from implementation guides specifically. For example:
using USCore version '6.1.0'
With this approach, the profiles defined in the USCore implementation guide are used to provide the model. This approach is referred to as "profile-informed authoring" and automates the patterns described above, so that rather than building fluent functions, the model contains elements for slices and extensions defined in the profiles of the implementation guide. For example:
define "Blood Pressure With Slices":
["BloodPressureProfile"] BP
where BP.systolic.value < 140 'mm[Hg]'
and BP.diastolic.value < 90 'mm[Hg]'
define "Patient With Birthsex":
Patient P
where P.birthsex = 'M'
define "Patient With Race":
Patient P
where P.race.ombCategory contains "American Indian or Alaska Native"
and P.race.detailed contains "Alaska Native"
For detailed information on how model information is produced for an implementation guide, see the Profile-informed ModelInfo section.
FHIR supports various types of terminology-valued elements, including:
These types map to the following CQL primitive types, respectively:
In addition to the type of element, FHIR provides the ability to bind these elements to specific codes, in the form of a direct-reference code (fixed constraint to a specific code in a CodeSystem), or a binding to a ValueSet. These bindings can be different binding strengths
Within CQL, references to terminology code systems, value sets, codes, and concepts are directly supported, and all such usages are declared within CQL libraries, as described in the Terminology section of the CQL Author's Guide.
When referencing terminology-valued elements within CQL, the following comparison operations are supported:
As a general rule, the equivalent (~
) operator should be used whenever the terminology being compared is a direct-reference code, and the in
operator should be used whenever the terminology being compared is a value set. The equal (=
) operator should only be used with code-valued elements that have a required binding.
In FHIR, code-valued elements are most often used with required bindings, meaning that the only values that can appear are established by the specification. Because of this, basic string comparison can be used, for example:
where Encounter.status = 'finished'
NOTE: The comparison here is to the code value, not the display
NOTE: Note also that there are edge-cases where the string-valued elements may contain terminology values. For more detail on this case, refer to the Using CQL IG
Most terminology-valued elements in FHIR are CodeableConcepts. If the terminology being compared is a value set (e.g. valueset "Inpatient Encounter"
), use the in
operator:
where Encounter.type in "Inpatient Encounter"
Note that the in
operator works whether the element is single cardinality or multi-cardinality.
If the terminology being compared is a direct-reference code (e.g. code "Blood Pressure"
), use the ~
operator:
where Observation.code ~ "Blood Pressure"
Note that this comparison only works if the element is single-cardinality. For multi-cardinality elements with direct-reference code comparison (e.g. code "Right Breast"
), each CodeableConcept must be tested using the ~ operator, so an exists is used:
where exists (Condition.bodySite S where S ~ "Right Breast")
Some terminology-valued elements in FHIR use the Coding type specifically. The same comparison patterns are used for elements of this type. For value sets (e.g. valueset "Inpatient Class"
), use in
:
where Encounter.class in "Inpatient Class"
And for direct-reference codes (e.g. code "Inpatient"
), use ~
:
where Encounter.class ~ "Inpatient"
For time-valued quantities, in addition to the definite duration UCUM units, CQL defines calendar duration keywords to support calendar-based durations and arithmetic. For example, UCUM defines an annum ('a') as 365.25 days, whereas the year ('year') duration in CQL is specifically a calendar year. This difference is important, especially when performing calendar arithmetic.
For example if we take a datetime and subtract a calendar year
@2019-01-01T05:00:00 - 1 year
This would resolve to 2018-01-01T05:00:00
However, if we take the same datetime and subtract a UCUM annum
@2019-01-01T05:00:00 - 1 'a'
This would resolve to 2017-12-31T23:00:00
See the definition of the Quantity type in the CQL Author's Guide, as well as the Date/Time Arithmetic discussion for more information.
Because clinical information is often incomplete, CQL provides constructs and support for representing and dealing with unknown or missing information. In FHIR, when the value of an element is not present, accessing that element will result in a null
:
MedicationRequest.doNotPerform
Given an instance of a MedicationRequest resource that does not have a doNotPerform
element specified, the above expression will return null
. In general, null
results will propagate through operations. For example:
MedicationRequest.doNotPerform = false
If the MedicationRequest instance does not have a doNotPerform
element, this expression will return null
. When a null
result is encountered in the evaluation of a criteria (such as a where
clause), it will be interpreted as false
. For this reason, best-practice when comparing boolean-valued elements such as doNotPerform
is to use the is true | false
predicate test:
MedicationRequest MR
where MR.doNotPerform is not true
This pattern ensures that whether the instance does not have a doNotPerform element, or the doNotPerform element is false, the result of the expression is true, correctly accounting for the potential missing information.
Another common case encountered in FHIR is the use of an unknown
code in terminology-valued elements:
MedicationRequest.status = 'unknown'
This is a special-case of characterizing missing information within FHIR resources. To treat this status
value as a null, the following pattern can be used:
if MedicationRequest.status is null or MedicationRequest.status ~ 'unknown'
For more information about dealing with Missing Information in CQL in general, see the Missing Information topic in the CQL Author's Guide.
FHIR offers several possibilities for describing what activity (i.e. request or event) is being performed (e.g. the code
element of a Procedure, or the medication
element of a MedicationRequest):
codeOptions
extension)These approaches allow for the extent of an activity to be defined:
The first two approaches make use of terminology to define the extent of an activity, and is the most common approach. The code in a terminology may identify a single, precise concept, or it may identify a class of concepts, such as a type of procedure, or a class of medications.
For example, the following MedicationAdministration indicates a specific drug:
{
"resourceType" : "MedicationAdministration",
...,
"medicationCodeableConcept" : {
"coding" : [{
"system" : "http://www.nlm.nih.gov/research/umls/rxnorm",
"code" : "1116635",
"display" : "ticagrelor 90 MG Oral Tablet"
}]
},
...
}
As opposed to specifying only a concept code:
{
"resourceType" : "MedicationAdministration",
...,
"medicationCodeableConcept" : {
"coding" : [{
"system" : "http://www.nlm.nih.gov/research/umls/rxnorm",
"code" : "11289",
"display" : "warfarin"
}]
},
...
}
Retrieving resources with codes specified using these approaches can be accomplished with a simple Retrieve:
define "Antithrombotic Therapy Administered":
[MedicationAdministration: "Antithrombotic Therapy"] AntithromboticTherapy
where AntithromboticTherapy.status = 'completed'
and AntithromboticTherapy.category ~ "Inpatient Setting"
This example retrieves MedicationAdministration
resources that have a code in the Antithrombotic Therapy
value set, a status of completed
, and a category of Inpatient Setting
.
The third approach (specifying the items with a value set) is enabled through the use of the codeOptions extension. Rather than specifying a code, this extension is used to indicate that the activity may be any one of the codes in the value set:
{
"resourceType" : "MedicationAdministration",
...,
"medicationCodeableConcept" : {
"extension" : [{
"url" : "http://hl7.org/fhir/StructureDefinition/codeOptions",
"valueCanonical" : "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1110.62"
}],
"text" : "Value Set: Antithrombotic Therapy for Ischemic Stroke"
},
...
}
When this pattern is used in FHIR resources, the CQL needs to take this into account by looking for the codeOptions
extension:
define "Antithrombotic Therapy Class Administered":
[MedicationAdministration] Administered
where Administered.medication.codeOptions() = "Antithrombotic Therapy".id
and Administered.status = 'completed'
and Administered.category ~ "Inpatient Setting"
This example retrieves MedicationAdministration
resources that use the codeOptions
extension to specify a candidate medication in the Antithrombotic Therapy
value set, a status of completed
, and a category of Inpatient Setting
.
NOTE: See the FHIRCommon library for the definition of the codeOptions()
fluent function.
To ensure both approaches are accounted for, these two expressions would then be used together:
define "Antithrombotics Administered":
"Antithrombotic Therapy Administered"
union "Antithrombotic Therapy Class Administered"
NOTE: Profile-informed authoring exposes elements that have a
codeOptions
extension using a Choice ofCodeableConcept
andValueSet
, which is then translated as a union, accounting for both cases as part of profile-informed authoring.
The other three approaches make use of structures such as PlanDefinition, RequestOrchestration, and the relationships between events and requests to establish the extent of an activity. See the Clinical Guidelines implementation guide for more information on using these approaches to characterize and manage the extent of activities.
The HL7 Cross-Paradigm Specification: Representing Negatives provides guidance and best practices for the representation of pertinent negatives and other negative semantics in clinical information. The following sections describe how these best practices may be represented in FHIR resources and profiles, as well as guidance for accessing negated information in CQL.
For an example of a set of profiles following these best practices to support the representation of negation in FHIR, see the Negation profiles in QI-Core.
In summary, negation statements typically cover three different use cases:
Given the representation of negative information in FHIR, two commonly used patterns for negation in clinical logic are:
For the purposes of clinical reasoning, when looking for documentation that a particular event did not occur, it must be documented with a reason in order to meet the intent. If a reason is not part of the intent, then the absence of evidence pattern SHOULD be used, rather than documentation of an event not occurring.
To address the reason an action did not occur (negation rationale), clinical logic must define the event it expects to occur using appropriate terminology to identify the kind of event (using a value set or direct-reference code), and then use additional criteria to indicate that the event did not occur, as well as identifying a reason.
The following examples differentiate methods to indicate (a) presence of evidence of an action, (b) absence of evidence of an action, and (c) negation rationale for not performing an action. In each case, the "action" is an administration of medication included within a value set for "Antithrombotic Therapy".
Evidence that "Antithrombotic Therapy" (defined by a medication-specific value set) was administered:
define "Antithrombotic Administered":
[MedicationAdministration: "Antithrombotic Therapy"] AntithromboticTherapy
where AntithromboticTherapy.status = 'completed'
and AntithromboticTherapy.category ~ "Inpatient Setting"
No evidence that "Antithrombotic Therapy" medication was administered:
define "No Antithrombotic Therapy":
not exists (
[MedicationAdministration: "Antithrombotic Therapy"] AntithromboticTherapy
where AntithromboticTherapy.status = 'completed'
and AntithromboticTherapy.category ~ "Inpatient Setting"
)
Evidence that "Antithrombotic Therapy" medication administration did not occur for an acceptable medical reason as defined by a value set referenced by the clinical logic (i.e., negation rationale):
define "Antithrombotic Not Administered":
[MedicationAdministration: "Antithrombotic Therapy"] NotAdministered
where NotAdministered.status = 'not-done'
and NotAdministered.statusReason in "Medical Reason"
In this example for negation rationale, the logic looks for a member of the value set "Medical Reason" as the rationale for not administering any of the anticoagulant and antiplatelet medications specified in the "Antithrombotic Therapy" value set.
As discussed in the Activity Extent section, to represent Antithrombotic Therapy Not Administered, implementing systems reference the canonical of the "Antithrombotic Therapy" value set using the (codeOptions) extension to indicate providers did not administer any of the medications in the "Antithrombotic Therapy" value set. By referencing the value set URI to negate the entire value set rather than a specific member code from the value set, clinicians are not forced to arbitrarily select a specific medication from the "Antithrombotic Therapy" value set that they did not administer in order to negate.
When this pattern is used in FHIR resources, the CQL needs to take this into account by looking for the codeOptions
extension:
define "Antithrombotic Class Not Administered":
[MedicationAdministration] NotAdministered
where NotAdministered.medication.codeOptions() = "Antithrombotic Therapy".id
and NotAdministered.status = 'not-done'
and NotAdministered.statusReason in "Medical Reason"
To ensure both cases are accounted for, these two expressions would then be used together:
define "Antithrombotics Not Administered":
"Antithrombotic Not Administered"
union "Antithrombotic Class Not Administered"
This approach ensures that the logic will retrieve negated activities whether they are recorded as singular activities (i.e. with a code from the value set) or as indications that none of the activities were performed (i.e. with a reference to a value set).
NOTE: Profile-informed authoring exposes elements that have a
codeOptions
extension using a Choice of CodeableConcept and ValueSet, which is then translated as a union, accounting for both cases as part of profile-informed authoring.
Evidence that "Antithrombotic Therapy" medication was prohibited for an acceptable medical reason makes use of the appropriate Request
resource:
define "Antithrombotic Therapy Prohibited":
[MedicationRequest: "Antithrombotic Therapy"] Prohibited
where Prohibited.status = 'active'
and Prohibited.doNotPerform is true
and Prohibited.statusReason in "Medical Reason"
This example retrieves MedicationRequest
resources with a code in the Antithrombotic Therapy
value set that have a status of active
, a doNotPerform of true
, and a statusReason in the Medical Reason
value set.
As with negation of events, the extent of the activity can be accounted for by searching for instances that make use of the codeOptions
extension.
Evidence that a proposal to administer "Antithrombotic Therapy" was rejected for an acceptable medical reason makes use of the Task
resource:
define "Antithrombotic Therapy Requested":
[MedicationRequest: "Antithrombotic Therapy"] MR
where MR.status = 'active'
and MR.doNotPerform is not true
define "Antithrombotic Therapy Rejected":
"Antithrombotic Therapy Requested" MR
with [Task: Fulfill] T
such that T.focus.references(MR)
and T.status = 'rejected'
and T.statusReason in "Medical Reason"
This example retrieves "Antithrombotic Therapy Requested" resources that have a fulfillment Task focused on the request, a status of rejected
, and a statusReason in the Medical Reason
value set.
As with negation of events, the extent of the activity can be accounted for by searching for request instances that make use of the codeOptions
extension.