Changes from the 2025-08-20 proposal

This note accompanies Proposal for Federation Tier with AQL - revised and records what changed relative to the original openEHR International Federation Working Group proposal "Proposal for Federation Tier with AQL" (2025-08-20). The revised specification stands on its own and does not narrate this history; this note does.

Headline (breaking) changes

# Change Where in the revised spec Rationale

C1

Federation is keyed on the per-node local ehr_id, not on EHR_STATUS.subject. The gateway resolves subject → ehr_id outside AQL and dispatches standard, non-federated AQL per node.

§4, §5, §6 (N3, N7), §7

MPI advisory §3 (storing an identifier in EHR_STATUS.subject is undesirable); RSO "Record identification".

C2

The federated example that filtered WHERE …/ehr_status/subject/external_ref/id/value at the node level is removed. subject now appears only as façade input, rewritten to ehr_id before dispatch.

§7 (before→after)

Internal consistency: the old example keyed federation on subject.

C3

The gateway is defined as a transparent façade - a client can query and follow up without knowing it is federated; the API is a conformant openEHR Query API.

§1, §3.2, N1

HL7 Intermediaries White Paper (transparency).

C4

Storing a directly identifying patient identifier in EHR_STATUS.subject is no longer part of the federation model (storing the BSN there was the RSO interim approach). Identity is resolved in an MPI/demographic layer; no such identifier need be stored in the CDR.

§5, N5

MPI advisory §3; RSO’s own "Hide"/external-ref migration note.

C5

Identity / localization / addressing are bound to IHE profiles (PIXm, PDQm, XCPD, PMIR, mCSD) as the proposed binding, with the Dutch Generic Functions as a regional alternative in an informative annex.

§5, §14, §15, Annex A, Annex B

Consolidated design decision; MPI advisory §4.

C6

Follow-up read and write routing is specified, provenance-honest: rows carry an endpoint identifier and routing uses the creating_system_id inside the OBJECT_VERSION_ID; openEHR uids are never rewritten.

§9, §12 (N21–N24)

Intermediaries White Paper (routing follow-ups); Exchange-Routing IG (self-routing base URL).

C7

Four previously-absent areas are added: authN/authZ handoff (§13), localization vs record-locator split (§14) and addressing (§15), follow-up routing (§12), and a Connectathon-style test guide (§16–§17).

§12–§17

Gaps flagged in the change brief.

C8

The federation surface is the whole ITS-REST API, read and write - not AQL alone - with the unsupported parts named (DEMOGRAPHIC; Definition management is single-node in v1 - stored queries have since gained an optional gateway registry, The SEC review amendments (0.9.x, no version bump)), the vendor /rest/openehr prefix removed in favour of a deployment-chosen base URL, both EHR addressing forms supported, and OPTIONS {base}/ added so a gateway describes itself and the endpoints behind it.

§7a (N28–N32), N43

Review meeting: transparency has to cover reads and writes, and the prefix is vendor-specific.

C9

Identifier hygiene is specified beyond subject. In the query the gateway composes, a node is located by the ehr_id and nothing else identifying. Predicates and projections over PARTY_IDENTIFIED/DV_IDENTIFIER, PARTY_RELATED and ENTRY-level subject, plus the path, query string and headers, are all in scope, and a query that cannot be brought into that state is rejected. Write payloads are explicitly out of scope: a commit body is archetyped clinical content and passes through unmodified. The node’s obligation is restated as an interface one (be invocable on ehr_id, and have some way to exchange a patient identifier for it) with no mandate on what a node stores.

§5.4, §5.5 (N33, N34)

Review meeting: the subject rewrite alone still leaks; and the spec should not legislate node-internal storage.

C10

Completeness, timeouts and cross-node result shaping are made normative. Best-effort is the declared default with a meta.complete flag (both since superseded - see the SEC amendments); per-node and overall timeout budgets are required and discoverable; ORDER BY + LIMIT is re-applied at the Tier to give the true global top n; OFFSET must be rejected or computed correctly; undirected aggregates must be refused with a reason, never answered with per-node rows.

§11.4–§11.6 (N37–N39)

