Common CQL Artifacts for FHIR (US-Based)
2.0.0 - Informative 2 United States of America flag

Common CQL Artifacts for FHIR (US-Based), published by HL7 International / Clinical Decision Support. This guide is not an authorized publication; it is the continuous build for version 2.0.0 built by the FHIR (HL7® FHIR® Standard) CI Build. This version is based on the current content of https://github.com/HL7/us-cql-ig/ and changes regularly. See the Directory of published versions

Query Management

Page standards status: Informative

This topic provides query management guidance for implementers to support the use of CQL within FHIR artifacts such as questionnaires, typically from a DTR application. The functions and guidance provided here facilitate:

NOTE: The guidance in this section discusses several options for accessing data more broadly when more specific capabilities are not available. As much as possible, these fallback retrieval patterns should be bounded by clinical context, measurement or prior authorization period, resource type, and supported search parameters, and should avoid broad payer-driven data sweeps where targeted queries are available.

Overview

All data access within CQL is expressed abstractly, using the Retrieve expression, and surfaced from a packaged Library as dataRequirement elements. These data requirements describe what data the logic needs, independent of how any particular server exposes it. A DTR-style client that evaluates CQL must bridge that gap: translating abstract data requirements into concrete FHIR searches, executing them against a server whose capabilities vary, and shaping the results so the CQL engine can evaluate the logic correctly.

Query management is the collection of techniques and helper functions used to bridge that gap. This topic complements the Initial Data Retrieval discussion in the Architectural Guidance topic, which describes how the effective data requirements for a specific set of expressions are determined; the guidance here focuses on turning those requirements into robust, performant queries against real-world servers.

A recurring theme throughout is the separation between correctness and optimization: query management techniques such as _elements filtering, _include, and less-selective search parameters change how data is retrieved, but the CQL logic must remain correct regardless of which technique is used. When a technique cannot be supported by a given server, the data-access layer should fall back to a more broadly supported query and, where necessary, filter locally, rather than allowing the result of the logic to change.

Optional Server-Side Parameters

The search parameter used to satisfy a given retrieve may vary depending on the capabilities of the server being accessed. Many US Core search parameters are optional (for example, the code parameter for Condition), so a query that is ideal against one server may not be supported by another.

This situation, and the use of a Module Configuration Library to provide alternative FHIR queries, is described in the Performant Data Access discussion in the Architectural Guidance topic. In summary, when a preferred search parameter is unavailable, a less-selective filter can be used (for example category=problem-list-item in place of code) followed by local filtering in the CQL, at the cost of transferring more data.

Related to this, terminology-based filters (the :in modifier used to filter a retrieve by value set) are also optional server capabilities. The Terminology Considerations discussion describes the approaches available when a server cannot perform value set membership filtering server-side, including choosing a less-selective filter or in-lining an expansion of the value set codes.

Modifier Elements and Extensions

Because modifier elements and modifier extensions can change the overall meaning of a resource, any resource retrieved for evaluation should be considered with respect to these elements before the data is used.

This is discussed in detail in the Element Considerations section of the Overall Patterns topic:

  • Modifier Elements: For each modifier element (for example, Condition.clinicalStatus), authors must consider whether each possible value would impact the intent of the expression. The FHIRCommon library provides fluent functions such as .active() and .confirmed() for filtering based on these status elements.
  • Modifier Extensions: A resource that carries a modifier extension that is not understood cannot be safely processed. The checkModifiers() function can be used within CQL to ensure that either no modifier extensions are present, or that only expected modifier extensions are present.
  • ImplicitRules: The implicitRules element is a modifier element that, if present, must be understood. A checkImplicitRules() function can be used to guard against unexpected implicit rules.

From a query management perspective, these checks can be performed either in the CQL logic itself, or as part of the surrounding application environment (for example, by validating retrieved resources before handing them to the CQL engine). Best practice is to be explicit about where the responsibility lies so that unexpected modifiers cause a clear error rather than silently altering the result of the logic.

_include and _revInclude

CQL logic frequently traverses references between resources. For example, a MedicationRequest may reference a Medication resource through medicationReference, or an Observation may reference the Specimen from which it was collected. When data requirements analysis reveals such a traversal, the data-access layer has two broad choices for retrieving the referenced resources:

  1. Issue a separate query for the referenced resources (for example, retrieve the MedicationRequest resources, collect the referenced Medication ids, then query for those Medication resources).
  2. Use the _include search parameter to ask the server to return the referenced resources alongside the primary results in a single round-trip.

