OpenEMR calculates eCQMs from a measure bundle for each performance period:
for every measure, the measure JSON (including its CQL, compiled as ELM) and its
value sets expanded from VSAC. This repository holds both the tool that builds
the bundles and the bundles themselves, one {year}_reporting_period directory
per performance period.
The tool runs cqm-parsers in a Docker container, so no Ruby setup is needed.
OpenEMR's composer.json defines the Composer package openemr/oe-cqm-parsers
inline, pinned to one commit of a bundle repository. Composer installs it in
vendor/openemr/oe-cqm-parsers, and OpenEMR reads
vendor/openemr/oe-cqm-parsers/{year}_reporting_period/json_measures, with the
year taken from the global Eligible Clinician eCQM Performance Period.
Changes here reach OpenEMR only when OpenEMR pins a newer commit.
Once a year, when CMS publishes the eCQM Annual Update:
- Build the bundle in this repository (see below), check the summary says no measure failed, then commit and push.
- Pin it in OpenEMR in one pull request:
- In
composer.json, in the inlineopenemr/oe-cqm-parserspackage, set the repository URL, bothreferencevalues and the zipball URL to the new commit, and raiseversion. Then runcomposer update openemr/oe-cqm-parsers. - Add the generated
ecqm_{year}_reporting.sqltosql/database.sql, and to the currentsql/*-upgrade.sqlwith av_databasebump.
- In
- Verify it (see Verify it).
- Users choose the new year in Administration > Globals > CDR > Eligible Clinician eCQM Performance Period. Changing its default is a separate decision.
- Docker with Compose v2.24 or later.
- A free UMLS account and its API key (UTS profile page). VSAC uses it to expand value sets.
- The CMS eCQM packages for the period: on the
eCQI Resource Center, choose
Eligible Clinician eCQMs for the performance period and download the
measure packages. Each measure is a zip, for example
CMS122-v14.0.000-QDM.zip.
-
Set up once:
cp .env.sample .env # then fill in VSAC_API_KEY -
Put the packages in
{year}_reporting_period/cms_measures/. -
Build:
docker compose run --rm builder ruby build_bundle.rb --year 2026
Each package becomes
json_measures/{cms_id}/{cms_id}.jsonandjson_measures/{cms_id}/value_sets.json. Measures already built are skipped, so running again retries only the ones that failed. Failures are listed at the end and written tofailed_measures_{timestamp}.login the period directory. VSAC errors are usually temporary.--rebuildbuilds everything again. -
Generate the measure list for OpenEMR's QRDA screens:
docker compose run --rm builder ruby generate_ecqm_list.rb --year 2026
This writes
ecqm_2026_reporting.sql, guarded with#IfNotRowso it can go into an upgrade script as it is. Use--defaults CMS122v,CMS165v,...to choose which measures are active by default.
If you change build_bundle.rb or generate_ecqm_list.rb, run
docker compose build to rebuild the image.
Each period's value sets come from the eCQM Annual Update published the year before (the May update is for the next performance period):
| Performance period | VSAC profile |
|---|---|
| 2026 | eCQM Update 2025-05-08 |
| 2025 | eCQM Update 2024-05-02 |
| 2024 | eCQM Update 2023-05-04 |
| 2023 | eCQM Update 2022-05-05 |
build_bundle.rb --help prints the table the script uses. For a new period, add
its line to VSAC_PROFILES in build_bundle.rb, or pass --profile. A wrong
profile still builds, but its value sets are wrong, so results are silently
wrong.
cqm-parsers fails a measure when VSAC expands one of its value sets to
nothing. Some packages still declare a retired value set they no longer use:
CMS157v14 (2026) declares the legacy ONC Administrative Sex
(2.16.840.1.113762.1.4.1) beside its replacement, Federal Administrative Sex,
and the 2025-05-08 profile expands it to nothing. Value sets listed in
ALLOWED_EMPTY_VALUE_SETS in build_bundle.rb, or passed with
--allow-empty-value-set OID, are kept empty instead. The build still fails the
measure if its logic (any ValueSetRef in its ELM) uses an empty value set, so
an empty value set can never silently drop data.
OpenEMR loads a measure from json_measures/{option_id}/{option_id}.json, and
the measure list's option_id is the measure's cms_id (for example
CMS122v14). Package file names changed in 2026 (CMS122-v14.0.000-QDM.zip),
so directories are always named by cms_id, never by the package file. To fix a
period built before this rule:
docker compose run --rm builder ruby build_bundle.rb --year 2026 --rename-existingIn OpenEMR, after pinning the new commit:
vendor/bin/phpunit -c phpunit-isolated.xml tests/Tests/Isolated/Services/Qdm/MeasureCalculationMethodTest.phpchecks every measure of every bundle.- Parity with the cqm-execution reference: generate test fixtures for the
period, then run
tests/Tests/Isolated/Cqm/Parity/PhpEngineParityTest.php. - End to end on a test database:
OPENEMR_ENABLE_CQM_TEST_PATIENTS=1 php bin/console openemr:cqm-load-test-patients --year=2026 --verify.
- Only the builder container is needed. cqm-parsers builds measures and value sets in memory, so no database is involved.
Gemfile.lockpins every gem, including the cqm-parsers commit, so a measure rebuilt from the same package gives the same measure JSON apart from record ids, which are generated on each build..gitattributeskeeps LF line endings, also on Windows checkouts.- Never commit
.env; it holds your VSAC API key.