Review meeting: timeouts and the limit/order/aggregate interaction were left to implementers and are not safely inferable.

C11

The four identifiers are pinned down. node_id, endpoint_id, system_id and creating_system_id are defined in the reader’s guide (§ The four identifiers), with which-one-routes-what made normative in §12a. The consequences are worked through: dedup must not send a write to a node holding an imported copy, an ehr_id claimed by two nodes is a 409 and an integrity incident, never a guess, and node-admission conditions (UUID ehr_id s, no reuse, no foreign-ehr_id adoption, unique system_id) prevent the collision in the first place. Admission is governance, so those conditions live in their own section, §12b Federation membership, and not inside the routing rules that depend on them.

§ Terminology, §12a, §12b, §10.3, §12.5 (N36, N41, N42, N42a)

Review meeting: these four were being conflated, and the failure mode is a mis-routed write.

C12

Endpoint targeting is supported both in and outside the AQL. The gateway must accept FROM ENDPOINT and the openEHR-federation-endpoint request header, a header and never a query parameter. A client needs only one. Conflicting node sets in one request are rejected. Provenance travels on write responses as headers, and meta.endpoints[] gains latency_ms, product/version and row_count. (The key was later nested as meta.federation.endpoints[]; see the SEC amendments.)

§8.4 (N35), §9.5 (N40, N31)

Review meeting: stored and user-authored AQL should not have to be rewritten to retarget it; and writes have no meta to carry provenance.

Requirement mapping (old numbers → new N-numbers)

The 2025-08-20 proposal had 14 numbered requirements. The revised spec renumbers them as N1–N27 and adds N28–N43 (all new, from the review meeting - see C8–C12 above; none maps to an old requirement). The table below maps old→new and marks the change.

New Change Old Note

N1

new

-

Transparent façade: no indication it is federated.

N2

new

-

Accept subject as façade input; never execute it at a node.

N3

new

-

Resolve subject → {node, ehr_id, consent} outside AQL.

N4

new

-

Undirected queries get their candidate nodes from a localization service (candidates only - not consent).

N5

new

-

No directly identifying identifier required in EHR_STATUS.subject; a returned subject column is re-injected input.

N6

new

-

not-resolved / consent-denied per node recorded, not fatal.

N7

changed

#1

Dispatch standard AQL keyed on ehr_id, not subject (the core shift).

N8

changed (SHOULD→MUST)

#3

Dispatch only to nodes where the patient is resolved (plus any optional consent pre-filter, N27a).

N9

kept

#13

WHERE/LIMIT/OFFSET execute within each node.

N10

changed

#6

Accept an undirected query identifying only the patient.

N11

changed (merge)

#7 + #8

Accept a directed query naming Endpoints (SHOULD: Organizations); directive is independent of patient resolution.

N12

kept

#2

Add Endpoint/Organization attributes to rows when selected.

N13

changed (SHOULD→MUST)

#14

DISTINCT and ORDER BY supported at the Tier.

N14

changed

#12

Block undirected aggregates unless cross-node-correct; directed single-node aggregate allowed.

N15

new

-

Default no dedup + provenance; opt-in object_id VERSION-identity dedup.

N16

kept (extended)

(Unresponsive)

Unresponsive nodes in meta.endpoints[] (later nested as meta.federation.endpoints[], The SEC review amendments (0.9.x, no version bump)); status set extended with not-resolved/consent-denied/excluded.

N17

kept

(result format)

{q, columns, rows} + meta; no endpoint columns unless selected.

N18

kept

(aliasing)

ENDPOINT aliases resolve collisions with EHR columns.

N19

changed (merge)

#9 + #11

FHIR Endpoint/Organization RECOMMENDED; define a real connectionType instead of the informal 'open-ehr-query-API' string.

N20

kept

#10

Every Endpoint has exactly one managing Organization (1..1).

N21

changed

#4

Registry also maps system_id/creating_system_id → CDR base URLs (the follow-up routing table).

N22

new

-

Follow-up reads route to the owning CDR by creating_system_id; uids never mutated.

N23

new

-

Versioned writes route to the single controlling CDR; new objects target one chosen node.

