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 1.1.0-cibuild 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:
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
:
Observation.interpretation
Given an instance of an Observation resource that does not have an interpretation element, 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:
MedicationRequeest.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.
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 two different extents:
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.
To represent Antithrombotic Therapy Not Administered, implementing systems reference the canonical of the “Antithrombotic Therapy” value set using the (cqf-notDoneValueSet) 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 cqf-notDoneValueSet
extension:
define "Antithrombotic Class Not Administered":
[MedicationAdministration] NotAdministered
where NotAdministered.medication.notDoneValueSet() = "Antithrombotic Therapy".id
and NotAdministered.status = 'not-done'
and NotAdministered.statusReason in "Medical Reason"
NOTE: See the Example.cql for the definition of the notDoneValueSet()
fluent function.
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
notDoneValueSet
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.