Netherlands - Generic Functions for data exchange Implementation Guide
0.3.0 - ci-build Netherlands flag

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

Building blocks

Page standards status: Draft

Purpose

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 three building blocks

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.

Exchange building blocks: artifacts and compositionExchange building blocks: artifacts and compositionTransport (Notification)Notified Pull (NP): focus targetsClinical Workflow (CW)Subscriptionstatuscriteria : SubscriptionTopic canonicalchannel.type = rest-hookchannel.endpoint : urlchannel.payload = id-onlychannel.heartbeat-periodCreation mode (not a subtype):- in-band: Client POSTs to Sender- out-of-band: Sender creates;channel.endpoint URL resolvedout-of-band from the addressingfunction (Care Services Directory)SubscriptionTopicurl : canonicalresourceTriggercanFilterByPublished per use case(CW workflow channel,NP data-availability channel, ...)Notification Bundletype = historyVariants (by SubscriptionStatus.type):- event-notification- handshake-notification- heartbeat-notificationSubscriptionStatussubscription : Referencestatustypenotification-eventevent-numbertimestampadditional-context : Reference[]focus : Reference(Any)«extension»notification-authorization-hintopaque token from Sender«focus target»single clinicalresource«focus target / container»List«focus target / container»Composition«focus target / container»DocumentReferenceFlat list of concrete resourcereferences. The Sender resolvesall dataset queries (canonical+ per-case) into entries beforenotifying. Receiver behaviour:- read focus, follow .entry / .section / .content- Patient + BSN on .subject.identifier- diff via _lastUpdated and _history- withdrawal via HTTP 410 / 404 / 403«focus target»Coordination Taskintent = orderrestriction.periodstatus (FHIR R4 normative)businessStatus (COW phase)code (use case)for.identifier (BSN)requester (Placer)owner (Fulfiller)output (outcome document)Fulfiller PUTs status /businessStatus back at thePlacer's FHIR endpoint(CW write-back, not NP).«focus target»CancellationRequestTaskstatuscode = abortCreated by Placer after theCoordination Task reachesin-progress.RequestcodesupportingInfosubject (Patient + BSN)ServiceRequest, MedicationRequest,DeviceRequest, ... = the Order.«at Nictiz»PlanDefinitionurl : canonicalaction[].inputCanonical definition of whata BgZ / eOverdracht / ...realization contains.Used by the Placer to evaluateits own data and assemble theList. Informative for theReceiver; the Receiver doesNOT execute these queries.criteria11entry[0]1111..*extension10..1focus11focus11instantiatesCanonical(informative)10..1focusfocusfocusfocusfocus (CW workflow channel)focus (CW cancellation)

Notification (Transport)

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.

Notified Pull (NP)

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.

Clinical Workflow (CW)

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.

Why the Sender pre-resolves the dataset

The data offered in a CW or pure NP exchange typically draws from three sources:

  1. A canonical dataset specification (e.g. Nictiz publishes a BgZ PlanDefinition that enumerates the FHIR queries comprising a BgZ snapshot).
  2. Specific resource instances the Sender wants to attach to this case (e.g. the trigger Observation that motivated a referral).
  3. Per-case supplemental queries the Sender wants to evaluate beyond the canonical dataset.

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 precision. Only the party that resolves a query can observe when its result changes. If the Sender resolves, notifications carry actual deltas (entry added, content updated, entry removed). If the Receiver resolves, the notification can at best say "something might have changed, please re-poll your queries", with no delta. NP's design assumes precise notifications; Option B forces it into a coarse polling mode.
  • Wire consistency. Every 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.
  • Privacy. The Placer curates exactly what is in the container. A query may match more resources than the Placer expected; closing that gap requires Placer-side filtering anyway, which is most of the work of Option A.
  • Audit clarity. 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:

  • The Placer evaluates every canonical PlanDefinition and every per-case supplemental query against its own data.
  • The Placer continuously detects when underlying data changes (new lab result, updated medication, withdrawn observation) and reflects that change in the relevant containers.
  • For each impacted container, the Placer updates the container's membership or content so that the data Subscription fires.

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.