For example, to retrieve medication requests together with their referenced medications:

[base]/MedicationRequest?subject=Patient/123&_include=MedicationRequest:medication

The _revinclude parameter handles the reverse direction — retrieving resources that reference the primary results. This is useful when the logic needs, for example, the Provenance resources that reference the retrieved data, or the Observation resources that reference a retrieved Specimen:

[base]/Specimen?subject=Patient/123&_revinclude=Observation:specimen

Guidance for managing _include and _revInclude:

  • Prefer _include/_revInclude when a reference traversal is present in the data requirements, as it reduces the number of round-trips to the server and avoids a second query that depends on the results of the first.
  • Not all servers support every _include/_revInclude target. The set of supported includes is advertised in the server's CapabilityStatement (rest.resource.searchInclude and searchRevInclude). When a target is not supported, fall back to issuing separate queries.
  • Contained references do not require an include. When a referenced resource is contained within the referencing resource, it is already present and no additional query or include is needed. Logic that follows references should account for both contained and external references (the .references() function in the shared libraries handles this distinction).
  • Watch for over-retrieval. A broad _revinclude can pull back a large number of resources; where only a specific reference is of interest, prefer a targeted include over _revinclude=*.

As with other query management techniques, the choice between separate queries and includes is an optimization: the CQL logic must produce the same result regardless of which approach is used.

_element Filtering

Because data requirements analysis can determine precisely which elements of a resource the CQL logic accesses, a client can use the _elements search parameter to request only those elements from the server, reducing the size of the payload that must be transferred and parsed:

[base]/Observation?subject=Patient/123&category=laboratory&_elements=status,code,value,effective

This can be a meaningful optimization for resources with large or numerous elements that the logic does not use. However, several considerations apply:

  • _elements is a hint, not a guarantee. A server may return more elements than requested. In particular, servers are expected to always return mandatory elements and any modifier elements, regardless of the _elements value, so that the returned resources remain safe to process.
  • Always include the elements needed for correctness. In addition to the elements the logic reads directly, the requested set must include any modifier elements and implicitRules (see Modifier Elements and Extensions) so that the guard checks can be performed. Omitting these to save payload would undermine correctness.
  • Resources may be tagged SUBSETTED. When a server honors _elements, it flags the returned resources with the SUBSETTED tag to indicate that they are not complete representations. These resources should not be persisted back as if they were complete.
  • Correctness must not depend on _elements. As with the other techniques described here, _elements is purely an optimization. If a server ignores the parameter and returns full resources, the logic must still evaluate correctly.

Version Differences Among Servers

CQL-based artifacts are commonly evaluated against a range of servers that differ along several dimensions. Query management must account for each:

  • US Core version differences. Systems in the field support different versions of US Core (for example, moving from 3.1.1 through 6.1.0 and 7.0.0). Newer versions generally add profiles rather than change the representation of existing ones, but there are exceptions. These are called out per resource type in the Cross-Version Considerations sections of the Patterns topic (for example, Observation Cross-Version Considerations). Where an element or extension changed across versions, the shared libraries provide forward- and backward-compatible functions — for example, USCoreCommon exposes both the deprecated .sex() function and the forward-compatible .individualSex() function so that logic can be written once and evaluated against either representation.
  • FHIR server search capability differences. Servers differ in which search parameters, terminology filters, and includes they support, as described in Optional Server-Side Parameters and _include and _revInclude. A Module Configuration Library can be used to supply per-server alternative queries.
  • Value set version differences. When logic is sensitive to value set or code system versions, a Version Manifest can be used to pin the specific versions used for a particular deployment, as described in the Terminology Considerations discussion.

A general pattern for managing these differences is to keep version-specific behavior out of the artifact logic and instead resolve it in the shared libraries (through compatibility functions) and the surrounding configuration (through module configuration and version manifests). This keeps the authored expressions stable while allowing the same artifact to be deployed against a heterogeneous set of servers.

Common Search Parameter Filters

Many retrieves share the same routine post-retrieval filtering — for example, restricting observations to results that are final, amended, or corrected, or restricting resources to a particular status or category. Rather than repeating these filters in every expression, the shared libraries provide fluent functions that encapsulate the common cases.