N24

new

-

Convey authenticated client identity on every routed follow-up.

N25

new

-

Client authenticates to the Tier; the Tier propagates identity onward (OAuth 2.0 client-credentials with an RFC 7523 signed JWT client assertion).

N26

new

-

Delegate access decisions needing gateway-invisible information to the source CDR.

N27

new

-

Consent enforcement is the node’s obligation, performed regardless of upstream filtering.

N27a

new

-

Step-1 consent pre-filtering is OPTIONAL, where a deployment has a consent service.

N28

new

-

ITS-REST at a deployment-chosen base URL; no mandated prefix (/rest/openehr removed).

N29

new

-

Canonical path form and AQL predicate form both supported.

N30

new

-

OPTIONS {base}/ describes the surface and lists the endpoints behind the gateway.

N31

new

-

Acting-endpoint response headers on routed requests; Location/ETag untouched.

N32

new

-

DEMOGRAPHIC not federated; unsupported areas declared; identifier namespaces distinct.

N33

new

-

In the composed query, a node is located by ehr_id alone; other identifier carriers stripped or the query rejected. Write bodies pass through untouched.

N34

new

-

Node invocable on ehr_id; environment supplies the cross-reference; no storage mandate.

N35

new

-

Targeting supported both in AQL and via the openEHR-federation-endpoint header (not a query parameter); conflicts rejected.

N36

new

-

Post-dedup writes route to the originator; never to a holder of a copy.

N37

new

-

Best-effort default with meta.complete; all-or-nothing opt-in only. Both since reversed - see The SEC review amendments (0.9.x, no version bump).

N38

new

-

Per-node and overall timeout budgets, discoverable and reported.

N39

new

-

Correct cross-node ORDER BY/LIMIT; OFFSET rejected or correct; aggregates refused with a reason.

N40

new

-

meta.endpoints[] (now meta.federation.endpoints[], The SEC review amendments (0.9.x, no version bump)) carries latency, and should carry product/version and row counts.

N41

new

-

Path ehr_id routing order; no ask-all probing for writes.

N42

new

-

ehr_id collisions → 409 + integrity incident (gateway, at request time).

N42a

new

-

Identifier-integrity conditions a node must satisfy to be admitted (federation operator, at admission; §12b).

N43

new

-

Definitions single-node-routed; optional, explicit, non-atomic template broadcast. Now the template rule and the no-registry fallback - see The SEC review amendments (0.9.x, no version bump).

N44

new

-

Optional gateway-held stored-query registry: authoritative, immutably versioned, expanded into a fan-out when invoked by name. Added by The SEC review amendments (0.9.x, no version bump).

Dropped / folded: old #5 (standalone Record Locator Service requirement) is split into localization (N4), addressing (N21/§15) and cross-reference (N3). The old federated WHERE …/subject example is replaced by the §7 before→after rewrite.

0.4.0: alignment with openEHR ITS-REST, and published schemas

Answering the question "is the federated AQL response not standard openEHR?" surfaced a gap no earlier review had noticed: most of the result envelope was already normatively defined by openEHR ITS-REST, and this specification never said so. §9 asserted a {q, columns, rows} + meta envelope as though it were new, and 0.3.1 went further by declaring its own example and table "jointly normative" for the whole structure, which this specification has no authority to do.

Each finding below has the same remedy: cite the standard that already exists, and scope this specification’s contract to what it actually adds. The bound release is ITS-REST Release-1.1.0 (19 July 2026), pinned by name so that the citation fixes one wire format; a latest pointer would not.

# Finding Where in the spec Disposition

F1

meta extension is conformant. ITS-REST declares ResultSetMetadata with additionalProperties: true, so complete, endpoints[], timeout and dedup are legitimate extensions of a standard open object (all four are now nested inside one federation member for the reasons in The SEC review amendments (0.9.x, no version bump); the conformance argument is unchanged) - no tension with N1. The specification never said this, so an implementer could not tell.

§9.1

Stated. The reassuring finding, and the one most worth writing down.

F2