Subscription topic strategies for the data channel

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.

Container choice for the data channel

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:

  • BGZ referral: List. BgZ is a data set with categorical entries; no narrative.
  • eOverdracht (nursing handoff): Composition. The handoff has sections, narrative, custodian, signatures.
  • Discharge letter delivery: 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.

Subscriptions summary

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.

Example: BGZ referral

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.

BGZ referral: building blocks stacked in a concrete exchangeBGZ referral: building blocks stacked in a concrete exchangePlacerPlacerPlacerPlacerPlacerPlacerFulfillerFulfillerFulfillerPlacerFulfillerPlacer(URA 11111111)Placer(URA 11111111)Fulfiller(URA 22222222)Fulfiller(URA 22222222)PlacerPlacerPlacerPlacerPlacerPlacerFulfillerFulfillerFulfillerPre-coordination: two long-lived Subscriptions per partner pairWorkflow channel (Notification + NP):Topic: Coordination Task changes.Filter: Task?owner.identifier=URA|22222222. Data channel (Notification + NP):Topic: data offer changes.Filter: List?intended-recipient.identifier=URA|22222222. Both Subscriptions are hosted at the Placer and long-livedwith the partnership.CW: Order placed at Placer1Create ServiceRequest (the Order, BgZ dataset)+ Coordination Task(Task.status = requested,Task.businessStatus = requested,Task.for.identifier = BSN 999911120)Notification + NP on the Coordination Task2POST notification Bundlefocus = Coordination Taskadditional-context = ServiceRequest (optional)3200 OK4GET Coordination Task(_include Task.focus = ServiceRequest)5200 OKTask + ServiceRequestNP pull on the workflow anchor.BSN read from Task.for.identifier.PlanDefinition canonical fromServiceRequest.instantiatesCanonicalis informative only: it declareswhich dataset to expect.The Receiver does not run its queries.CW: Fulfiller accepts (write-back to Placer)6PUT Coordination Task(Task.status = accepted,Task.businessStatus = accepted)7200 OKPlacer pre-resolves the dataset8Evaluate the BgZ PlanDefinition(and any per-case additions)against local data.Assemble a flat List of concrete references(Patient, Conditions, MedicationStatements,Observations, ...).Set List.intended-recipient = Fulfiller URAso the existing data Subscription matches.Notification + NP on the dataset9POST notification Bundlefocus = List (BgZ snapshot)10200 OK11GET List with _include entries12200 OKList + BgZ resourcesNP pull on the data anchor.The Receiver follows List.entry refs;no query execution at the Receiver.The Receiver dispatches the notificationto the right local Coordination Task(one extra lookup under the broadSubscription strategy).notification-authorization-hint scopesthe pull to this patient + dataset.CW: Fulfiller starts work (write-back)13PUT Coordination Task(Task.businessStatus = in-progress)14200 OKLoop: BgZ updates during the workflowloop[While the Coordination Task is in-progress]15BgZ data at the Placer changes;Placer updates the List(entry added / removed / version bumped)16POST notification Bundlefocus = List (event-number incremented)additional-context = changed entries (optional)17200 OK18GET refreshed entries(or _lastUpdated-filtered against the List)19200 OKCW: Fulfiller completes (write-back)20PUT Coordination Task(Task.status = completed,Task.businessStatus = completed,Task.output = outcome document)21200 OKList for this offer may be retained for audit (later pulls return 410 when actually withdrawn)or deleted. Both Subscriptions remain active for the partnership.

Three things to notice:

  1. The same Notification + NP machinery appears twice in this single exchange: once with focus referencing the Coordination Task, once with focus referencing a List (the BgZ snapshot).
  2. The only flow not expressible as Notification + NP is the Fulfiller writing the Coordination Task back at the Placer (steps marked "write-back"). Write-back is CW-specific; the Placer observes the writes at its own endpoint and does not notify itself.
  3. The Coordination Task's Task.businessStatus drives the COW state machine; Task.status follows the FHIR R4 normative state machine alongside.

Mapping to FHIR resources

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