CQL WorkbenchDocs
Docs contents
Core concepts

Core concepts

How the workbench represents a clause: the sources, atomic conditions and groups in the IR, how evidence contracts define each condition’s value, and why the generated CQL only takes parameters. The whole page uses the example clause from the CQL primer.

Clauses and the IR

A document is first split into separate reimbursement clauses, which the workbench’s interface calls tasks. An LLM then analyzes each clause into an IR (intermediate representation): a JSON document recording which conditions the clause contains, how they combine, and which passage of the source each one comes from.

The IR is the single source of truth. The CQL is generated from it, never written by hand, and the IR is also what the workbench’s Decision logic tab (決策邏輯) and the review page show and edit. Saving the logic regenerates the CQL and re-runs the checks, so what you see is what gets exported.

An IR has four parts:

FieldContentsExample clause
source_unitsSources: verbatim quotes from the clause, each with an ID.S1–S4
nodesAtomic conditions: each one a yes/no question.C1–C4
groupsGroups: combine conditions and other groups with AND, OR and NOT.G1, G2
decision.rootThe root: points at the outermost group, the verdict for the whole clause.GROUP:G1

Here is where the example clause’s four quotes sit in the source:

本品限內科專科醫師處方S1,病人需為成人(年滿十八歲)S2,且 eGFR ≥ 30 mL/min/1.73m²S3;未達標準者,須經腎臟科會診同意S4後使用。

Translation: Must be prescribed by an internal medicine specialist. The patient must be an adult (18 or older) with eGFR ≥ 30 mL/min/1.73m². Below that threshold, use requires nephrology consultation approval.

This is the example clause’s IR with nodes left out (they’re covered in the next section). Each condition points at these sources through source_ids; C3, for example, points at S3.

{
  "source_units": [
    { "source_id": "S1", "exact_quote": "限內科專科醫師處方" },
    { "source_id": "S2", "exact_quote": "病人需為成人(年滿十八歲)" },
    { "source_id": "S3", "exact_quote": "eGFR ≥ 30 mL/min/1.73m²" },
    { "source_id": "S4", "exact_quote": "須經腎臟科會診同意" }
  ],
  "groups": [
    { "group_id": "G1", "operator": "all", "members": ["C1", "C2", "GROUP:G2"] },
    { "group_id": "G2", "operator": "any", "members": ["C3", "C4"] }
  ],
  "decision": { "root": "GROUP:G1" }
}

Each exact_quote must appear word for word in the source. A quote that can’t be found doesn’t fail the analysis, but it is flagged for review, warning reviewers that conditions citing it may rest on text the model filled in itself. See Quote verification.

Atomic conditions and groups

Atomic conditions

An atomic condition (a node in the IR) is a yes/no question about the patient that asks exactly one thing. This is the node for C3:

{
  "node_id": "C3",
  "label": "eGFR ≥ 30 mL/min/1.73m²",
  "source_ids": ["S3"],
  "clinical_question": "病人的 eGFR 是否 ≥ 30 mL/min/1.73m²?",
  "definition": { "operator": "gte", "value": 30, "unit": "mL/min/1.73m²" }
}
node_id
The condition’s ID, numbered C1, C2 and so on. It is also the name of the CQL parameter, so it must start with a letter, contain only letters, digits and underscores, and be unique.
label
A short name shown on screen.
source_ids
Which quotes the condition comes from. At least one, and only IDs that exist in source_units.
clinical_question
The yes/no question the condition answers. It can’t be empty.
definition
How to decide it (optional): an operator plus a value and unit. The operator must be one of exists, not_exists, eq, ne, gt, gte, lt, lte, in, not_in, between; every operator except exists and not_exists needs a value, and only in, not_in and between take a list.

One condition, one question: When the CQL is generated, each condition becomes a single parameter; its definition isn’t written into the CQL. Pack “A and B” into one condition and the “and” silently disappears from the CQL, while the screen still shows what looks like a reviewable condition. That’s why the operator can’t be a combinator such as and, or or not; when the LLM writes one, the pipeline sends the whole IR back to be split into groups.

Groups

A group combines conditions and other groups. There are three operators, and each maps to one CQL operator:

operatorMeaningCQL
allEvery member holds (AND)and
anyAt least one member holds (OR)or
notIts single member doesn’t hold (NOT); exactly one membernot (…)

In members, a condition is written as its ID, like C1; a subgroup takes a prefix: GROUP:G2. decision.root points at the outermost group. The example clause looks like this:

  • G1all
    • C1Prescriber is an internal medicine specialist
    • C2Patient is 18 or older
    • G2any
      • C3eGFR ≥ 30 mL/min/1.73m²
      • C4Nephrology consultation approved

During analysis, each condition goes into exactly one group, and every condition and group must be reachable from the root; anything unreachable is treated as a structural error and sent back for repair. Groups can’t form a cycle either.

Exclusion clauses need care. “Not reimbursed if any of the following applies” means any single item excludes the patient, so the IR puts those items in an any group and wraps it in a not group, never a not over an all. If the clause had two exclusions, C5 and C6, the generated CQL would be:

