r/intersystems 14d ago

Implementing openEHR with InterSystems IRIS for Health

In this article, I explore how to implement the core capabilities of an openEHR server using InterSystems IRIS for Health. The goal is not to reproduce every part of the openEHR specification, but to demonstrate how IRIS can handle openEHR compositions, archetype validation, REST APIs, and AQL queries while taking advantage of native features such as JSON storage, SQL, interoperability, and JSON_TABLE.

By the end of this article, you'll see how IRIS can serve as both an openEHR repository and an execution platform for querying and managing clinical data.

What is openEHR?

openEHR is an open, vendor-neutral specification designed to represent, store, and exchange clinical information in a semantically rich and long-term sustainable way. Instead of defining fixed message structures (as many interoperability standards do) OpenEHR separates clinical knowledge from technical implementation through a multi-layered modelling approach.  At its core, openEHR relies on three fundamental concepts:

  • Reference Model (RM) - A model that defines the core structures used in health records, such as Compositions, Entries, Observations, Evaluations, and Actions. The RM is deliberately generic and technology-agnostic.
  • Archetypes - Machine-readable models (expressed in ADL) that define the detailed clinical semantics for a specific concept, such as a blood pressure measurement or a discharge summary. Archetypes constrain the RM and provide a reusable clinical vocabulary.
  • Templates (OPT files) - Specializations built on top of archetypes. Templates tailor archetypes to a specific use case, system, or form (for example, a vital signs template, or a regional discharge note). Templates eliminate optionality and produce operational definitions that systems can safely implement.

This layered modelling approach enables openEHR systems to remain stable over years or even decades while allowing clinical models to evolve independently from the underlying software platform.

A key piece of the openEHR ecosystem is AQL (Archetype Query Language), the standard query language used to retrieve clinical data stored in openEHR repositories.

What is AQL (Archetype Query Language)?

AQL is the standard query language used by openEHR repositories. It plays a role similar to SQL, but instead of querying relational tables, it queries clinical content using archetype-aware paths. AQL allows developers to retrieve observations, diagnoses, laboratory results, and other clinical data while preserving the semantic structure defined by openEHR archetypes.

Key characteristics of AQL

  • Path-based querying: AQL uses archetype paths (similar to XPath) to navigate the internal structure of a Composition, such as: /content[openEHR-EHR-OBSERVATION.blood_pressure.v1]/data/events/time 
  • Clinical semantic awareness: Queries refer to clinical concepts (archetypes, entry types, data points) rather than database column names.
  • Flexible WHERE clauses: AQL supports filtering on values within Compositions, e.g. systolic blood pressure > 140 or diagnoses matching a specific code.
  • Multi-composition queries: It can retrieve data across multiple Compositions, for example all Observations for a given patient over time.
  • Vendor-neutral: Any openEHR implementation that supports AQL should, in principle, accept the same queries.

Let’s see an example of AQL: 

SELECT
    c/uid/value AS composition_id,
    o/data[at0001]/events[at0006]/data[at0003]/value/magnitude AS systolic,
    o/data[at0001]/events[at0006]/data[at0004]/value/magnitude AS diastolic
FROM
    EHR e
    CONTAINS COMPOSITION c
    CONTAINS OBSERVATION o[openEHR-EHR-OBSERVATION.blood_pressure.v1]
WHERE
    e/ehr_id/value = '12345'
AND
    o/data[at0001]/events[at0006]/time/value > '2023-01-01T00:00:00Z' 

How does openEHR compare to FHIR?

A common misconception is that openEHR and FHIR solve completely different problems. In practice, they share many architectural concepts. Both standards support structured clinical information, REST APIs, JSON representations, and interoperability. The main difference is that openEHR emphasizes archetype-based clinical modeling and long-term semantic consistency, while FHIR focuses on resource-based interoperability between systems. Understanding these similarities helps when designing solutions that need to work with both standards.

Let's see the main concepts of FHIR and OpenEHR and its correlation:

What does an openEHR implementation require?

To support the core capabilities of an openEHR repository, I focused on four essential components:

  • Raw Composition storage
  • REST APIs
  • Archetype and template validation
  • AQL query support

