-
Notifications
You must be signed in to change notification settings - Fork 27
CQL Related Ballots 2026 May
This topic provides updates on CQL artifacts processing with the publisher, as well as ballot change reviews for the CQL-related ballots in the HL7 2026 May Ballot Cycle:
The FHIR IG Publisher now supports Measure, ActivityDefinition, PlanDefinition, and Questionnaire refresh processing, including creation of effective data requirements.
- Ensure you are using the latest version of the publish by running the _updatePublisher script
- Set the path-binary implementation guide parameter to the
input/cqldirectory in your IG - Set the
contentelement of the FHIR Library resource to reference the CQL file (e.g."id": "ig-loader-MyLibrary.cql")
For example, in the US CQL IG, set the parameter in the source for the implementation guide:
<parameter>
<code value="path-binary"/>
<value value="input/cql"/>
</parameter>Next, for each CQL library, ensure there is a FHIR Library resource, and that it has a content element that has only an id referencing the CQL source file:
{
"resourceType": "Library",
"id": "CumulativeMedicationDuration",
"url": "http://hl7.org/fhir/us/cql/Library/CumulativeMedicationDuration",
"name": "CumulativeMedicationDuration",
"title": "Cumulative Medication Duration",
"status": "active",
"experimental": false,
"type": {
"coding": [ {
"system": "http://terminology.hl7.org/CodeSystem/library-type",
"code": "logic-library"
} ]
},
"description": "This library provides cumulative medication duration calculation logic for use with FHIR medication prescription, administration, and dispensing resources. The logic here follows the guidance provided as part of the 5.6 version of Quality Data Model.",
"usage": "Note that the logic here assumes single-instruction dosing information. Split-dosing, tapering, and other more complex dosing instructions are not handled.",
"content": [ {
"id": "ig-loader-CumulativeMedicationDuration.cql"
} ]
}The content element will be replaced with the base-64 encoded CQL, and the base-64 encoded ELM XML and/or JSON, depending on the settings in the cql-options.json file in the CQL directory.
Any Measure resources in the IG that have library elements referencing CQL libraries processed in this way will have effective data requirements calculated.
In addition, the publisher supports model info processing as well in the same way. For example:
{
"resourceType": "Library",
"id": "USCore-ModelInfo",
"url": "http://hl7.org/fhir/us/cql/Library/USCore-ModelInfo",
"version": "7.0.0",
"name": "USCore",
"title": "US Core Model Information",
"status": "active",
"experimental": false,
"type": {
"coding": [ {
"system": "http://terminology.hl7.org/CodeSystem/library-type",
"code": "model-definition"
} ]
},
"description": "CQL model information for the US Core version 7.0.0 implementation guide.",
"content": [ {
"id": "ig-loader-uscore-modelinfo-7.0.0.xml"
} ]
}For more details, see the Binary Adjunct Files topic in FHIR IG Guidance.
US CQL Common Informative 2 Ballot
Comparing Date and DateTime values results in implicit conversion of the Date to a DateTime with null components, and this can lead to unexpected partial comparison case. Authors should be explicit about precision when comparing Date and DateTime values, as comparing with mismatched precision can yield null results (which are often intepreted as false). In most cases, using day of precision is appropriate when comparing a Date and a DateTime.
In addition, authors should consider using day of when comparing between events within a specific period (such as within the measurement period) and date values are clinically appropriate, and should consider using minute of or second of precision when comparing between events where time values are clinically appropriate.
For more information on Date, DateTime, and Time comparison, see Comparing Dates and Times in the CQL Author's Guide.
Note that a new feature of CQL R2, default comparison precision may be useful in supporting this best practice.
{: #time-valued-quantities}
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 yearThis results in 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 results in run-time error because a UCUM annum is defined as 365.25 days and this value should never be used for calendar arithmetic. If this is truly the intended calculation, authors must convert the annum to seconds:
@2019-01-01T05:00:00 - convert 1 'a' to 's'This results in 2017-12-31T23:00:00.
Note carefully that when implicitly converting FHIR Duration values to CQL, a UCUM annum is converted to a calendar year, and a UCUM mo is converted to a calendar month. This is because when years and months appear in FHIR durations, it almost universally the intent that they represent calendar durations, rather than definite-time durations:
Patient.birthDate + (Condition.onset as Age)If the duration in value in FHIR truly represents a definite-time duration, conversion to seconds is required in order to perform the date/time calculation.
See the definition of the Quantity type in the CQL Author's Guide, as well as the Date/Time Arithmetic discussion for more information.
These best-practices are proposed for inclusion in the base specification (https://jira.hl7.org/browse/FHIR-53145) as well as the Using CQL With FHIR Specification (https://jira.hl7.org/browse/FHIR-53575).
This topic documents proposed updates to the Patient Patterns page in the US CQL IG.
Although patient name will typically already be known and established by application context, patterns for accessing and displaying patient name are provided for applications that may still need to establish, discover, or display this information.
Because FHIR allows for multiple names for different uses, as well as different usage periods, the name of a patient is not always straightforward to determine. For completeness, the US Core Elements library defines several functions for accessing the name of a Patient. In the most common case, the .name() function can be used:
Patient.name()The name function is just the first official, usual, or non-official non-usual name that is defined for the Patient. The avaiable name functions defined in the US Core Elements library are:
description: Returns the first official, usual, or other name
This function returns the first official name if present, otherwise the first usual name, otherwise the first non-official, non-usual name. In each case, the function returns the most recent name that either has no period specified, or has a period overlapping today.
define fluent function name(practitioner Practitioner):
Coalesce(practitioner.officialName().first(), practitioner.usualName().first(), practitioner.firstNonOfficialNonUsualName())description: Returns the first usual name
This function returns the first usual name for a patient. The most recent name entry is return that either has no period specified, or has a period overlapping today.
define fluent function usualName(patient Patient):
patient.name name
where name.use ~ 'usual'
and name.period is not null implies name.period includes Today()
sort by start of period descdescription: Returns the first official name
This function returns the first official name for a patient. The most recent name entry is return that either has no period specified, or has a period overlapping today.
define fluent function officialName(patient Patient):
patient.name name
where name.use ~ 'official'
and name.period is not null implies name.period includes Today()
sort by start of period descdescription: Returns the first non-official, non-usual name
This function returns the first non-official, non-usual name for a patient. The most recent name entry is return that either has no period specified, or has a period overlapping today.
define fluent function firstNonOfficialNonUsualName(patient Patient):
First(
patient.name name
where not(name.use ~ 'official') and not(name.use ~ 'usual')
and name.period is not null implies name.period includes Today()
sort by start of period desc
)In addition, the library defines functions for common use cases for the HumanName type:
define fluent function firstName(name HumanName):
name.given.first()
define fluent function middleNames(name HumanName):
Combine(Skip(name.given, 1), ' ')
define fluent function lastName(name HumanName):
name.family
define fluent function firstMiddleLast(name HumanName):
Combine(name.given, ' ') + ' ' + name.family
define fluent function lastFirstMiddle(name HumanName):
name.family + ', ' + Combine(name.given, ' ')Typically, patient birthDate is just represented using the birthDate element:
Patient.birthDateHowever, FHIR also defines a birthTime extension, and applications that want to consider this level of specificity when determining patient age can make use of this extension.
Since the birthTime extension is defined in the extensions pack, it can be used anywhere, and the FHIRCommon CQL library defines a function for accessing it, as well as a function for accessing the birthDate as a dateTime, considering this birthTime extension if present:
Patient.birthTime()
Patient.birthDateTime()description: Returns the birthTime of the patient, if present
This function returns the value of the birthTime extension, if present, null otherwise.
define fluent function birthTime(patient Patient):
patient.birthDate.ext('http://hl7.org/fhir/StructureDefinition/patient-birthTime').value as dateTimedescription: Returns the birth date (and time if present) (as a DateTime)
This function returns the birthTime of the patient, if present, else the birthDate of the patient as a DateTime
define fluent function birthDateTime(patient Patient):
Coalesce(patient.birthTime().value, patient.birthDate.toDateTime())Patient information includes the birth date, and CQL provides a built-in function to calculate the age of a patient, either current age (i.e. as of now), or as of a particular date. In quality improvement artifacts, age is typically calculated as of a particular date. In the context of a Questionnaire, this is typically just today's date, and can be accessed using the ageInYears() function:
define "Patient Age Between 50 and 75":
Patient.ageInYears() between 50 and 75NOTE: The AgeAt functions in CQL use the data model (US Core in this case) to understand how to access the patient's birth date information.
NOTE: CQL supports age calculation functions using both
DateandDateTimevalues. In both cases the function is shorthand for a date/datetime duration calculation. If theDateTimeoverloads are used, note that the timezone offset is considered and there may be edge cases that result in unexpected results, depending on how large the timezone offset is from the execution timestamp. To avoid these edge cases, best practice is to use thedate fromextractor as shown in the above pattern to ensure theDatecalculation is used.
The US Core Elements library defines the following patient age calculation functions:
description: Returns the age in days of the patient as of the given date, using birth time if known
This function returns the number of whole days between the patient birth date and the asOf date. If the patient has a birthTime, the calculation is performed using the birthDateTime, and the time component of the asOf parameter is considered. Note that when the birth time is known, timezone offset normalization will be used.
define fluent function ageInDaysAt(patient Patient, asOf DateTime):
if patient.birthTime() is not null then
CalculateAgeInDaysAt(Patient.birthDateTime(), asOf)
else
CalculateAgeInDaysAt(Patient.birthDate, date from asOf)description: Returns the current age in days of the patient, using birth time if known
This function returns the number of whole days between the patient birth date and the current date. If the patient has a birthTime, the calculation is performed using the birthDateTime and Now(), otherwise the calculation is performed using Today(). Note that when the birth time is known, timezone offset normalization will be used.
define fluent function ageInDays(patient Patient):
if patient.birthTime() is not null then
CalculateAgeInDaysAt(Patient.birthDateTime(), Now())
else
CalculateAgeInDaysAt(Patient.birthDate, Today())description: Returns the age in months of the patient as of the given date, using birth time if known
This function returns the number of whole calendar months between the patient birth date and the asOf date. If the patient has a birthTime, the calculation is performed using the birthDateTime, and the time component of the asOf parameter is considered. Note that when the birth time is known, timezone offset normalization will be used.
define fluent function ageInMonthsAt(patient Patient, asOf DateTime):
if patient.birthTime() is not null then
CalculateAgeInMonthsAt(Patient.birthDateTime(), asOf)
else
CalculateAgeInMonthsAt(Patient.birthDate, date from asOf)description: Returns the current age in months of the patient, using birth time if known
This function returns the number of whole calendar months between the patient birth date and the current date. If the patient has a birthTime, the calculation is performed using the birthDateTime and Now(), otherwise the calculation is performed using Today(). Note that when the birth time is known, timezone offset normalization will be used.
define fluent function ageInMonths(patient Patient):
if patient.birthTime() is not null then
CalculateAgeInMonthsAt(Patient.birthDateTime(), Now())
else
CalculateAgeInMonthsAt(Patient.birthDate, Today())description: Returns the age in years of the patient, as of the given date
This function returns the number of whole calendar years between the patient birth date and the given date. Regardless of whether the patient has a birthTime, the calculation is performed using only the birth date. If the given date has a time component, it is ignored, on the grounds that birth time is almost universally not considered when determining age in years.
define fluent function ageInYearsAt(patient Patient, asOf DateTime):
CalculateAgeInYearsAt(Patient.birthDate, date from asOf)description: Returns the current age in years of the patient
This function returns the number of whole calendar years between the patient birth date and the current date. Regardless of whether the patient has a birthTime, the calculation is performed using only the birth date and Today(), on the grounds that birth time is almost universally not considered when determining age in years.
define fluent function ageInYears(patient Patient):
CalculateAgeInYearsAt(Patient.birthDate, Today())These changes are proposed for inclusion in the US CQL IG: (https://jira.hl7.org/browse/FHIR-53458)
This topic documents proposed updates to the AllergyIntolerance Patterns topic in the US CQL IG.
Abatement for an AllergyIntolerance can be represented in multiple ways:
-
.abatement(): Returns the abatement of the given AllergyIntolerance, if present -
.resolutionAge(): Returns the resolutionAge of the given AllergyIntolerance, if present
Using these options, an abatement, as well as a prevalence interval, can be calculated:
-
.abatementInterval(): Returns an interval representing the normalized abatement of the given AllergyIntolerance resource. -
.prevalenceInterval(): Returns an interval representing the normalized prevalence period of the given AllergyIntolerance resource
To facilitate checking allergy intolerance status, the following functions are defined in the FHIRCommon library. The is... functions operate on a single AllergyIntolerance (and so are typically used within a where clause), while the other functions operate on a list of AllergyIntolerance resources, and so are typically used when dealing with an entire set of resources as a single expression:
The AllergyIntolerance resource also has a verificationStatus element to represent information such as whether the allergy has been confirmed. The element is not required, but if it is present, it is a modifier element, and has the potential to negate the information the resource is conveying (e.g. the refuted status). For most usage, when application intent is looking for positive evidence of an allergy, the verification statuses of refuted and entered-in-error should be excluded if verificationStatus is present:
define "Verified Allergies":
[AllergyIntolerance] VerifiedAllergyIntolerance
where VerifiedAllergyIntolerance.verificationStatus is not null implies
(VerifiedAllergyIntolerance.verificationStatus ~ "confirmed"
or VerifiedAllergyIntolerance.verificationStatus ~ "unconfirmed"
)To support reuse of this pattern, the FHIRCommon library defines the isVerified and verified functions, along with other functions for testing verification status. As with the clinical status functions there are two sets, one using an is prefix, that are used with single AllergyIntolerance instances, and one set without, used with sets of AllergyIntolerance resources.
.isVerified().verified().isConfirmed().confirmed().isUnconfirmed().unconfirmed().isRefuted().refuted()
These changes are proposed for inclusion in the US CQL IG: (https://jira.hl7.org/browse/FHIR-53460)
This page documents proposed changes to the Observation Patterns in the US CQL IG
The USCoreCommon library defines functions and terminology declarations to support determining status of an observation:
-
isResulted(): returns true if the status isfinal,amended, orcorrected -
isFinal(): Returns true if the status isfinal -
isAmended(): Returns true if the status isamended -
isCorrected(): Returns true if the status iscorrected
As well as for filtering lists of observations with a given status:
-
resulted(): returns Observations in the given list with a status isfinal,amended, orcorrected -
final(): Returns Observations in the given list with a status isfinal -
amended(): Returns Observations in the given list with a status isamended -
corrected(): Returns Observations in the given list with a status iscorrected
-
.hasCategory(Code): Returns true if the given observation has the given category -
.isSocialHistory(): Returns true if the given observation has a category of social history -
.isVitalSign(): Returns true if the given observation has a category of vital sign -
.isImaging(): Returns true if the given observation has a category of imaging -
.isLaboratory(): Returns true if the given observation has a category of laboratory -
.isProcedure(): Returns true if the given observation has a category of procedure -
.isSurvey(): Returns true if the given observation has a category of survey -
.isExam(): Returns true if the given observation has a category of exam -
.isActivity(): Returns true if the given observation has a category of activity
Note that the interpretation element of an observation may not be present, and may not be coded as expected. Care must be taken in the use of this element to ensure that data conforms with the expectations of the logic.
-
.positive(): Returns Observations in the given list that have an interpretation of positive -
.negative(): Returns Observations in the given list that have an interpretation of negative
-
.during(Encounter): Returns Observations in the given list that were issued during the given Encounter -
.within(Quantity): Returns Observations in the given list that were issued within the given time duration before now -
.consecutively(): Returns Observations consecutively by when they were issued -
.consecutivelyFrom(Observation): Returns Observations consecutively by when they were issued, on or after when the given Observation was issued
In addition, the USCoreElements library defines expressions for accessing the various USCore profiles, such as:
"All Laboratory Results""Resulted Laboratory Results""Pediatric BMI for Age"
In general, the expressions to retrieve observations for a particular profile include the .resulted() function to ensure only final, amended, or corrected observations are returned.
This example illustrates logic for identifying three consecutive negative "stick tests":
define StickTest:
[Observation: "Stick Test Codes"] O
where O.status in { 'final', 'amended', 'corrected' }
define "Three Consecutive Negative Stick Tests":
exists (
StickTest.during(Encounter).consecutively().take(3).negative().count() = 3
)In addition, this expression can be parameterized with current context (for example from a trigger context) with:
StickTest.consecutivelyFrom(%context).take(3).negative().count() = 3These changes are proposed for inclusion in the US CQL IG: (https://jira.hl7.org/browse/FHIR-53459)
This topic documents proposed updates to the Observation Patterns page in the US CQL IG.
US Core allows for the presence of pregnancy to be represented in multiple resources. The US Core profile Pregnancy Status allows for the representation of pregnancy as an Observation. However, pregnancy information may also be represented in a laboratory test result, an encounter diagnosis, or a problem list item.
To determine if an individual is pregnant, the following approach may be used to consider all three approaches:
valueset "Pregancy Condition Codes": 'TBD'
valueset "Pregnancy Test Codes": 'TBD'
codesystem "SNOMEDCT": 'http://snomed.info/sct'
code "Pregnant": '77386006' from "SNOMEDCT" display 'Pregnant (finding)'
code "Positive": 'positive' from "Interpretation Codes"
define "Positive Pregnancy Observation":
["Observation Pregnancy Status Profile"] PregnancyStatus
where PregnancyStatus.status = 'final'
and PregnancyStatus.value ~ "Pregnant"
and PregnancyStatus.effective.toInterval() overlaps "Measurement Period"
define "Positive Pregnancy Test Result":
["Laboratory Result Observation": "Pregnancy Test Codes"] PregnancyTest
where PregnancyTest.status = 'final'
and PregnancyTest.value ~ "Positive"
and PregnancyTest.effective.toInterval() during "Measurement Period"
define "Pregnancy Encounter Diagnosis":
[ConditionEncounterDiagnosis: "Pregnancy Condition Codes"] EncounterDiagnosis
with "Encounters" Encounter
such that EncounterDiagnosis.encounter.references(Encounter)
define "Pregnancy Condition":
[ConditionProblemsHealthConcerns: "Pregnancy Condition Codes"] Condition
where Condition.clinicalStatus ~ "active"
and Condition.verificationStatus ~ "confirmed"
and Condition.prevalenceInterval() starts during "Measurement Period"
define IsPregnant:
exists "Positive Pregnancy Observation"
or exists "Positive Pregnancy Test"
or exists "Pregnancy Condition"
or exists "Pregnancy Encounter Diagnosis"Note that the examples above assume the existence of a ValueSet "Pregnancy Condition Codes" containing codes pertaining to conditions while pregnant.
In addition, the "Pregnancy Encounter Diagnosis" criteria is assuming the existence of an "Encounters" expression that constrains the encounter diagnoses to measure intent as well as the measurement period.
If a use case requires the use of prevalence period (onset and/or abatement time for a condition), it will require the use of condition profiles because onset and/or abatement times are only available in the Condition resource.
The Pregnancy Intent profile sets minimum expectations for the Observation resource to record, search, and retrieve the "patient's intent to become pregnant" over the next year.
codesystem "LOINC": 'http://loinc.org'
code "Pregnancy intention in the next year - Reported": '86645-9'
code "Yes, I want to become pregnant": 'LA26438-4'
define "Positive Pregnancy Intent":
["Observation Pregnancy Intent Profile"] PregnancyIntent
where PregnancyIntent.effective.toInterval() during "Measurement Period"
and PregnancyIntent.value ~ "Yes, I want to become pregnant"These changes are proposed for inclusion in the US CQL IG: (https://jira.hl7.org/browse/FHIR-53462)
This page documents proposed additions to the Observation and Service Patterns pages in the US CQL IG.
Clinical test results (including imaging results) in US Core use the ObservationClinicalResult profile. By default, clinical test results in US Core are characterized by the code element.
define ObservationClinicalResult:
["Observation Clinical Result Profile"] O
where O.isResulted()Note that observations associated with imaging are expected to represent specific measurements obtained from imaging. See the Imaging Procedures discussion for more information.
NOTE: US Core 5 defined two different profiles for clinical results, one for test results, and one for imaging results. These profiles were combined in US Core 6 in the clinical result profile.
US Core defines the Procedure profile to represent an in-progress or complete procedure for a patient. By default, Procedure resources in US Core are characterized by the code element.
define "Application of Intermittent Pneumatic Compression Devices":
["Procedure": "Application of Intermittent Pneumatic Compression Devices (IPC)"] DeviceApplied
where DeviceApplied.status = 'completed'NOTE: Because the Procedure profile does not fix the value of the
statuselement, authors must consider all the possible values of the element to ensure the expression meets measure intent. In this case,completedstatus is used to indicate that only completed procedures should be returned.
In FHIR generally, because they involve significant and specialized information, imaging procedures (such as X-Rays, CT-Scans, MRIs, etc.) have resources designed specifically to represent imaging information. Overall, imaging procedures are represented using a combination of the following FHIR resources:
-
ServiceRequest - representing a proposal, plan, or order for an imaging procedure, using the
codeelement to distinguish the type of procedure being ordered -
Procedure - representing the actual performance of the imaging procedure, using the
codeelement to distinguish the type of procedure performed -
ImagingStudy - representing the resulting images and other DICOM content, using the
procedureCode,procedureReference, and/orbasedOnelements to associate the study to an order or procedure -
DiagnosticReport - representing the interpretation or clinical impression of the imaging procedure (detected or ruled out diagnoses), using the
codeelement to distinguish the type of test or report -
Observation - representing individual measurements and comments on the images, using the
codeelement to distinguish the type of measurement -
Claim - representing the claim for an imaging procedure, specifically the
procedure[x]element to distinguish the type of procedure performed -
ExplanationOfBenefit - representing an adjudicated claim response for an imaging procedure, specifically the
procedure[x]element to distinguish the type of procedure performed
Depending on the procedure, and the extent to which the results of that procedure are captured by clinical systems, there is variation in whether each of these resources is present for any given imaging procedure. In particular, Some diagnostic procedures might not have a Procedure record. The Procedure record is only necessary when there is a need to capture information about the physical intervention that was performed to capture the diagnostic information (e.g. anaesthetic, incision, scope size, etc.). As a result, authors must consider how to approach gathering information for specific procedures to ensure expressions match application intent.
For example, if application intent is that a CT Scan was performed, regardless of the results of that scan, authors should consider looking in all of the above possible locations for evidence that a CT Scan was performed:
- ServiceRequest with an intent of order and a status of completed
- Procedure with a status of completed
-
ImagingStudy with a status of available and related by
basedOnor one of theprocedureelements - DiagnosticReportNote with a status of final, amended, appended, or corrected
- ObservationClinicalResult with a status of final, amended, or corrected
-
Claim with a status of
activeand a use ofclaim -
ExplanationOfBenefit with a status of
activeand a use ofclaim
If application intent is only looking for the performance of the scan, not whether it was resulted, the first two may suffice:
define "CT Scan Order Completed":
["ServiceRequest": "CT Scan Codes"] SR
where SR.intent = 'order'
and SR.status = 'completed'
define "CT Scan Procedure Performed":
["Procedure": "CT Scan Codes"] P
where P.status = 'completed'
define "CT Scan Performed":
exists "CT Scan Order Completed"
or exists "CT Scan Procedure Performed"However, if application intent is that the scan was performed and resulted, diagnostic report provides the next level of checking:
define "CT Scan Diagnostic Report":
["DiagnosticReportNote": "CT Scan Notes"] DR
where DR.status in ('final', 'amended', 'corrected', 'appended')An additional check may be considered by looking for any imaging study results:
define "CT Scan Imaging Study":
["ImagingStudy"] IS
where exists (IS.procedureCode C where C in "CT Scan Procedure Codes")
or exists ("CT Scan Procedure Performed" P
where IS.procedureReference.references(P)
)As well as potentially looking for any measurements performed as part of the scan or study.
NOTE: This topic is a summary of discussion with the Orders & Observations Work Group in the following FHIR Zulip chat: https://chat.fhir.org/#narrow/channel/179256-Orders-and-Observation-WG/topic/How.20to.20represent.20CT-Scan.3F/with/541254708. Also note that part of that discussion suggests that ChargeItem may be useful as a source of evidence that something was done. This needs followup and additional investigation.
These changes are proposed for inclusion in the US CQL IG: (https://jira.hl7.org/browse/FHIR-53461)
define fluent function isProfessional(claim Claim):
claim.type ~ "professional"
define fluent function isInstitutional(claim Claim):
claim.type ~ "institutional"
define fluent function isActcive(claim Claim):
claim.status = 'active'
define fluent function isClaim(claim Claim):
claim.use = 'claim'
define fluent function isProfessional(eob ExplanationOfBenefit):
claim.type ~ "professional"
define fluent function isInstitutional(eob ExplanationOfBenefit):
claim.type ~ "institutional"
define fluent function isActcive(eob ExplanationOfBenefit):
claim.status = 'active'
define fluent function isClaim(eob ExplanationOfBenefit):
claim.use = 'claim'/*
@description: Returns the claim diagnosis elements for the given encounter
@comment: See the QICore 6 Authoring Patterns discussion on [Principal Diagnosis and Present on Admission](https://github.com/cqframework/CQL-Formatting-and-Usage-Wiki/wiki/Authoring-Patterns-QICore-v6.0.0#conditions-present-on-admission-and-principal-diagnoses) for more information
*/
define fluent function claimDiagnosis(encounter Encounter):
encounter E
let
claim: ([Claim] C where C.status = 'active' and C.use = 'claim' and exists (C.item I where I.encounter.references(E))),
claimItem: (claim.item I where I.encounter.references(E))
return claim.diagnosis D where D.sequence in claimItem.diagnosisSequence/*
@description: Returns the claim diagnosis element that is specified as the principal diagnosis for the encounter
@comment: See the QICore 6 Authoring Patterns discussion on [Principal Diagnosis and Present on Admission](https://github.com/cqframework/CQL-Formatting-and-Usage-Wiki/wiki/Authoring-Patterns-QICore-v6.0.0#conditions-present-on-admission-and-principal-diagnoses) for more information
*/
define fluent function principalDiagnosis(encounter Encounter):
singleton from (
(encounter.claimDiagnosis()) CD
where CD.type.includesCode("Principal Diagnosis")
)/*
@description: Returns the condition that is specified as the principal diagnosis for the encounter and has a code in the given valueSet.
@comment: See the QICore 6 Authoring Patterns discussion on [Principal Diagnosis and Present on Admission](https://github.com/cqframework/CQL-Formatting-and-Usage-Wiki/wiki/Authoring-Patterns-QICore-v6.0.0#conditions-present-on-admission-and-principal-diagnoses) for more information
*/
define fluent function hasPrincipalDiagnosisOf(encounter Encounter, valueSet ValueSet):
(encounter.principalDiagnosis()) PD
return PD.diagnosis in valueSet
or PD.diagnosis.getCondition().code in valueSetdefine "Encounter With Principal Diagnosis Of Asthma":
[Encounter] E
where E.hasPrincipalDiagnosisOf("Asthma")/*
@description: Returns true if the given diagnosis is present on admission, based on the given poaValueSet
@comment: See the QICore 6 Authoring Patterns discussion on [Principal Diagnosis and Present on Admission](https://github.com/cqframework/CQL-Formatting-and-Usage-Wiki/wiki/Authoring-Patterns-QICore-v6.0.0#conditions-present-on-admission-and-principal-diagnoses) for more information
*/
define fluent function isDiagnosisPresentOnAdmission(encounter Encounter, diagnosisValueSet ValueSet, poaValueSet ValueSet):
exists (
(encounter.claimDiagnosis()) CD
where CD.onAdmission in poaValueSet
and (
CD.diagnosis in diagnosisValueSet
or CD.diagnosis.getCondition().code in diagnosisValueSet
)
)define "Encounter With Asthma Present On Admission":
[Encounter] E
where E.isDiagnosisPresentOnAdmission("Asthma", "Present On Admission Indicators")/*
@description: Returns the claim procedure elements for the given encounter
*/
define fluent function principalProcedure(encounter Encounter):
encounter E
let
claim: [Claim] C where C.status = 'active' and C.use = 'claim' and exists (C.item I where I.encounter.references(E)),
claimItem: claim.item I where I.encounter.references(E),
princProcedure: singleton from (claim.procedure P where P.sequence in claimItem.procedureSequence and P.type.includesCode("Primary procedure"))
return princProcedure
define fluent function hasPrincipalProcedureOf(encounter Encounter, procedureValueSet ValueSet):
encounter E
let
PPx: E.principalProcedure(),
CPx: singleton from ([Procedure] P where PPx.procedure.references(P.id))
return PPx.procedure in procedureValueSet
or CPx.code in procedureValueSetdefine "Encounters With Principal Procedure of General Surgery":
[Encounter] E
where E.hasPrincipalProcedureOf("General Surgery")define "Claim Item":
[Claim] C
where C.isActive()
and C.isClaim()
and (C.isInstitutional() or C.isProfessional())
return C.itemdefine "EoB Item":
[ExplanationOfBenefit] E
where E.isActive()
and E.isClaim()
and (E.isInstitutional() or E.isProfessional())
return E.itemThese changes are proposed for inclusion in the US CQL IG: (https://jira.hl7.org/browse/FHIR-53461)
This page documents proposed additions to the Service Patterns page in the US CQL IG.
Applying this pattern to mammography, we may find evidence of mammography performed in:
- ObservationClinicalResult - Recording a mammography test result
- Claim - Recording a claim for a mammography provided
- ExplanationOfBenefit - Recording an adjudicated claim for a mammography
Note that we are excluding the Procedure, DiagnosticReportNote, and LaboratoryTestResult possibilities
For mammography, the following patterns can be used:
define "Mammography Observation":
["Observation Clinical Result Profile": "Mammography Result"] MammographyResult
where MammographyResult.isImaging()
and MammographyResult.isResulted()define "Mammography Claim":
"Claim Item" I
where I.serviced.toInterval() during "Measurement Period"
and I.productOrService in "Mammography"
define "Mammography EoB":
"EoB Item" I
where I.serviced.toInterval() during "Measurement Period"
and I.productOrService in "Mammography"These changes are proposed for inclusion in the US CQL IG: (https://jira.hl7.org/browse/FHIR-53461)
Quality Measure IG UV STU2 Ballot
Measure stratification is specified using the stratifier element of the Measure resource. Stratification can be defined in two different ways:
- Criteria-based: Criteria are specified as population expressions with the same population basis as the rest of the population criteria in the measure (e.g.
CMS146.AgesUpToNine) - Value-based: Criteria are specified as an expression that is evaluated for each member of the population, yielding a stratum value (e.g.
Patient.deceased)
Using criteria-based stratifiers is the most straightforward stratification approach as it is simply another set of population criteria for the measure. The expressions used to define criteria-based stratifiers must be consistent with the basis of the measure (i.e. they must return the same type as all the other population criteria expressions in the measure). For example, consider stratification into three age ranges: 0-20, 21-40, 41+.
NOTE: These examples are deliberately ignoring the measurement period, as well as the fact that age a moving point. Real measures will of course need to take these factors into account, but these examples are focusing solely on the structural representation of the various approaches to stratification.
Consider a simple, subject-based proportion measure:
Percentage of patients with a well-visit
define "Denominator":
Patient.active is true
define "Numerator":
exists ([Encounter: "Well-Visit Encounter Codes"])Criteria-based stratification by age range is then expressed as:
define "Stratifier P0Y--P21Y":
Patient.ageInYears() between 0 and 20
define "Stratifier P21Y--P41Y":
Patient.ageInYears() between 21 and 40
define "Stratifier P41Y--P9999Y":
Patient.ageInYears() >= 41Next, consider a simple, encounter-based proportion measure:
Percentage of well-visits with a blood pressure observation
define "Denominator":
[Encounter: "Well-Visit Encounter Codes"]
define "Numerator":
[Encounter: "Well-Visit Encounter Codes"] E
with [Observation: "Blood Pressure Observation Codes"] O such that O.issued during E.periodCriteria-based stratification by age range is then expressed as:
define "Stratifier P0Y--P21Y":
[Encounter: "Well-Visit Encounter Codes"] E
where Patient.ageInYears() between 0 and 20
define "Stratifier P21Y--P41Y":
[Encounter: "Well-Visit Encounter Codes"] E
where Patient.ageInYears() between 21 and 40
define "Stratifier P41Y--P9999Y":
[Encounter: "Well-Visit Encounter Codes"] E
where Patient.ageInYears() >= 41Value-based stratifiers are expressed as a set of stratifier components, each one identifying a path or an expression that returns a value from each member of the population being stratified. Using the same simple example measures as above:
Subject-based, valued-based, as a path, stratification by gender:
<stratifier>
<component>
<criteria>
<expression value="gender"/>
</criteria>
</component>
</stratifier>Note that with value-based stratifiers represented as a path, the value is an expression written from the perspective of the measure subject (Patient in this case) for subject-based measures, but the measure basis for non-subject-based measures.
Subject-based, value-based, as an expression
define "Age Range Stratifier":
case
when Patient.ageInYears() between 0 and 20 then 'P0Y--P21Y'
when Patient.ageInYears() between 21 and 40 then 'P21Y--P41Y'
when Patient.ageInYears() >= 41 then 'P41Y--P9999Y'
else null
endThe first thing to note here is that this approach required defining only a single stratifier element, "Age Range Stratifier", rather than the criteria-based approach, which required defining a stratifier element for each stratification group. Obviouisly, the more stratification groups, the more economical this approach becomes.
Non-subject-based, value-based, as an expression
define function "Age Range Stratifier"(encounter Encounter):
case
when Patient.ageInYearsAt(start of encounter.period) between 0 and 20 then 'P0Y--P21Y'
when Patient.ageInYearsAt(start of encounter.period) between 21 and 40 then 'P21Y--P41Y'
when Patient.ageInYearsAt(start of encounter.period) >= 41 then 'P41Y--P9999Y'
else null
endNote that with value-based stratifiers represented as an expression, the expression is written from the perspective of the measure subject, but also defines a parameter with the type of the basis. This is the same approach used to calculate measure observations, because the stratifier expression must be evaluated for each member of the population to determine the stratum value to which that member of the population belongs.
While criteria-based stratifiers are expressed using the stratifier.criteria element, value-based stratifiers are specified using any number of component elements. Each component of the stratifier specifies an expression that returns the value of that component of the stratifier for members of the population. The advantage of using component stratifiers is that the combinations are computed, rather than having to be constructed explicitly in the logic. For example, consider the case of stratifying by age-range and gender, for the subject-based example measure:
define "Stratifier P0Y--P21Y":
Patient.ageInYears() between 0 and 20
define "Stratifier P21Y--P41Y":
Patient.ageInYears() between 21 and 40
define "Stratifier P41Y--P9999Y":
Patient.ageInYears() >= 41
define "Stratifier Male":
Patient.gender = 'male'
define "Stratifier Female":
Patient.gender = 'female'To do this using criteria-based stratifiers would require specifying a stratifier expression for each possible combination of age range group and gender. With even larger stratifications, this quickly becomes untenable. Value-based stratifiers simplify this, so the stratification can be expressed as:
define "Age Range Stratifier":
case
when Patient.ageInYearsAt(start of Encounter.period) between 0 and 20 then 'P0Y--P21Y'
when Patient.ageInYearsAt(start of Encounter.period) between 21 and 40 then 'P21Y--P41Y'
when Patient.ageInYearsAt(start of Encounter.period) > 41 then 'P41Y--P9999Y'
else null
end
define "Gender Stratifier":
Patient.gender
define "Age Range and Gender Stratifier":
"Age Range Stratifier" + ':' + Coalesce("Gender Stratifier", 'Unknown')This is better, in that it only requires the creation of a single stratifier element referencing the "Age Range and Gender Stratifier" expression, but it arbitrarily forces construction of stratum values by combining the values from the other stratifiers. Instead, multi-component stratifiers allow this to be done directly:
<stratifier>
<component>
<criteria>
<expression value="Age Range Stratifier"/>
</criteria>
</component>
<component>
<criteria>
<expression value="Gender Stratifier"/>
</criteria>
</component>
</stratifier>It is often the case that the set of possible values for a stratifiers is known and expressible using a terminology. When this is the case, the valueSet element of the stratifier may be used. When this is done, the value set used must be consistent with the other aspects of the stratifier definition. For example, given the "Age Range Stratifier" above, a value set with codes from the Time Period Ranges code system can be used to provide the expected possible values as part of the stratifier specification:
{
"resourceType": "ValueSet",
"url": "http://example.org/ValueSet/example-age-ranges",
...
"compose" : {
"include" : [{
"system" : "http://terminology.hl7.org/CodeSystem/time-period-ranges",
"concept": [{
"code": "P0Y--P21Y",
"display": "0 to 20 years"
}, {
"code": "P21Y--P41Y",
"display": "21 to 40 years"
}, {
"code": "P41--P9999Y",
"display": "41+ years"
}]
}]
}
}This can be used as the valueSet element of the stratifier:
<stratifier>
<component>
<valueSet value="http://example.org/ValueSet/example-age-ranges"/>
</component>
</stratifier>Note that ValueSet stratifiers can be used with or without a computable expression that determines the value. However, if they are used together, they must be consistent (i.e. the expression must always evaluate to a value that is a member of the value set).
In R4, this is accomplished with the cqm-valueSet extension as specified in the CQMComputableMeasure profile.
Typically, stratifiers are constructed such that every member of a given population falls into one and only one stratum (i.e. the strata are disjoint groups). However, this is not required to be the case, so care must be taken to ensure stratifiers are expressed correctly. For example, consider the following stratifiers:
- Age up to 1 week
- Age up to 1 month
- Age up to 1 year
The implied intent is that these stratum are disjoint, but consider this criteria-based representation of this stratifier:
define "Age Up To 1 Week":
AgeInDays() <= 7
define "Age Up To 1 Month":
AgeInMonths() <= 1
define "Age Up To 1 Year":
AgeInYears() <= 1An infant aged 1 week or less would appear in all 3 groups.
Consider the value-based expression:
define "Age Range":
case
when AgeInDays() <= 7 then 'P0D--P8D'
when AgeInMonths() <= 1 then 'P8D--P2M'
when AgeInYears() <= 1 then 'P2M--P2Y'
else null
endAnd the actually equivalent criteria-based expression:
define "Age Up To 1 Week":
AgeInDays() <= 7
define "Age 8 Days To 1 Month":
AgeInDays() >= 8 and AgeInMonths() <= 1
define "Age 2 Months To 1 Year":
AgeInMonths() >= 2 and AgeInYears() <= 1See Measure Conformance - Stratification
The Measure resource now supports the use of a "reportingTiming" extension to specify how often a measure is expected to be reported:
A reporting Timing must specify a "repeat" that has:
- frequency
- period
- periodUnit
- timeOfDay (optionally)
This can be used to specify reporting frequencies such as:
- Monthly
- Twice daily
- Daily at midnight
{
"url" : "http://hl7.org/fhir/uv/cqm/StructureDefinition/cqm-reportingTiming",
"valueTiming" : {
"repeat" : {
"frequency" : 1,
"period" : 1,
"periodUnit" : "d",
"timeOfDay" : [
"00:00:00"
]
}
}
}See reporting timing
Systems can be notified of changes to Measure specifications using a SubscriptionTopic:
{
"resourceType" : "SubscriptionTopic",
"id": "covid-reporting-program-changes",
"url": "http://hl7.org/fhir/uv/cqm/SubscriptionTopic/covid-reporting-program-changes",
...
"resourceTrigger" : [
{
"description" : "CDC Reporting Program Measure resource update",
"resource" : "http://hl7.org/fhir/StructureDefinition/Measure",
"supportedInteraction" : [
"update"
],
"fhirPathCriteria" : "%previous.version < %current.version"
}
],
...
}See COVID Reporting Program Changes
Add a subscription to that topic:
{
"resourceType" : "Subscription",
"id" : "covid-reporting-changes",
"status" : "requested",
"topic" : "http://hl7.org/fhir/uv/cqm/SubscriptionTopic/covid-reporting-program-changes",
"reason" : "ACME hospital to listen of changes tothe CDC Covid Reporting Program",
"criteria" : "Measure?url=http://example.org/fhir/Measure/BedCapacityMeasureMonthly",
"channel" : {
"type" : "rest-hook",
"endpoint" : "https://example.org/fhir/updates/measures",
"payload" : "application/fhir+json",
"header" : [
"Authorization: Bearer secret-token-abc-123"
]
}
}See Emergency Capacity Reporting Example
Measures now support specifying supporting evidence, arbitrary values that can be specified per population to provide additional details about that population:
{
"url" : "http://hl7.org/fhir/StructureDefinition/cqf-supportingEvidenceDefinition",
"valueExpression" : {
"extension" : [{
"url" : "http://hl7.org/fhir/StructureDefinition/expression-coding",
"valueCoding" : {
"system" : "http://example.org/fhir/CodeSystem/example-supporting-evidence-codes",
"code" : "always-true",
"display" : "Always True"
}
}],
"description" : "Example of supporting evidence that is always true",
"name" : "AlwaysTrue",
"language" : "text/cql-identifier",
"expression" : "always true"
}
}Risk-adjusted measures can be expressed with a ratio of continuous-variable measures approach, where the numerator observation is an observed value, and the denominator observation is a predicted value. The CDC Hospital Safety Measure Catheter Associated Urinary Tract Infection (CAUTI) Standardized Infection Ratio (SIR) is such a measure, and the ballot includes an example representation of this measure specification:
Liquid Measure templates have been significantly improved since the last ballot, and the updated templates have been included in the ballot, as well as in the sample content IG, and have just been incorporated into the FHIR base ig template.
The latest release of the liquid templates is v0.5.4.
The templates generally follow the principle that, for criteria, if the measure specification does not have a criteria specified, nothing is rendered in the narrative.
This is true for:
- Population Criteria
- Supplemental Data Elements
- Risk Adjustment Variables
- Stratifier Criteria
However, this has been a common discussion area, and represents an aspect of the human readable for a measure that is different than the QDM eCQM rendering. Specifically, QDM eCQMs will in general include a None or N/A marker in the human readable when an otherwise expected criteria is not present in the measure.
Quality measure implementation guide establishes the set of allowable criteria types per scoring type in Table 3-1: Measure populations based on types of measure scoring. Arranged from the perspective of the scoring type, this gives:
- Cohort
- Initial Population
- Proportion
- Initial Population
- Denominator
- Denominator Exclusion
- Denominator Exception
- Numerator
- Numerator Exclusion
- Ratio
- Initial Population
- Denominator
- Denominator Exclusion
- Numerator
- Numerator Exclusion
- Measure Observation
- Continuous Variable
- Initial Population
- Measure Population
- Measure Population Exclusion
- Measure Observation
Assuming these are the criteria that we want to display in the narrative, regardless of whether the specification includes them, we can adjust the narrative display to always output these sections (in both the metadata and definitions), with the text 'None' if the measure specification does not include the relevant criteria.
For supplemental data elements and risk adjustment factors, these already display in the metadata section as a single element with guidance, so we can adjust that to display a 'None' if no guidance is present in the measure. In addition, the data elements section can be modified to include a header for 'None' when the measure does not define any supplement data.
And similarly for stratifiers, in both the metadata description and the definition, we can include a 'None' indicator if the measure group does not specify any stratification criteria.
In addition, in order to accommodate the possibility that different groups may want to make different decisions about whether missing criteria are rendered in this way, we can tie the display of the 'None' indicator to an extension in the measure specification, allowing the decision to be made potentially at the measure level.
The ballot introduces a new extension to control this behavior:
{
"url" : "http://hl7.org/fhir/uv/cqm/StructureDefinition/cqm-renderMissingElements",
"valueBoolean" : true
}When this extension is present, and true, it indicates that criteria that would otherwise be expected to be present, based on scoring type, should be rendered with an appropriate indicator.
By way of illustration, the following table provides links to example measures for each scoring type, rendered with the new missing element extension:
| Scoring Type | Example Measure (with Missing Elements) |
|---|---|
| Cohort | CohortEmpty |
| Proportion | ProportionEmpty |
| Ratio | RatioEmpty |
| Continuous-Variable | CVEmpty |
Data Exchange for Quality Measures UV STU1
Key Changes
- Generalized to Universal Realm
- Clarified exchange and reporting use cases
- Submit Data as a Bundle POST
- Aligned Submit Data with Upcoming Bulk IG
- Support for Invited Pull
Authoring Patterns - QICore v4.1.1
Authoring Patterns - QICore v5.0.0
Authoring Patterns - QICore v6.0.0
Cooking with CQL Q&A All Categories
Additional Q&A Examples
Developers Introduction to CQL
Specifying Population Criteria