name was missing. ITS-REST has five top-level members; N17 mandated four and never mentioned name. Minor - it is optional and meaningful only for stored queries - but §7a.1 lists {base}/v1/definition/query/… as an exposed area, so stored queries are in scope, and a gateway built strictly to N17 omitted a standard member.

N17, §9.1

Adopted by reference. N17 now cites the ITS-REST member list in place of its own enumeration, so the list cannot drift from ITS-REST again.

F3

The §9 example contradicted ITS-REST on rows. The example serialised rows as objects keyed by column name, while ITS-REST defines ResultSetRow as an ordered array of values. Because 0.3.1 had declared that example normative, this was a wire-format divergence: two vendors following the two documents would have serialised rows differently.

§9.4, N17, CP-35

Resolved in favour of ITS-REST; the example was wrong and is corrected. The columns[]/row correspondence is now positional, which makes the §9.2 provenance rule more load-bearing, since a client matching ORDER BY terms now depends on column order as well as path. Both reference implementations emitted objects and are corrected to match.

F4

The underscore convention was unstated. Standard meta fields are _-prefixed and the federation’s additions are not. That is correct - the prefix marks openEHR-defined fields - but the specification never said so, and an implementer had no way to know _complete was not intended.

§9.1, N17

Stated normatively. The _ prefix is reserved to openEHR; federation additions are never prefixed.

F5

latency_ms was an unconditional MUST. N40 and the §9.5 table required it of every endpoint, including those reported excluded or not-localized, which were never asked and so never timed. Writing the schema exposed it: the rule as written obliged a gateway to invent a measurement.

§9.5, N40

Scoped to dispatch. Required for active, offline and time-out. It MUST be omitted, not invented, for the statuses settled before any request existed to time, including not-resolved, which is in scope and so clears complete yet was never dispatched to.

F6

Two OPTIONS keys were required in prose but absent from the example. §11.4 requires all-or-nothing completeness be opt-in per request via a named header, and §10 gives dedup a selecting header; the 0.3.1 example declared all_or_nothing: true without saying how to select it, and omitted dedup.request_header. Both reference implementations emit them.

§7a.2

Added to the example, and opt_in is now conditionally required by the schema whenever the opt-in mode is offered. (In 0.4.0 that flag was all_or_nothing; the SEC amendments reversed the default, so the same conditional now guards best_effort - see The SEC review amendments (0.9.x, no version bump).)

F7

The membership status vocabulary was undefined. §7a.2 said what the OPTIONS endpoint status is not (the §11.1 per-query vocabulary) but never said what it is, implying a closed set that does not exist.

§7a.2

Declared open. It is a free-form string, and active is conventional. Fixing a vocabulary is a membership-lifecycle question this release does not specify (§18). The two schemas model the two statuses as genuinely different types, one a closed seven-value enum and the other an unconstrained string, so the distinction, previously only asserted, is now enforceable.

Two JSON Schemas are now published and enforced in CI (§18 previously promised them):

  • federated-result-set.schema.json - the result envelope. It does not restate RESULT_SET; it inlines a constrained subset of the ITS-REST definitions, each commented with its source, and adds the federation’s meta constraints. The definitions are inlined instead of $ref-ed by URL so that validation is self-contained and CI does not depend on a third-party site being reachable.

  • options-root.schema.json - the OPTIONS body, which has no upstream standard and is therefore normative in this specification alone.

tools/check-schemas.sh validates every whole-instance [source,json] block in the specification against the schema that governs it, and compiles both schemas. It would have caught F3 automatically, and did catch F5 and F6 while being written.

This release is 0.4.0, not 0.3.2: correcting rows changes the wire contract, so federation.spec_version moves from "0.3" to "0.4".

0.9.0: validated by implementation

This release changes no normative requirement, no wire structure and no schema. It changes the specification’s standing: every section of the normative body has now been built against real openEHR CDRs by a reference implementation whose conformance suite mirrors §17 one-to-one. The version number moves because a text that has been implemented end to end is a different artifact from one that has only been reviewed.

