The CaboLabs AQL Guide: A Practical Reference to the Archetype Query Language
Archetype Query Language (AQL) is the openEHR standard query language for archetype-based clinical data. Its reason for existing is portability: mainstream query languages like SQL depend on the physical data schema of a particular database, so a query written for one system will not usually work on another — even when both store the same clinical data. AQL solves this by querying against the clinical models — archetypes and the openEHR Reference Model (RM) — rather than physical storage, making queries shareable across systems and enterprise boundaries [1]. This guide condenses the official AQL specification into a practical reference: the query anatomy, the path and predicate machinery, operators and functions, and a method for writing queries by hand.
Anatomy of an AQL Query
An AQL statement has five clauses, which must appear in this order: SELECT (mandatory — what to return), FROM (mandatory — the data source and containment scope), WHERE (optional — value criteria), ORDER BY (optional — sorting), and LIMIT (optional — pagination). The canonical example from the specification — abnormal blood pressures in one EHR, latest first [1]:
SELECT
o/data[at0001]/events[at0006]/data[at0003]/items[at0004]/value/magnitude AS systolic,
o/data[at0001]/events[at0006]/data[at0003]/items[at0005]/value/magnitude AS diastolic,
c/context/start_time AS date_time
FROM
EHR e[ehr_id/value=$ehrUid]
CONTAINS COMPOSITION c[openEHR-EHR-COMPOSITION.encounter.v1]
CONTAINS OBSERVATION o[openEHR-EHR-OBSERVATION.blood_pressure.v1]
WHERE
o/data[at0001]/events[at0006]/data[at0003]/items[at0004]/value/magnitude >= 140 OR
o/data[at0001]/events[at0006]/data[at0003]/items[at0005]/value/magnitude >= 90
ORDER BY c/context/start_time DESC
LIMIT 5
The mental model: FROM defines the subset of the repository the query runs over, WHERE filters within that subset, and SELECT projects exactly the data to return — from whole objects (a full COMPOSITION) down to primitive values. Keywords are case-insensitive.
Paths: How AQL Locates Data
AQL uses the openEHR path syntax in two flavors [3]. Archetype paths navigate nodes defined in an archetype, using at-codes as node identifiers — /data[at0001]/events[at0006]/data[at0003]/items[at0004]/value resolves to the systolic DV_QUANTITY in the blood pressure archetype. RM attribute paths point at Reference Model attributes not defined by any archetype — /context/start_time, /uid/value, /category on a composition. An identified path combines a variable declared in FROM with a path (and optionally a predicate): o/data[at0001]/.../value/magnitude. Variables are declared only where needed — classes in FROM that no other clause refers to don't need one.
Practical rule: never hand-type archetype paths from memory. Extract them from the archetype using the modelling tools listed on the openEHR website — at-codes are archetype facts, and specialized archetypes introduce dotted codes (at0013.1) that only make sense against the specialization hierarchy.
Predicates: Three Kinds of Brackets
Everything in square brackets is a predicate, and AQL defines three types [1]:
- Standard predicate — full form with operand, operator, and value:
[ehr_id/value='123456'],[ehr_id/value=$ehrUid]. - Archetype predicate — a shortcut containing only an archetype ID, used exclusively in the
FROMclause to scope the data source:[openEHR-EHR-OBSERVATION.blood_pressure.v1]. It is formally equivalent to a standard predicate onarchetype_node_id, which is exactly how engines canonicalize it. - Node predicate — fine-grained criteria on nodes, from a bare at-code
[at0002], through name-based disambiguation[at0002, 'Systolic']or[at0002 and name/value=$nameValue], to term-coded names like[at0002, snomed_ct(3.1)::313267000]and general criteria such as[at0002 and value/defining_code/terminology_id/value=$terminologyId][2].
Name-based node predicates are the tool for repeated sibling structures — two clusters with the same at-code distinguished only by their runtime names — a situation every real-world lab-results query eventually meets.
Operators, matches, and Terminology
Comparison operators are the usual set (=, !=, >, >=, <, <=) plus two pattern matchers. LIKE does simple string patterns — ? matches one character, * any sequence, and the whole value must match, so "contains" searches need '*term*'. matches is the powerful one, taking three right-hand forms inside curly braces [1]:
-- 1. Value list (implicit OR across items)
WHERE o/.../code_string matches {'18919-1', '18961-3', '19000-9'}
-- 2. Terminology URI (e.g., a SNOMED CT hierarchy)
WHERE diagnosis/data/items[at0002.1]/value/defining_code
matches { terminology://snomed-ct/hierarchy?rootConceptId=50043002 }
-- 3. TERMINOLOGY() function calling an external terminology server
WHERE p/data/items[at0002]/value/defining_code/code_string
matches TERMINOLOGY('expand', 'hl7.org/fhir/4.0',
'http://snomed.info/sct?fhir_vs=isa/50697003')
The TERMINOLOGY(operation, service_api, params_uri) function is AQL's bridge to terminology services: operations like expand, validate, map, and subsumes executed against, for example, a FHIR terminology service, with results fed back into the query. This is how "all diagnoses that are a subtype of autoimmune disease" becomes one query instead of an application-side join.
Logical operators are AND, OR, NOT, and EXISTS (does data exist at this path). Two composition-level idioms deserve attention: NOT EXISTS c/content[...] filters on the absence of an entry within matched data, while NOT CONTAINS in the FROM clause expresses an exclusion constraint on containment itself — e.g., referral compositions that contain no lab result observation. Containment expressions compose with AND, OR, and parentheses, so multi-archetype scopes like "encounters containing an HbA1c or a glucose observation" are first-class.
Functions and Aggregation
AQL defines built-in single-row functions — string (LENGTH, CONTAINS, POSITION, SUBSTRING, CONCAT, CONCAT_WS), numeric (ABS, MOD, CEIL, FLOOR, ROUND), and date/time (CURRENT_DATE, NOW(), CURRENT_TIMEZONE) — plus aggregates: COUNT (with DISTINCT and * forms), MIN, MAX, SUM, AVG, which ignore NULLs [1]. Aggregating clinical values is a one-liner:
SELECT
MAX(o/data[at0001]/events[at0006]/data[at0003]/items[at0004]/value/magnitude) AS maxSystolic,
AVG(o/data[at0001]/events[at0006]/data[at0003]/items[at0004]/value/magnitude) AS meanSystolic
FROM EHR e CONTAINS COMPOSITION c[openEHR-EHR-COMPOSITION.encounter.v1]
CONTAINS OBSERVATION o[openEHR-EHR-OBSERVATION.blood_pressure.v1]
Note the specification defines this core set; implementations may offer additional functions, which is a portability consideration when queries must run across platforms.
Results, Ordering, and Pagination
Three behaviors trip up newcomers. First, result shape: an AQL result is conceptually a two-dimensional table (rows of selected column expressions), with platforms returning an annotated result set — such as the one defined by the openEHR REST API — carrying column metadata [4]. Second, ordering is undefined by default: without ORDER BY, the specification guarantees nothing about result order, so any query feeding pagination or "latest value" logic must order explicitly [1]. Third, pagination is LIMIT row_count OFFSET offset combined with ORDER BY for deterministic pages — the older TOP modifier is deprecated. DISTINCT deduplicates rows before LIMIT/OFFSET apply. And parameters ($ehrUid, $systolicCriteria) should carry every runtime value: parameterization is built into the language precisely so statements can be shared and reused within and across systems.
AQL Clause Cheat Sheet
| Clause | Purpose | Key Tools | Watch Out For |
|---|---|---|---|
FROM |
Scope the data source | Class expressions, archetype predicates, CONTAINS (with AND/OR/NOT) |
Population query vs. single-EHR query: omitting the ehr_id predicate queries every record |
WHERE |
Filter on data values | Comparison operators, LIKE, matches, EXISTS, TERMINOLOGY() |
Quoting rules: strings and date/times quoted, numbers and booleans not |
SELECT |
Project the return data | Identified paths, whole-object variables, functions, literals, AS aliases, DISTINCT |
Selecting whole compositions when only two fields are needed inflates payloads |
ORDER BY |
Sort results | ASC/DESC, multiple sort expressions |
No default ordering exists — unordered pagination is nondeterministic |
LIMIT |
Paginate | LIMIT n OFFSET m with ORDER BY |
TOP is deprecated; don't mix it with LIMIT |
A Method for Writing AQL by Hand
The specification itself recommends a three-step method, which we've found matches how experienced modellers actually work [1]. Step 1 — FROM: identify the clinical concepts in the question ("blood pressure", "health encounter"), map each to its archetype, decide single-EHR versus population scope, and wire the containment hierarchy using the RM (compositions contain observations). Step 2 — WHERE: translate each criterion into an identified expression — declare a variable for the archetype you filter on, build the path to the data value, choose the operator, join criteria with logical operators. Step 3 — SELECT: write the identified paths for exactly the values to return, alias them readably, then add ORDER BY and LIMIT if the question implies "latest" or "top N". Working FROM-first keeps you honest: it forces the archetype and containment decisions — the semantic heart of the query — before any path detail.
Where CaboLabs Fits
CaboLabs doesn't just use AQL — we helped shape it: CaboLabs founder Pablo Pazos is a credited contributor to the official AQL specification, including the LIMIT/OFFSET pagination mechanism introduced in Release 1.1.0, and we've implemented AQL query engines from the ground up [1]. That specification-level and engine-level depth is what we bring to consulting engagements: AQL training for clinical and engineering teams, query design and review, archetype-path indexing strategies, and openEHR platform evaluation. Our openEHR-native clinical data repository, Atomik, puts it into production — standards-based archetype querying over a CDR built by people who know the language from the inside.
If your team is adopting AQL, migrating queries between platforms, or needs the query layer of a clinical data platform designed right, talk to us at cabolabs.com — few teams know AQL better, and we can prove it in the spec's own change log.
