Querying Clinical Data with AQL: Paths, Predicates, and Production Practices
Archetype Query Language (AQL) is the openEHR standard for querying clinical data in a CDR. Its syntax is officially described as a synthesis of SQL structural syntax and openEHR path syntax: familiar SELECT / FROM / WHERE / ORDER BY clauses, combined with path expressions that navigate the archetype and composition hierarchy, and a CONTAINS operator that expresses containment between reference model structures [1]. The crucial property — and the reason AQL exists at all — is that queries are written against clinical models, not physical schemas: the archetype identifier in brackets scopes the query to a clinical concept regardless of which template, form, or application stored the data.
A complete example, following the pattern from the specification — all hypertensive blood pressure readings in an 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
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
Reading AQL Paths: Where the at-Codes Come From
AQL paths are derived directly from the archetype structure, and the specification distinguishes two kinds: archetype paths, which refer to nodes within an archetype, and reference model attribute paths, which point at RM attributes like c/context/start_time or e/ehr_id/value [1]. The segments in brackets — at0001, at0004 — are at-codes: local node identifiers defined in the archetype's terminology section, each bound to a human-readable name. In the blood pressure archetype, items[at0004] is the systolic node and items[at0005] the diastolic node; the path /data[at0001]/events[at0006]/data[at0003]/items[at0004]/value/magnitude reads as "the magnitude of the systolic element, within any event, within the observation's data."
Nobody should construct these by hand from memory. The reliable workflow is to inspect the archetype's ADL definition or — better — navigate the node tree in a visual modelling tool and copy the generated path; raw reference-model paths are difficult to handle unaided, and at-codes are archetype facts, not guessable strings. Two practical details that save debugging time: name-based predicates can disambiguate repeated siblings (e.g., items[at0011, 'Fractions'] in the specification's lab examples), and specialized archetypes introduce dotted at-codes (at0013.1) whose meaning only makes sense against the specialization hierarchy — another reason the archetype, not the query, is the source of truth. For a full walkthrough of paths, predicates, and query construction, see the CaboLabs AQL Guide [5].
Query on Archetypes, Not Templates
When the same clinical concept can be recorded through multiple templates — and in any mature CDR, it will be — prefer querying on archetype identifiers rather than template-specific structures. The archetype predicate (OBSERVATION o[openEHR-EHR-OBSERVATION.blood_pressure.v1]) matches every composition containing that observation regardless of the template used to commit it, so the query keeps returning correct results as new templates are deployed and old ones evolve. This is the query-side dividend of two-level modelling: your analytics are coupled to the governed clinical model, not to the faster-changing form layer. Template-scoped queries have their place — when you genuinely want "data captured through this document" — but they should be the deliberate exception, not the reflex.
Executing AQL in Production: The REST API
The openEHR REST API standardizes query execution across conformant platforms [3]. Ad-hoc queries go to the query endpoint — with POST recommended over GET, since long AQL strings and parameter lists quickly exceed URI length limits:
POST [base]/query/aql HTTP/1.1
Content-Type: application/json
{
"q": "SELECT ... FROM EHR e[ehr_id/value=$ehrUid] CONTAINS OBSERVATION o[openEHR-EHR-OBSERVATION.blood_pressure.v1] WHERE o/.../value/magnitude >= $systolicThreshold",
"query_parameters": {
"ehrUid": "554f896d-faca-4513-bddf-66454114630d",
"systolicThreshold": 140
}
}
Three API capabilities deserve deliberate use rather than incidental discovery:
- Parameter binding: AQL supports parameterization by design (
$ehrUid,$systolicThreshold), with values supplied at execution viaquery_parameters. Use it always — string-concatenating values into AQL is the same anti-pattern as concatenating SQL, with the same injection and caching consequences. - Stored (named) queries: the REST API's definitions endpoints manage registered queries that clients execute by qualified name and version [4]. In our experience this is the single most effective cure for AQL strings scattered and duplicated across application code: queries become versioned, governed server-side artifacts, and applications hold references, not copies.
- Result control: scope queries to a single EHR wherever the use case allows (via
ehr_idparameter or header) rather than running population queries by default, order results explicitly, and paginate large result sets rather than fetching unbounded lists.
AQL Query Hygiene at a Glance
| Practice | Instead Of | Why It Matters |
|---|---|---|
| Archetype-based predicates | Template-coupled queries | Queries stay valid across template changes and new form deployments |
| Paths copied from modelling tools | Hand-typed at-code paths | at-codes and specialization codes are archetype facts, not guessable strings |
| Parameter binding ($params) | String-concatenated values | Safety, reuse, and shareable statements — parameterization is built into the language |
| Stored named queries | AQL strings in application code | Versioned, governed, server-side query definitions with a stable contract |
| EHR-scoped execution + pagination | Unbounded population queries | Predictable performance; population queries become a deliberate, monitored choice |
Performance ultimately depends on how the underlying CDR structures and indexes compositions — AQL defines the semantics, not the execution strategy — so the same query can behave very differently across platforms. openEHR CDRs such as EHRbase, Better, and our own Atomik each implement AQL execution over their own persistence designs; in our experience, testing your heaviest queries against realistic data volumes on your actual platform belongs in evaluation and pre-production, not after the first slow dashboard.
Where CaboLabs Fits
AQL is where an openEHR investment starts paying analytical dividends — if the queries are correct, maintainable, and fast. CaboLabs has deep, implementation-level AQL expertise: we've built AQL query engines, designed archetype-path indexing strategies for relational persistence, and helped teams turn ad-hoc query sprawl into governed, stored-query catalogs. Our openEHR-native clinical data repository, Atomik, puts that experience into production: archetype-based querying over a standards-based CDR, with your clinical models — not your database schema — as the query surface.
If your team is learning AQL, your queries are outgrowing your CDR's performance, or you're evaluating openEHR platforms and want to understand what's really behind each AQL endpoint, talk to us at cabolabs.com — we speak AQL at the engine level.