Why 0.9.0 and not 0.5.0. The number measures remaining distance from 1.0 and says nothing about the volume of change in this release. The document is feature-complete for its stated scope and has survived implementation, and 0.5.0 would have understated that. The remaining work before 1.0 is external: working-group review, a second independent implementation, and settling the items §18 defers.

Implementation findings are recorded where they apply and are not repeated here. The 0.4.0 findings above (F1-F7) came from building the reference implementation, and the corrections they describe shipped in 0.4.0 as they were found, so this release records no new findings.

Status moves from "Draft for review" to "Release candidate". The new status describes stability and does not claim finality: the specification remains open for comment, nothing in it is frozen, and a release candidate can still be withdrawn.

federation.spec_version moves from "0.4" to "0.9". The move is the only client-visible consequence. The field is major.minor and tracks minor releases (§7a.2), so it moves even though no structure changed. It reports which release a gateway was built against, and a client pinned to "0.4" should learn the document has moved on. Clients matching that value exactly will stop matching. Per §7a.2 they were already forbidden from matching a patch component, and a client needing to accept both should compare the minor version and not the literal string.

The SEC review amendments (0.9.x, no version bump)

A review with SEC raised four points. Three change the wire contract, and one closes a guidance gap. All four landed in the 0.9.x line.

These changes landed without a version bump on purpose, and that is a known hazard

federation.spec_version was not moved, and antora.yml, package.json, README.md and the "spec_version": "0.9" literal in §7a.2 are unchanged. The decision was to keep the document at 0.9.x while the SEC round is still open and not spend a minor number on an in-progress review.

The consequence is a real hazard, accepted knowingly: two different wire contracts now exist under the marker "0.9". A gateway built against 0.9.0 emits meta.complete flat and returns 200 with partial rows by default. A gateway built against this text emits meta.federation.complete and fails closed. Both report "0.9", and a client cannot tell them apart from the OPTIONS body. The completeness object is the only practical discriminator, and only by accident: a body declaring completeness.default: "all-or-nothing" and best_effort is post-amendment while one declaring all_or_nothing is not.

The bump is owed before any release. It is recorded as an open item in §18 so it cannot be lost, and CONTRIBUTING.md names the four hand-edit sites that must move together when it happens.

A fifth site sits in another repository. The reference implementation (syntaric/openehr-federation-ref) will diverge until it is updated: its ConformanceOptionsIT still asserts the pre-amendment completeness shape, and its result-envelope tests still assert the flat meta keys. That divergence is out of scope for this document, and is flagged here so nobody meets it as a surprise.

# Change Where in the spec Rationale

S1

The federation’s meta additions are grouped under one meta.federation object. complete, endpoints[], timeout and dedup move from direct members of meta into a single nested, unprefixed object. The -prefix prohibition is retained and reinforced: stays reserved to openEHR, so the grouping is federation, not _federation.

§9.1, §9.4, §9.5, N17, CP-35, federated-result-set.schema.json

SEC: nothing on the wire said which meta keys belonged to the federation. One namespace also removes the collision risk with a future openEHR meta member of the same name.

S2

All-or-nothing becomes the default completion strategy; best-effort becomes opt-in. A query in which an in-scope node was asked and did not answer now fails - 504 for timeout/unreachability, 424 Failed Dependency for a node error - where it previously returned 200 with the rows it got. A failing response MUST still carry the diagnostic envelope. Best-effort remains available via openEHR-federation-completeness: partial. Two carve-outs are stated explicitly: not-resolved and consent-denied clear the completeness flag but do not fail the query.

§11.4, §11.3, §11.2, N37, CP-30, CP-12, options-root.schema.json

SEC: best-effort is the wrong default for clinical data. An incomplete answer that looks complete is a safety hazard, because a clinician who does not know a node was missing may conclude the data does not exist. The mode is retained because a flagged partial answer can beat no answer.

S3

A gateway MAY hold federated stored queries in an authoritative registry. The gateway stores the definition, versions it immutably on ITS-REST’s own semver path segment, and expands it into an ordinary fan-out when it is invoked by name - which makes a federated query invocable by name at all. Distribution of the definition to nodes is a further option on §12.6’s template-upload terms, and an endpoint-targeted definition MUST NOT be distributed. Drift between the registry’s copy and a node’s is named as a new hazard the facility creates. Additive: §12.6/N43’s single-node rule is unchanged and is the fallback.

