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:

  • rows is the only required member. meta, name, q and columns are 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.

  • name exists. 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 meta object 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. ResultSetMetadata is open and openEHR may add to it. A future meta.timeout or meta.complete defined 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 named federation cannot 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 meta are never _-prefixed, and they live under federation. Two separate rules, both normative:

  1. The underscore prefix marks the fields openEHR itself defines (href, _type, _schema_version, _created, _generator, _executed_aql) and is reserved to openEHR. A gateway MUST NOT emit _complete, _endpoints or _federation, and a client MUST NOT expect them. A gateway MAY emit the standard -prefixed fields; this specification neither requires nor forbids them.

  2. The federation’s own additions MUST be nested under the unprefixed meta.federation object. A gateway MUST NOT emit complete, endpoints, timeout or dedup as direct members of meta, and a client MUST NOT read them there.

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

columns[] MUST be rendered by the gateway from the client’s submitted AQL, and is independent of any column paths a node reports. A gateway MUST NOT pass a node’s rendering through (N17, CP-35).

Real CDRs disagree on the same query: for FROM … c CONTAINS COMPOSITION, one product reports c/context/start_time/value, retaining the FROM alias, and another reports /context/start_time/value, stripping it. In a single-CDR deployment the question never arises, because there is one answer. In a fan-out there are N candidate answers for a field the envelope treats as singular, so a gateway echoing whichever node replied first produces output that depends on response scheduling. A client then cannot match ORDER BY terms or projections against a path whose shape depends on which node was fastest.

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

endpoint_id

Stable registry identifier of the Endpoint (N19).

organisation / organization_id

The managing Organization (N20).

system_id

The node’s openEHR logical EHR-system id - the follow-up routing key (§12, N21).

url

The CDR base URL (from the registry; MAY be omitted from rows and left in meta).

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

id

MUST

The endpoint_id - the stable registry identifier of the endpoint that was addressed (§ The four identifiers).

status

MUST

One of the statuses in §11.1.

node_id

SHOULD

The owning node (§ The four identifiers). Distinguishes two endpoints of the same node.

system_id

SHOULD

The node’s openEHR system_id, where known (§ The four identifiers).

organisation

SHOULD

The managing Organization (N20).

error

MUST, when status is offline, time-out, node-error or not-resolved

The node-reported or gateway-reported error (§11.2). For node-error it carries the node’s own failure, at minimum its HTTP status. Not required for consent-denied, which a Step-1 pre-filter can set with no node error to carry.

latency_ms

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 time-out endpoint this is the elapsed time at abandonment (§11.5). The value is the gateway’s own measurement; a node’s self-report is not used.

The obligation follows dispatch, not scope. It is required for active, offline, time-out and node-error, the four statuses that describe an attempted request. It is not required for the statuses settled before any request exists to time: excluded and not-localized, where the node was never in scope (§11.1), and not-resolved or a pre-filtered consent-denied, both settled during Step-1 resolution (§5, §13.2.1). A gateway MUST omit the field in those cases; a 0 would read as "answered instantly". (N40.)

product / version

SHOULD

The node’s product name and version, as the gateway knows them - from the registry, from the node’s own capability/OPTIONS response, or from cached discovery. Absent when unknown; a gateway MUST NOT invent them. (N40.)

row_count

SHOULD

Rows this endpoint contributed before federation-level DISTINCT, dedup (§10) and LIMIT (§11.6). Makes suppression auditable.

url

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.