9. Result-set format & origin metadata
9.1 Relationship to openEHR ITS-REST
A federated AQL response is an openEHR RESULT_SET, as defined by the openEHR Query API in ITS-REST Release-1.1.0 (19 July 2026), and not a federation-specific structure that resembles one. This specification does not define the envelope; it inherits it and adds to it.
The transparency claim of N1 holds on the wire for this reason: a client that can parse the AQL response of a single CDR can parse a federated one, because it is the same structure. Everything federation-specific lives in an extension point openEHR left open for the purpose.
What is inherited
ITS-REST defines five top-level members of RESULT_SET (meta, name, q, columns and rows) with their types and obligation levels. This specification does not re-state them; the normative list is the one in ITS-REST Release-1.1.0 (computable/OAS/query-validation.openapi.yaml, schema ResultSet). Two consequences follow that a reader of this document alone would not know:
-
rowsis the only required member.meta,name,qandcolumnsare all optional in ITS-REST. Where this specification requires one of them, that is a federation constraint tightening the standard, not a restatement of it - see N17. -
nameexists. It carries the stored-query name in[{namespace}::]{query-name}form and means something only for stored queries. A gateway exposing{base}/v1/definition/query/…(§7a.1) SHOULD emit it when answering a stored query, exactly as a single CDR would, and MUST emit it where it offers the stored-query registry of §12.7, where it names the gateway’s definition and never a node’s (§12.7, N44). Earlier drafts enumerated four members and left it out; citing ITS-REST by reference stops that recurring.
rows entries are ordered arrays of values, not objects. ITS-REST defines ResultSetRow as a JSON array whose i-th element corresponds to columns[i]. This rule has the largest consequences of any inherited here: the two serialisations cannot be converted into each other without the column list, and a gateway that emits objects is not returning a RESULT_SET.
What this specification adds
meta is an ITS-REST ResultSetMetadata. That schema declares additionalProperties: true, so it is an open object by design, and carrying additional keys in it is conformant and does not deviate from N1. This specification uses that extension point once, for a single additional member named federation, and puts everything it contributes inside it.
The federation object carries four members: complete (§11.4), endpoints[] (§9.5), timeout (§11.5) and dedup (§10). They are addressed as meta.federation.complete, meta.federation.endpoints[], and so on.
Why they are grouped. Earlier drafts placed all four directly on meta, beside openEHR’s own members. Nesting them buys three things:
-
One namespace, one owner. A reader of a
metaobject can tell at a glance which members this specification is responsible for: exactly one, and everything reachable through it. A flat layout means knowing the list by heart. -
No collision with a future openEHR member.
ResultSetMetadatais open and openEHR may add to it. A futuremeta.timeoutormeta.completedefined by openEHR would silently collide with a federation key of the same name, with no way to tell which document a given value answered to. One member namedfederationcannot collide with an openEHR member unless openEHR picks that name for a federation concept, in which case the collision is a real one worth having. -
Self-describing on the wire.
"federation": { … }says what the extension is without reference to this document.
|
Federation additions to
|
Which document governs
This specification is normative for its own additions, the meta.federation object and its members above, and for the federation-specific constraints it places on inherited members: the columns[] provenance rule (columns[] is the gateway’s rendering, never a node’s), single-CDR row-shape compatibility and the ENDPOINT attribute set (N17, N18). It does not redefine RESULT_SET.
Where the two documents could be read differently, ITS-REST governs the inherited members and this specification governs its own additions.
Both halves are published as JSON Schema alongside this specification: federated-result-set.schema.json for this envelope, and options-root.schema.json for the OPTIONS body of §7a.2.
9.2 Envelope
The result set MUST be an ITS-REST RESULT_SET (§9.1) carrying q, columns, rows and meta (N17). Endpoint columns appear only when ENDPOINT attributes are selected; otherwise the row shape is identical to running the same query directly on a single CDR (single-CDR compatibility, N17). ENDPOINT attribute aliases resolve any name collision with EHR-derived columns (N18).
The example in §9.4 and the field table in §9.5 are jointly normative for the federation additions, the meta.federation members this specification contributes, and for the federation-specific constraints of §9.1. The table fixes the fields and their obligation level; the example fixes their arrangement. Where the two could be read differently, the table governs. They are not the contract for the inherited members of RESULT_SET; ITS-REST is (§9.1). Both are now also expressed as a JSON Schema, validated against the example in CI.
columns[] is the gateway’s rendering, never a node’s
Real CDRs disagree on the same query: for |
9.3 ENDPOINT attribute set
The selectable ENDPOINT attributes are the endpoint/organization provenance fields plus the openEHR-native routing key system_id:
| Attribute | Meaning |
|---|---|
|
Stable registry identifier of the Endpoint (N19). |
|
The managing Organization (N20). |
|
The node’s openEHR logical EHR-system id - the follow-up routing key (§12, N21). |
|
The CDR base URL (from the registry; MAY be omitted from rows and left in |
Including system_id in the row-level attribute set puts the openEHR-native routing key into the result, so the Application tier can route a follow-up without a second registry lookup (§12).
9.4 Example
{
"q": "SELECT p/id AS endpoint_id, p/system_id AS system_id, c/uid/value AS composition_id FROM ENDPOINT p [\"node_1\",\"node_2\"] CONTAINS EHR e CONTAINS COMPOSITION c WHERE e/ehr_status/subject/external_ref/id/value = '12345'",
"columns": [
{ "name": "endpoint_id", "path": "p/id" },
{ "name": "system_id", "path": "p/system_id" },
{ "name": "composition_id", "path": "c/uid/value" }
],
"rows": [
["node_1", "cdr1.rso.nl", "8849182a-1d4b-4e3d-a3f3-f303d2f4f34b::cdr1.rso.nl::1"],
["node_2", "cdr2.rso.nl", "6ba7b810-9dad-11d1-80b4-00c04fd430c8::cdr2.rso.nl::1"]
],
"meta": {
"federation": {
"complete": true,
"endpoints": [
{ "id": "node_1", "node_id": "node_1", "system_id": "cdr1.rso.nl", "organisation": "Org A",
"url": "https://ehr.hospital-a.example/openehr/v1", "status": "active",
"latency_ms": 118, "product": "VendorX CDR", "version": "3.4.1", "row_count": 1 },
{ "id": "node_2", "node_id": "node_2", "system_id": "cdr2.rso.nl", "organisation": "Org B",
"url": "https://ehr.clinic-b.example/openehr/v1", "status": "active",
"latency_ms": 306, "product": "VendorY", "version": "2026.1", "row_count": 1 },
{ "id": "node_3", "node_id": "node_3", "organisation": "Org C",
"status": "excluded" }
]
}
}
}
Rows are arrays, and the correspondence to columns[] is positional: rows[n][i] is the value of columns[i], per the ITS-REST ResultSetRow definition (§9.1). A row carries no field names of its own, and columns[] is the only thing that names them. Note that columns[] entries carry name and path and nothing else. ITS-REST defines no type member, and a gateway that needs to convey a type does so in the row value itself, in openEHR’s own {"_type": "DV_TEXT", …} form.
Positional rows make the columns[] provenance rule of columns[] is the gateway’s rendering, never a node’s more load-bearing. A client matching an ORDER BY term or a projection against the result now depends on column order as well as path shape. If two nodes disagree on a path, the gateway’s single rendering makes position i mean the same thing in every row; a gateway echoing a node’s rendering would make the meaning of a row position depend on which node replied first.
Each composition_id is an OBJECT_VERSION_ID whose middle segment is the creating_system_id (cdr1.rso.nl, cdr2.rso.nl), which §12 routes on.
node_3 shows a reported-but-not-queried member. This query was directed at node_1 and node_2, so the directive excluded it, and the gateway reports it as §11.1 says it SHOULD (§11.1). Had the query been undirected and localization simply not named it, its status would be not-localized instead. Neither clears meta.federation.complete, which is true here because every node that was in scope answered.
Note also that the four federation members sit inside a single federation object and not directly on meta (§9.1). A gateway emitting meta.complete is not conformant.
9.5 meta.federation.endpoints[] fields
meta.federation.endpoints[] is the per-node record of what happened, and it is the normative carrier for coverage information (§11.1). Every in-scope node appears, whether or not it contributed rows.
| Field | Meaning | |
|---|---|---|
|
MUST |
The |
|
MUST |
One of the statuses in §11.1. |
|
SHOULD |
The owning node (§ The four identifiers). Distinguishes two endpoints of the same node. |
|
SHOULD |
The node’s openEHR |
|
SHOULD |
The managing Organization (N20). |
|
MUST, when |
The node-reported or gateway-reported error (§11.2). For |
MUST, when a query was dispatched to the endpoint |
Wall-clock time the gateway observed for its request to this endpoint, in milliseconds. For a The obligation follows dispatch, not scope. It is required for |
|
SHOULD |
The node’s product name and version, as the gateway knows them - from the registry, from the node’s own capability/ |
|
|
SHOULD |
Rows this endpoint contributed before federation-level |
|
MAY |
The CDR base URL. MAY be omitted from rows and left here. |
meta.federation itself MUST additionally carry complete (§11.4) and SHOULD carry the effective timeout budget in force for the request (§11.5).
Why latency and version belong here: a federated query is only as good as its slowest and its oldest member, and an operator debugging "why is this slow" or "why does node 3 not support this AQL feature" would otherwise have to correlate gateway logs by hand. The same fields appear in OPTIONS {base}/ (§7a.2), with product and version as the same two separate fields and latency as an aggregate over recent requests; there it is not a single request’s measurement.
9.6 Responses with no envelope: headers
An AQL response carries meta. A write or a routed ITS-REST read does not, since a 201 Created has only headers. On those requests the provenance that meta.federation.endpoints[] carries for a query travels in the openEHR-federation-endpoint / openEHR-federation-system-id response headers instead. See §7a.3 and N31.