§12.7, §7a.1, §7a.2, N44, CP-40, §9.1, options-root.schema.json

SEC: stored queries had no federation story. A definition lived at one node, so invoking it by name got one node’s answer, and two nodes could hold different definitions under one name with nothing to detect the drift.

S4

§13.4 states what a deployment MUST settle about authentication - a checklist of obligations, not mechanisms: which identity crosses the trust boundary and against which authentic source, who authenticates the end user and where that trust stops, how purpose of use is conveyed, what the token is bound to (and whether transport identity is being read as organization identity), and which risks are carried by agreement and which by technique. The federated trust model is named as the model behind the checklist. Annex B gains §B.4a, documenting the VWS Harmonisatie track (concept v0.9, 13 July 2026) as a distinct national track from the Nuts GF profile of §B.4.

§13.4, Annex B §B.4a, N25, CP-39

SEC: §13 did not say what a deployment must decide, nor that it must decide it. The normative body still cannot prescribe a concrete solution, being deployment- and region-specific, so the obligation is to answer the questions, with the Dutch answers kept informative in the annex.

What is left out, and why. No specific authentication profile was hoisted into the normative body, because FAPI 2.0 and RAR are §B.4a’s answers to two of §13.4’s questions and other programmes may answer them differently. The _-prefix rule was not relaxed to carry the grouping: prefixing is openEHR’s namespace and S1 does not borrow it.

Corrections found while aligning the reference implementation (2026-09-28)

Implementing S2 in syntaric/openehr-federation-ref exposed three gaps in the amended text. All three are in the 0.9.x line, under the same no-bump hazard as the SEC amendments above.

# Change Where in the spec Rationale

R1

A node-error status is added to the §11.1 vocabulary. It names the node that was reached and answered with a failure, and is the status behind S2’s 424. error and latency_ms are required on it, as for offline and time-out. Where one fan-out has both an unanswered node and an erroring one, 504 takes precedence over 424. Wire change: the schema enum grows from seven values to eight.

§11.1, §11.2, §11.4, §9.5, N16, N37, N40, CP-11, CP-30, federated-result-set.schema.json

S2 split the failure into 504 and 424 but left the vocabulary at seven values with no status for "answered with an error", so a gateway had to report the 424 case as offline (which it was not) or active (which would set complete: true), and a client could not tell the two recoveries apart from the envelope, the field the spec calls the normative carrier.

R2

Reporting out-of-scope members is now a SHOULD. §11.1 required every in-scope node to appear and defined excluded and not-localized as out of scope, so nothing actually required them to appear; the §9.4 example showed one and §8 said they "are reported", with no obligation level.

§11.1, §8, N16, CP-11

A gateway that dropped directive-excluded members from the list was conformant by the letter and contradicted the example. §14.1’s fail-closed MUST for not-localized is unchanged.

R3

§10 names the dedup members. mode, suppressed_rows and suppressed_endpoints[] were defined only in the schema; the prose said "record the suppressed endpoints" without saying where.

§10.2, §10.3

Editorial; no wire change.

Other notable edits

  • Result set: system_id added to the selectable ENDPOINT attribute set, so a row can carry the openEHR-native routing key (§9).

  • Deduplication: the imported-composition case (same object_id, two `creating_system_id`s) is now handled by an opt-in VERSION-identity dedup (§10).

  • Partial results: the endpoint status set is extended and mapped to HTTP status codes aligned with the Exchange-Routing IG; "patient found nowhere" is a 200 with empty rows, not a 404 (§11).

  • Diagrams: the two reference-flow sequence diagrams are authored in PlantUML (diagrams/*.puml) and embedded as rendered images.

  • Spec vs regional realisation: all Dutch-specific detail (Mitz/OTV, NVI, LRZa, did:web/VC/DPoP, Rego/mitz_consent, pseudonymised-BSN localization) and the ACP/Vitaly worked example stay in the informative Annex B. The normative body names only abstract roles and their proposed IHE binding (§2.4).