Skip to content

From FHIR data to measure results

  1. Acquire — pull FHIR resources from the EHR (bulk export, paginated search or per-patient cascade) and keep the raw NDJSON as an archive.
  2. Normalize — inject missing reference types, fix encounter statuses, attach attribution (TIN, NPI) from your own configuration.
  3. Identify the denominator — patients in the age band with qualifying encounters in the measurement period and the measure’s condition (for example diabetes).
  4. Load only those patients’ data into a working store indexed by patient and date.
  5. Evaluate each measure — exclusions, numerator, exceptions — and record one result per patient (or per encounter, for episode-based measures).
  6. Report — QRDA Category III for eCQMs, or QPP JSON (performance met / not met / eligible population / exceptions) for direct submission.

Keeping acquisition separate from evaluation is what makes the pipeline maintainable: when measure logic changes, you re-run steps 3–6 from the archive without touching the EHR again.

To know the results are right, validate them: certify the calculation with Cypress each year, then compare your FHIR results with the EHR’s own QRDA I export and quality dashboard. See Validating FHIR results.

The same measure number can have two specifications with different rules. The most visible difference is when age is measured:

Measure MIPS CQM eCQM
#001 Diabetes: glycemic status > 9% (CMS122) 18–75 at the encounter 18–75 at end of period
#112 Breast cancer screening (CMS125) 41–74 at start of period 42–74 at end of period
#113 Colorectal cancer screening (CMS130) 45–75 at the encounter 46–75 at end of period
#236 Controlling high blood pressure (CMS165) 18–85 at the encounter 18–85 at end of period
#134 Depression screening (CMS2) 12+ at start of period 12+ at start of period

The code lists differ too — for example, the 2026 MIPS CQM diabetes code list and the eCQM diabetes value set do not contain the same number of codes. Extract the superset of both age bands, keep date of birth, and apply the right rule at calculation time.

  • Never hard-code clinical codes in measure logic. Load value sets each year — from VSAC for eCQMs, and from CMS’s annual code file for MIPS CQMs — and look codes up by value set.
  • Some measures also use direct-reference codes named in the specification (for example a specific LOINC or SNOMED code). Those are legitimately fixed for that year.
  • Match on every coding and translation, never only coding[0]. Normalize case before comparing.
  • Where overlapping value sets share codes, look each value set up separately so one does not hide the other.
  • Choose a date in a fixed order of precedence, for example effectiveDateTime → effectivePeriod.start → issued. Exclude resources with no usable date rather than guessing.
  • Measurement periods are calendar years, and windows in the specifications are closed at both ends.
  • Compute age in whole years with a birthday adjustment, anchored where the specification says (start of period, end of period, or encounter date).
  • Drop future-dated records.
  • Most recent, then lowest: for lab and vital-sign measures, take the most recent day, then the lowest value on that day (A1c, systolic and diastolic separately).
  • Deterministic tie-breaks: always order by a total key (date, then a stable id) so a re-run gives the same answer.
  • Exclusions win: if any record for a patient qualifies for an exclusion, report the exclusion.
  • Deduplicate resources by id at ingest, and patients across sources by name and date of birth.
  • When encounter types are poorly coded, a strict qualifying-encounter check excludes almost everyone. Enforce it only when the value sets match a meaningful share of encounters, and flag results calculated in lenient mode.
  • Text fallbacks (matching “mammogram” in a report title, for example) recover real numerator events from poorly coded EHRs — but turn them off for well-coded sources, where they only add false positives.
  • Never leave a result blank: record “not met” or “not documented” explicitly, so a missing value is not mistaken for “not reported”.

eCQMs are published in CQL, but many production systems implement each measure’s logic by hand and use the CQL as the specification and the source of value-set OIDs. If you do this, watch for the translation traps:

  • CQL intervals are closed: Interval[24, 64] includes both 24 and 64.
  • Check the age anchor — age at end of period and age at the test date give different answers.
  • result is not null means an ordered but unresulted test does not count.
  • Qualifying-encounter lists differ between measures that look alike — one may include the Annual Wellness Visit and its neighbour not.
  • Check for stratifiers: an age range in a stratification is not the denominator’s age range.
  • Use the measure package that matches what you are certified against (QDM vs QI-Core); OIDs that happen to match can hide that you are reading the wrong one.