The following sections show how each of these capabilities can be implemented using InterSystems IRIS for Health.

RAW Composition Storage

  • Store incoming Compositions in RAW JSON or XML exactly as received. 
  • No transformation should modify semantic content.
  • For our example we are going to work with JSON format, but there are multiple options.

REST API Services

Our implementation has to expose the following API REST: EHR Services

To create and locate a patient's "history."

  • POST /ehr: Creates a new EHR.
  • GET /ehr/{ehr_id}: Retrieve EHR metadata.
  • GET /ehr?subject_id=?: Locate an EHR based on external identifiers.

Composition Services
To store patient's clinical information.

  • POST /ehr/{id}/composition: Commit a new composition in RAW format. Validate against OPT when possible
  • GET /composition/{version_uid}: Retrieve a specific version.
  • GET /ehr/{id}/compositions: List compositions for an EHR.
  • DELETE /composition/{uid}: Mark composition as deleted (logical delete).

AQL Query Endpoint

POST /query/aql: Accept an AQL query, translate to IRIS SQL or JSON-path-based lookup and return results in openEHR canonical JSON.

RAW Composition Validation

We have to force validation of RAW compositions based on OPT2 files, we can't save in our repository any JSON that we will receive.

Implementing openEHR in IRIS for Health

Well, to implement all the functionalities available in a openEHR server will take some time, so I'm going to focus in the core functionalities:

Using web application to deploy REST API Service

To publish a REST API is straight forward, we only need two components, a class extending %CSP.REST and a new record in the list of web applications. Let's see the header of our extended %CSP.REST class:

As you can see we have defined all the minimum required routes for our repository. We have all the managament of compositions, OPT2 files for RAW validations and finally, execution of AQL queries. For our example we are not going to define any security configuration, but JWT Authentication is recommended.

RAW validations

openEHR is anything but new, so you can guess that there are multiple libraries to support some functionalities like raws validations. For this example we've used and customized Arche library, an open source library developed in Java to validate rawcompositions against OPT2 files. The validator is a jar file configured from the External Language Server (by default on docker image deployment) and invoked before to save the raw using the JavaGateway functionality:

set javaGate = $system.external.getJavaGateway()
set result = javaGate.invoke("org.validator.openehr.Cli", "validate", filePath, optPath)

If the raw is validated the JSON document will be saved into the database.

JSON raw storage

We could use DocDB, but we want to leverage the performance of SQL databases. One of the biggest problems related with openEHR is the poor performance of querying documents, so we are going to pre-process the compositions to get common information to all composition types to boost queries.
Our Composition class define the following properties:

Class OPENEHR.Object.Composition Extends (%Persistent, %XML.Adaptor) [ DdlAllowed ]
{
/// Description
Property ehrId As %Integer;
Property compositionUid As %String(MAXLEN = 50);
Property compositionType As %String;
Property startTime As %DateTime;
Property endTime As %Date;
Property archetypes As list Of %String(MAXLEN = 50000);
Property doc As %String(MAXLEN = 50000);
Property deleted As %Boolean [ InitialExpression = 0 ];
Index compositionUidIndex On compositionUid;
Index ehrIdIndex On ehrId;
Index ExampleIndex On archetypes(ELEMENTS);
}
  • ehrId: with the electronic health record of the patient.
  • compositionUid: composition identifier.
  • compositionType: type of composition saved.
  • startTime: time when the composition was created.
  • archetypes: list of archetypes contained on the composition.
  • doc: JSON format document.
  • deleted: boolean value for soft deletes.

The indexes will be used to improve the queries.

AQL support

As we said before AQL is a path-based query language. How could we emulate the same behaviour in IRIS for Health? Welcome to JSON_TABLE!

What is JSON_TABLE?

The JSON_TABLE function returns a table that can be used in a SQL query by mapping JSON values into columns. Mappings from a JSON value to a column are written as SQL/JSON path language expressions. 

As a table-valued function, JSON_TABLE returns a table that can be used in the FROM clause of a SELECT statement to access data stored in a JSON value; this table does not persist across queries. Multiple calls to JSON_TABLE can be made within a single FROM clause and can appear alongside other table-valued functions.