define "Group G3":
  "Criterion C5"
  or "Criterion C6"

define "Group G4":
  not ("Group G3")

If an exclusion can’t be checked, not null is still null, so the patient doesn’t become eligible by default. The full rules are in Three-valued logic in the CQL primer.

On the review page, reviewers can switch a group between AND, OR and NOT and move conditions between groups; conditions and groups cut off from the root are listed under Unlinked (未連結) until they’re added back. See Editing logic.

Evidence contracts

Every condition in the CQL is a parameter, and someone has to supply its value. The comment at the top of the CQL calls that party an evidence adapter: a program reading hospital data, an NLP tool extracting from text, or a human reviewer. The workbench doesn’t prescribe which. Instead it generates an evidence contract for each clause that spells out what each value means:

ValueWhat the contract says it means
trueAccepted evidence confirms the condition.
falseAccepted evidence contradicts the condition.
nullEvidence is missing, stale, ambiguous, or not mapped to a data source.

The contract has one entry per condition, copied from the IR: the parameter name, clinical question, definition, sources, and preferred and fallback evidence. Every entry’s missing_result is unknown: if nothing is found, the answer is “don’t know”. The per-condition cross-check also notes on each IR condition which FHIR R4 resource and element could prove it, as a hint for whoever builds the adapter.

The contract also lists what an adapter records when it resolves a condition:

status
true, false or unknown. Unknown goes into the CQL as null.
evidence_refs
Where the evidence is: resource IDs, database row IDs, or document and page anchors.
observed_at
When the clinical evidence applied.
resolved_by
Who decided: a data adapter, an NLP extractor or a human reviewer.
confidence
A confidence score from 0.0 to 1.0.
notes
Notes on ambiguity and on reconciling sources.

Confidence never turns unknown into false: The contract says so explicitly: however low the confidence, “don’t know” must not become “doesn’t meet”. A patient with missing data should get insufficient-data, not ineligible.

The workbench has no built-in adapter that reads patient records. To see what a set of values produces, open the Eligibility tab (資格評估) in the workbench, set each condition to meets (符合), doesn’t meet (不符合) or unknown (未知), and press Evaluate (執行判定). The CQL engine returns eligible, ineligible or insufficient data, and marks whether the result agrees with the IR’s three-valued logic.

The contract is exported with the package, under fixtures, for your data pipeline to build its adapter against.

Parameter-based CQL

renderCql generates the CQL from the IR. It writes no FHIR retrieve (the CQL syntax for pulling data from the record, such as [Observation]); instead each part of the IR maps to CQL in a fixed format:

IRGenerated CQL
Each condition, e.g. C3parameter "C3" Boolean default nulldefine "Criterion C3"
Each group, e.g. G2 (any)define "Group G2"members joined by or
decision.rootdefine "Eligibility"
(always generated)define "Eligibility Status"maps to eligible, ineligible or insufficient-data

For a line-by-line read of the generated file, see Parameters in the CQL primer.

Why parameters

  • Clause logic stays separate from data mapping. The clause says “eGFR ≥ 30”; it doesn’t say which system holds eGFR or which code it uses. During analysis the LLM is told not to choose hospital-specific mappings; that is left to the adapter. So the same CQL works whether the values come from a FHIR server, a hospital database or a person reading the chart.
  • Each condition can be tested on its own. Parameters can be overridden one at a time, with no need to synthesize a full set of FHIR test data first. That is exactly what the ablation check below does.
  • Missing data is an explicit null. Parameters default to null, so a condition nobody filled in is “don’t know”, and Eligibility Status turns that into insufficient-data rather than ineligible. See The result and is true and Four scenarios in the CQL primer.

The trade-off is that the CQL can’t look anything up by itself: if nobody supplies values, every parameter is null and the result is always insufficient-data. The NHI Administration’s official pre-check rules take the other route and read FHIR request data directly in CQL; see Where CQL Workbench fits for a comparison.

The ablation check

Because every condition is a parameter, the check can knock conditions out one at a time. The workbench first evaluates with every parameter set to true (the baseline), then sets each condition in turn to false and to null, keeping the rest true. cql-engine compiles the CQL to ELM, the backend’s CQL execution engine (cql-execution) computes Eligibility, and a case passes only if that result equals what the IR’s three-valued logic expects. The example clause has 4 conditions, so 9 cases:

CaseOverrideExpected Eligibility
baselineall truetrue
C1-falseC1 = false, others truefalse
C1-nullC1 = null, others truenull
C2-falseC2 = false, others truefalse
C2-nullC2 = null, others truenull
C3-falseC3 = false, others truetrue
C3-nullC3 = null, others truetrue
C4-falseC4 = false, others truetrue
C4-nullC4 = null, others truetrue

All four C3 and C4 cases stay true: G2 is an OR, and the other condition still holds. If the CQL had G2 as an and, those four cases would fail. Results appear in the workbench’s Validation report tab (驗證報告).

The ablation check only confirms that the CQL matches the IR: whether the IR is faithful to the clause is up to quote verification and physician review.