Netherlands - Generic Functions for data exchange Implementation Guide
0.3.0 - ci-build
Netherlands - Generic Functions for data exchange Implementation Guide, published by Stichting Nuts. This guide is not an authorized publication; it is the continuous build for version 0.3.0 built by the FHIR (HL7® FHIR® Standard) CI Build. This version is based on the current content of https://github.com/nuts-foundation/nl-generic-functions-ig/ and changes regularly. See the Directory of published versions
| Page standards status: Draft |
The exchange specifications in this IG are designed as three stackable building blocks. Each block defines a small set of mechanisms with explicit extension points. A use case is a concrete combination of these blocks: it picks a topic, a focus shape, a dataset binding, and an authorization model, and inherits everything else from the blocks below.
This page explains the layering, documents the design decisions behind it, and shows one concrete instantiation (a BGZ referral) so the role of each block is visible end to end.
The diagram below shows the artifacts of each block (the FHIR resources, extensions and references that make up the wire surface) and how they compose. Composition arrows (filled diamond) show "contained in"; reference arrows (open arrowhead) show "points at". The cross-layer arrows from notification-event.focus show that NP focus targets and CW focus targets are alternatives at the same slot.
The wire-level mechanism: a long-lived Subscription between two partners for a given SubscriptionTopic, a notification Bundle carrying an event with an event-number for gap detection, and the handshake / heartbeat lifecycle. Subscription.channel.endpoint is a plain URL value (not a FHIR Reference to an Endpoint resource); the Sender resolves the Receiver's URL out-of-band from the Care Services Directory and copies it into the Subscription. See Notification.
What the Receiver does with a notification: pull the resource that notification-event.focus points at, follow its references, detect updates via _lastUpdated / _history, and observe withdrawals via HTTP 410 / 404 / 403 on the next pull. NP is strictly read-only at the Sender. The notification-authorization-hint opaque token is carried here. See Notified Pull.
A profile on HL7 Clinical Order Workflows Pattern 2 ("Subscriptions with Task at Placer"). CW reuses NP twice: once on a Coordination Task (workflow channel) and once on the dataset (data channel). It adds the things NP intentionally lacks: a Coordination Task lifecycle expressed via Task.businessStatus, Fulfiller write-back to the Placer's Task resource, multi-candidate solicitation, cancellation negotiation via a CancellationRequest Task, and dataset binding via Request.instantiatesCanonical to a PlanDefinition. See Clinical Workflow.
The data offered in a CW or pure NP exchange typically draws from three sources:
PlanDefinition that enumerates the FHIR queries comprising a BgZ snapshot).Two design options for how these reach the Receiver:
Option A (proposed): the Sender pre-resolves everything into a flat container. The Placer evaluates the canonical queries and any supplemental queries against its own data, gathers the matching resources, and writes the references into a single container (List, Composition, or DocumentReference depending on the use case). The container is the operational truth on the wire.
Option B (less optimal): the container carries query specifications. Entries would be a mix of Reference(clinical resource) for instances and Reference(PlanDefinition) or contained Library resources for queries the Receiver must execute.
The working group leans toward Option A. The reasons:
notification-event.focus resolves to a "read and follow references" interaction on the Receiver side, regardless of use case. No branching on entry resource type. One pull loop in the Receiver covers every scenario.List._history gives an auditable trail of what was offered when. Option B has no equivalent because the queries are stable while their results drift outside the container.The cost the Sender pays.
Option A is an implementation tax on the Placer:
EHRs that cannot watch their own data for changes cannot participate as Placers for this profile. This is a deliberate trade: the Receiver implementation stays simple and uniform; the capability requirement sits on the Sender. We accept it because Receiver heterogeneity is the harder problem to manage at national scale.
The workflow channel is straightforward: one long-lived Subscription per partner pair, filtered on Task.owner.identifier = <Fulfiller URA>. The data channel admits two strategies with materially different operational profiles.
Strategy 1: broad per-Receiver Subscription (recommended default).
One data Subscription per partner pair, long-lived alongside the partnership. The filter selects containers destined for this Receiver via an nl-intended-recipient extension on List / Composition / DocumentReference. The Placer sets this extension on every container it offers; the existing Subscription matches automatically.
| Property | Value |
|---|---|
| Subscription count at the Placer | One per partner pair |
| Lifecycle | Long-lived; aligned with the partnership |
| Filter shape | List?intended-recipient.identifier=http://fhir.nl/fhir/NamingSystem/ura\|<Fulfiller URA> |
| Receiver-side dispatch | Receiver looks up which Coordination Task / offer a container belongs to (one extra step per notification) |
| Multi-Receiver offers | Native: nl-intended-recipient is 0..* on the container |
| Requires new IG artifacts | Extension nl-intended-recipient and a matching SearchParameter |
Strategy 2: narrow per-container Subscription.
A new Subscription is created when an offer is accepted, with a filter pinning the specific container by _id. Retired when the Coordination Task reaches a terminal state.
| Property | Value |
|---|---|
| Subscription count at the Placer | One per active offer per Receiver |
| Lifecycle | Bound to the offer; created on accept, retired on terminal Task state |
| Filter shape | List?_id=<offer-list-id> |
| Receiver-side dispatch | Implicit: one Subscription equals one offer |
| Multi-Receiver offers | Requires parallel Subscriptions, one per candidate |
| Requires new IG artifacts | None beyond standard _id search |
Scale and the recommendation.
Notification volume per Receiver is the same in both strategies (each Receiver receives only its own notifications). What differs is the Placer's Subscription registry.
At realistic Dutch volumes (a hospital can run thousands of active referrals + transfers of care concurrently, across dozens of partners) Strategy 2 turns the Subscription registry into the bottleneck: thousands of create / retire events per day, each tied to a Coordination Task lifecycle. Strategy 1 reduces this to a fixed registry sized by the partner graph (dozens of Subscriptions, durably configured).
This IG proposes Strategy 1 as the default. Use cases with low offer volume, strict isolation requirements between offers, or implementations that cannot author the nl-intended-recipient extension MAY use Strategy 2 by documenting the choice in the use-case profile.
The workflow channel is unaffected: it stays broad per-Receiver, filtered on Task.owner. CW is a profile on COW; that filter mirrors COW Pattern 2 exactly.
The container is a per-use-case decision, not a building-block decision. NP supports four focus shapes; CW inherits them via the data channel:
| Container | Use when |
|---|---|
List |
The offer is a flat collection of resource references with no document semantics. Recommended default for data sets. |
Composition |
The offer is a clinical document with section-based narrative, author, custodian, attestation. |
DocumentReference |
The offered payload is an opaque attachment (PDF/A, image, ZIP). |
| Single clinical resource | The offer is one resource (e.g. a single Observation availability event). No wrapper container. |
Concrete picks for known use cases:
List. BgZ is a data set with categorical entries; no narrative.Composition. The handoff has sections, narrative, custodian, signatures.DocumentReference. The payload is a PDF.The container choice does not affect the building-block layering. NP's pull-and-follow semantics work identically across the four shapes; CW just specifies that its data channel's focus is whatever container the use case selected.
Combining the decisions above: a CW exchange under the recommended profile uses two long-lived Subscriptions per partner pair.
| Subscription | Topic example | Filter example | Lifetime |
|---|---|---|---|
| Workflow channel | .../SubscriptionTopic/nl-cow-coordination-task-changes |
Task?owner.identifier=URA\|<Fulfiller URA> |
Long-lived; one per partner pair |
| Data channel | .../SubscriptionTopic/nl-data-offer-changes |
List?intended-recipient.identifier=URA\|<Fulfiller URA> |
Long-lived; one per partner pair |
The data channel Subscription is shared between CW (where containers are tied to Coordination Tasks) and pure NP (where containers are offered without a workflow above them). One topic, one filter shape, two consumers.
The diagram below shows a single BGZ referral from a Placer hospital (URA 11111111) to a Fulfiller hospital (URA 22222222). Each step is annotated with the building block in use.
Three things to notice:
focus referencing the Coordination Task, once with focus referencing a List (the BgZ snapshot).Task.businessStatus drives the COW state machine; Task.status follows the FHIR R4 normative state machine alongside.| Building block | Primary FHIR artifacts |
|---|---|
| Notification | Subscription, SubscriptionTopic, notification Bundle (Backport profiles) |
| Notified Pull | Any FHIR resource as focus; container patterns use List, Composition, or DocumentReference; nl-intended-recipient extension for broad per-Receiver Subscriptions |
| Clinical Workflow | Task (Coordination + CancellationRequest), ServiceRequest / MedicationRequest / DeviceRequest, PlanDefinition |