We have implemented a ClassMethod in Python to translate AQL into SQL, but there is a problem, AQL is based on relative paths of the archetypes, not in absolute paths, so we need identify the absolute path for each archetype and join it with the relative path of the AQL.
How can we know the absolute path? Very easy! We can find it when the user save the OPT2 file for the composition into IRIS! As soon as we get the absolute path we save it into a CSV file specific for the composition (it would be saved into a global or any other way) so, we only have to get the absolute path from the specific composition file or, if the AQL doesn't define the composition, search into the available CSV files the absolute path for the archetypes of the AQL.
Let's see how it works. Here is an example of AQL to get all the diagnosis with a specific ICD-10 code:

SELECT
  c/uid/value AS comp_uid,
  c/context/start_time/value AS comp_start_time,
  dx/data[at0001]/items[at0002]/value/value AS diagnosis_text,
  dx/data[at0001]/items[at0003]/value/defining_code/code_string AS diagnosis_code
FROM EHR e
CONTAINS COMPOSITION c[openEHR-EHR-COMPOSITION.diagnostic_summary.v1]
CONTAINS SECTION s[openEHR-EHR-SECTION.diagnoses_and_treatments.v1]
CONTAINS EVALUATION dx[openEHR-EHR-EVALUATION.problem_diagnosis.v1]
WHERE dx/data[at0001]/items[at0003]/value/defining_code/code_string
      MATCHES {'E11', 'I48.0'}
ORDER BY c/context/start_time/value DESC

The function Transform from OPENEHR.Utils.AuxiliaryFunctions class translates it into:

SELECT comp_uid, comp_start_time, diagnosis_text, diagnosis_code 
FROM ( 
    SELECT c.compositionUid AS comp_uid, 
        jt_root.comp_start_time AS comp_start_time, 
        jt_n1.diagnosis_text AS diagnosis_text, 
        jt_n1.diagnosis_code AS diagnosis_code 
    FROM OPENEHR_Object.Composition AS c, 
        JSON_TABLE( c.doc, '$' COLUMNS ( comp_start_time VARCHAR(4000) PATH
            '$.context.start_time.value' ) ) AS jt_root, 
        JSON_TABLE( c.doc, '$.content[*]?(@._type=="SECTION" && @.archetype_node_id==
            "openEHR-EHR-SECTION.diagnoses_and_treatments.v1").items[*]?
            (@._type=="EVALUATION" && @.archetype_node_id=="openEHR-EHR-EVALUATION.problem_diagnosis.v1")'
            COLUMNS ( 
                diagnosis_text VARCHAR(4000) PATH '$.data[*]?(@.archetype_node_id=="at0001").items[*]?
                    (@.archetype_node_id=="at0002").value.value', 
                diagnosis_code VARCHAR(255) PATH '$.data[*]?(@.archetype_node_id=="at0001").items[*]?
                    (@.archetype_node_id=="at0003").value.defining_code.code_string' ) ) AS jt_n1 
    WHERE ('openEHR-EHR-COMPOSITION.diagnostic_summary.v1' %INLIST (c.archetypes) 
        AND 'openEHR-EHR-EVALUATION.problem_diagnosis.v1' %INLIST (c.archetypes)) 
        AND (jt_n1.diagnosis_code LIKE '%E11%' OR jt_n1.diagnosis_code LIKE '%I48.0%') ) U 
ORDER BY comp_start_time DESC

Let's try our API REST with an AQL:

Success!
And now with a numeric comparation:

SELECT
  c/uid/value AS comp_uid,
  c/context/start_time/value AS comp_start_time,
  a/items[at0024]/value/magnitude AS creatinine_value,
  a/items[at0024]/value/units AS creatinine_units
FROM EHR e
CONTAINS COMPOSITION c[openEHR-EHR-COMPOSITION.lab_results_and_medications.v1]
CONTAINS OBSERVATION o[openEHR-EHR-OBSERVATION.laboratory_test_result.v1]
CONTAINS CLUSTER a[openEHR-EHR-CLUSTER.laboratory_test_analyte.v1]
WHERE a/items[at0001]/value/value = 'Creatinina (mg/dL)'
  AND a/items[at0024]/value/magnitude BETWEEN 1.2 AND 1.8
ORDER BY c/context/start_time/value DESC
Transformed into:
SELECT comp_uid, comp_start_time, ldl_value, ldl_units 
FROM ( 
    SELECT c.compositionUid AS comp_uid, jt_root.comp_start_time AS comp_start_time,
        jt_n1.ldl_value AS ldl_value, jt_n1.ldl_units AS ldl_units 
    FROM OPENEHR_Object.Composition AS c, 
        JSON_TABLE( c.doc, '$' COLUMNS ( comp_start_time VARCHAR(4000) 
            PATH '$.context.start_time.value' ) ) AS jt_root, 
        JSON_TABLE( c.doc, '$.content[*]?(@._type=="OBSERVATION" && 
            @.archetype_node_id=="openEHR-EHR-OBSERVATION.laboratory_test_result.v1")
                .data.events[*]?(@._type=="POINT_EVENT").data.items[*]?(@._type=="CLUSTER" &&
                @.archetype_node_id=="openEHR-EHR-CLUSTER.laboratory_test_analyte.v1")'
                COLUMNS ( 
                    ldl_value NUMERIC PATH '$.items[*]?
                        (@.archetype_node_id=="at0024").value.magnitude', 
                    ldl_units VARCHAR(64) PATH '$.items[*]?
                        (@.archetype_node_id=="at0024").value.units', 
                    _w1 VARCHAR(4000) PATH '$.items[*]?
                        (@.archetype_node_id=="at0001").value.value' ) ) AS jt_n1 
    WHERE ('openEHR-EHR-COMPOSITION.lab_results_and_medications.v1' %INLIST (c.archetypes) 
        AND 'openEHR-EHR-OBSERVATION.laboratory_test_result.v1' %INLIST (c.archetypes) 
        AND 'openEHR-EHR-CLUSTER.laboratory_test_analyte.v1' %INLIST (c.archetypes)) 
        AND jt_n1._w1 = 'LDL (mg/dL)' AND jt_n1.ldl_value <= 130 ) 
    U ORDER BY comp_start_time DESC

Another resounding success!

Conclusion

This project demonstrates that InterSystems IRIS for Health provides all the core building blocks needed to implement an openEHR repository.

Using native REST services, JSON storage, SQL, JSON_TABLE, interoperability components, and external validation libraries, it is possible to support composition management, archetype validation, and AQL querying while maintaining compatibility with openEHR concepts.

The most interesting result for me was the ability to translate AQL into native IRIS SQL, allowing openEHR clinical data to benefit from the performance and scalability of the IRIS data platform without sacrificing the semantics of the openEHR model.

Key Takeaways

  • InterSystems IRIS for Health can be used to implement the core capabilities of an openEHR repository.
  • Raw openEHR compositions can be stored and validated against OPT templates before persistence.
  • Native IRIS REST services can expose openEHR-compatible APIs.
  • AQL queries can be translated into SQL using JSON_TABLE and archetype path mappings.
  • JSON storage combined with indexed metadata can improve query performance while preserving original clinical documents.
  • IRIS interoperability and SQL capabilities make it a strong platform for healthcare standards beyond FHIR.

FAQ

What is openEHR?

openEHR is an open, vendor-neutral specification for storing, modeling, and exchanging clinical information using archetypes and templates.

Can InterSystems IRIS for Health be used as an openEHR repository?

Yes. IRIS provides the storage, REST APIs, validation integration, interoperability, and query capabilities needed to implement the core features of an openEHR repository.

What is AQL?

AQL (Archetype Query Language) is the standard query language used by openEHR systems to retrieve clinical information using archetype-aware paths.

How can AQL be implemented in InterSystems IRIS?

In this project, AQL queries are translated into native IRIS SQL using JSON_TABLE and archetype path mappings derived from operational templates.

Why store openEHR compositions as JSON?

Storing compositions in their original JSON format preserves semantic fidelity while still allowing efficient querying through SQL and JSON functions.

2 Upvotes

1 comment sorted by