For observations, these are described in the Common Elements and Functions section of the Observation Patterns topic, including:

  • Status filters: resulted() (returns observations with a status of final, amended, or corrected), along with final(), amended(), and corrected(), and the corresponding singleton predicates isResulted(), isFinal(), isAmended(), and isCorrected().
  • Category filters: .hasCategory(Code), .isLaboratory(), .isVitalSign(), .isSurvey(), and related helpers.

For example, retrieving only resulted laboratory observations is as simple as:

define "Resulted Laboratory Results":
  "All Laboratory Results".resulted()

In general, the USCoreElements library follows the convention of providing an All Statuses variant of each observation expression that returns observations regardless of status, alongside a default expression that applies .resulted(). This lets authors choose between the filtered and unfiltered forms without re-writing the underlying retrieve. Where a use case needs a different common filter, the same pattern — a small fluent function that expresses the filter once — should be followed and contributed back to the shared libraries.

Extension Retrieval

Many data elements that logic needs to access are represented as extensions rather than as first-class elements — for example, the US Core race, ethnicity, birth sex, and tribal affiliation extensions on Patient, or extensions defined by Da Vinci CRD/DTR. Accessing extensions directly through their canonical URLs is verbose and error-prone, so the shared libraries expose expected extensions as named fluent functions.

The USCoreCommon library defines these functions on top of the generic ext() and exts() fluent functions (which return a single extension or all extensions with a given URL, respectively). For example:

define fluent function birthSex(patient FHIR.Patient):
  patient.ext('http://hl7.org/fhir/us/core/StructureDefinition/us-core-birthsex').value as FHIR.code

This allows logic to access the extension by name, without repeating the canonical URL or the value type cast:

define "Patient Birth Sex":
  Patient.birthSex()

USCoreCommon provides functions of this kind for the US Core patient extensions (race(), ethnicity(), birthSex(), individualSex(), tribalAffiliation(), sexParameterForClinicalUse()), as well as for other profile-specific extensions such as the PDMP prescription transmission method. Refer to the US Core Common library for the complete set.

Guidance for extension retrieval:

  • Only rely on extensions that are expected to be present. As with any element, an extension should only be depended upon when the profile marks it must support or there is otherwise a reasonable expectation of its presence (see the Element Considerations discussion). Extension accessors return null when the extension is absent, so logic must handle the null case.
  • Wrap expected extensions in a named function. For extensions defined by other implementation guides that logic depends on — such as Da Vinci CRD coverage-information or DTR extensions — follow the same pattern of defining a fluent function that encapsulates the canonical URL and the value type. This keeps the canonical URL in one place, makes the logic readable, and localizes the impact if the extension definition changes across versions.
  • Use exts() for repeating extensions. When an extension may appear more than once (for example, the detailed component of the race extension), use exts() to return all occurrences and process them as a list, as illustrated by the race() and ethnicity() functions.

Complex Element Retrieval

Some data elements needed by an artifact are not stored directly as a single value but must be derived from other data, sometimes through non-trivial computation. To keep artifact logic focused and consistent, this guide provides reusable libraries and expressions for these complex derivations.

Cumulative Medication Duration

The total duration of a medication — accounting for dosage, timing, and dispense information — is a common but intricate calculation. The Cumulative Medication Duration library provides logic for computing cumulative medication duration from FHIR MedicationRequest, MedicationDispense, and MedicationAdministration resources, following the guidance established in the 5.6 version of the Quality Data Model.

Note that this logic assumes single-instruction dosing; split-dosing, tapering, and other more complex dosing instructions are not handled. As with all such patterns, care should be taken to ensure the data on which the logic runs conforms to this assumption.

Body Surface Area

Body Surface Area (BSA) is often needed for dosing and eligibility logic. Best practice is to prefer a recorded value when one is available, and only compute a value as a fallback. The USCoreElements library provides:

  • "Most Recent BSA": the most recent resulted Body surface area observation value, converted to m2.
  • CalculateBSA(algorithm, height, weight): computes BSA from height and weight using either the Mosteller or DuBois and DuBois formula, for use when no recorded observation is available.

An artifact can then combine these to prefer the recorded value and fall back to a calculation, for example:

define "BSA":
  Coalesce("Most Recent BSA", "Calculated BSA - Mosteller")

This pattern — retrieve the recorded value where present, compute only as a documented fallback, and make the computation method explicit — generalizes to other derived clinical values.

See Also