Architect ← Insights

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 FROM clause to scope the data source: [openEHR-EHR-OBSERVATION.blood_pressure.v1]. It is formally equivalent to a standard predicate on archetype_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.

References & Verifiable Sources

  1. openEHR Foundation: Archetype Query Language (AQL) Specification (The official STABLE specification on which this guide is based: query structure, path syntax, predicates, operators, functions, LIMIT/OFFSET pagination, the deprecation of TOP, and the amendment record crediting contributors including CaboLabs' Pablo Pazos).
  2. openEHR Foundation: AQL Examples (Official companion document with worked queries, including name-based node predicates, specialized at-codes, terminology matching, and arithmetic examples).
  3. openEHR Foundation: openEHR Architecture Overview — Paths and Locators (Official definition of the openEHR path syntax and predicate expressions that AQL builds on; supports the paths and predicates sections).
  4. openEHR Foundation: openEHR REST API — Query (Official REST API for executing ad-hoc and stored AQL queries, defining the annotated result set structure and parameter passing; supports the results and execution notes).

Do you have any questions?

Let us know how we can help you.

Company CaboLabs Health Informatics
Address Juan Paullier 995, Montevideo, Uruguay
Phone +598 99 043 145