OZO FHIR implementation guide
0.7.8 - ci-build
OZO FHIR implementation guide - Local Development build (v0.7.8) built by the FHIR (HL7® FHIR® Standard) Build Tools. See the Directory of published versions
Team-to-team messaging enables organizations (pharmacies, clinics, hospitals) to communicate as units while maintaining individual auditability. This pattern uses the OZOOrganizationalCareTeam profile to represent organizational teams — these are CareTeams without a patient subject, linked to a managing Organization.
For individual messaging (RelatedPerson ↔ Practitioner), see Individual Messaging.
CommunicationRequest uses the senderCareTeam extension to specify a CareTeam as the reply-to address. This is needed because FHIR R4 CommunicationRequest.sender does not allow CareTeam references. The extension:
CommunicationRequest.requester is always an individual (who initiated the thread)CommunicationRequest.sender is always an individual (same as requester)Communication.sender is always an individual (who sent each message)senderCareTeam extension on the CommunicationRequest identifies the initiating team. The OZO FHIR Api uses this to determine which team members receive Tasks when a reply is created.This IG distinguishes the following roles when processing team-to-team messages:
CommunicationRequest, Communication, Task and AuditEvent.Both teams use the OZO platform. Unlike individual messaging, there is no OZO client involved — both sides are practitioners.
In practice, a single Subscription is enough for most teams:
Task?status=requested — required. Covers unread tracking and new-message notification for the team (the AAA proxy automatically scopes this to the team's ownership). Use Task?id instead if the platform also needs to detect read receipts (REQUESTED → COMPLETED transitions).Optional additional subscriptions:
Communication?id — optional. Only needed to see messages sent by your own team members. Their own Task is set to COMPLETED on send and won't match status=requested.CommunicationRequest?id — optional. Only needed if you care about thread lifecycle events (creation, revoked, completed) separately from messages.In the Netherlands, healthcare data must not be pushed in subscription notifications. All subscriptions use the notify-then-pull pattern:
This means channel.payload must be left empty. The notification only signals that something matched the subscription criteria — the subscriber is responsible for fetching the actual data.
Each subscription serves a different purpose. Understanding when notifications fire is critical for correct client implementation:
| Subscription | Purpose | Required? | Fires when |
|---|---|---|---|
Task?status=requested |
Unread tracking and new-message notification for the team. Primary mechanism. | Required | Any change to a Task that matches status=requested. This includes status transitions to REQUESTED AND content changes (like focus) on Tasks already REQUESTED. |
Communication?id |
Visibility of messages sent by your own team members (sender's Task goes to COMPLETED). | Optional | A new Communication is created (POST). |
CommunicationRequest?id |
Thread lifecycle changes (creation, revoked, completed). | Optional | A CommunicationRequest is created or its status changes. |
Important: When a new message arrives, the OZO FHIR Api updates the Task's
focusfield to reference the newCommunication. This ensuresTask?status=requestedfires even when the task was already in REQUESTED status — thefocuschange creates a new resource version. Thefocusfield also gives clients a direct pointer to the most recent unread message.This means
Task?status=requestedis a reliable single subscription for both unread tracking and new-message notification. The other two subscriptions are optional and only needed for specific edge cases.
A practitioner from Team A creates a new thread addressed to Team B. The process looks as follows:
CommunicationRequest object, the following fields are set:
requester is the Practitioner who initiates the conversation (for auditability)sender is the same Practitioner (individual sender)extension[senderCareTeam] is set to the CareTeam of Team A (reply-to address for team-level authorization)subject is the Patient referencerecipient is the CareTeam of Team Bstatus is set to ACTIVEpayload contains the initial messageTask for each member of Team B's CareTeam:
status is set to REQUESTEDintent is set to ORDERbasedOn is set to the CommunicationRequest referencesubject is set to the Patient referenceowner is set to the individual CareTeam memberfocus is not set at thread creation (there is no initial Communication yet — the thread's initial message is on CommunicationRequest.payload)CommunicationRequest and Task by Subscription:
CommunicationRequest subscription notifies Team B of the new threadTask (status REQUESTED) tracks the unread state per team memberA practitioner from Team B responds to the thread. The reply is addressed to Team A's CareTeam, which is discovered from the senderCareTeam extension on the original CommunicationRequest.
Communication with the following fields:
partOf is set to the reference of the CommunicationRequestinResponseTo is set to the reference of the previous Communication being replied tosender is set to the Practitioner from Team B (individual auditability)payload consists of text and optionally attachmentsstatus is set to COMPLETEDrecipient is not set — thread participants are defined on the CommunicationRequest.CareTeam (the recipient) and not the sender:
focus is updated to reference the new Communication (ensures the subscription fires even when status was already REQUESTED)status is set to REQUESTEDintent is set to ORDERbasedOn is set to the CommunicationRequest referencesubject is set to the Patient referenceowner is set to the individual CareTeam memberfocus is set to the new Communication referenceCareTeam who is not the sender:
focus is updated to reference the new CommunicationCommunication by Subscription:
Task subscription also fires: focus was updated to point to the new Communication, creating a new version even when the status was already REQUESTED.A different practitioner from Team A follows up on the thread. This demonstrates that any team member can participate in the conversation.
Communication with the following fields:
partOf is set to the reference of the CommunicationRequestinResponseTo is set to the reference of the previous Communicationsender is set to the different Practitioner from Team A (individual auditability — note this is a different person than the original requester)payload consists of text and optionally attachmentsstatus is set to COMPLETEDWhen a practitioner reads a message in a team thread:
AuditEvent with the following properties:
type is set to http://terminology.hl7.org/CodeSystem/iso-21089-lifecycle|accessaction is set to 'R'recorded field is set to the current timestampagent.who field is set to the Practitioner who read the messageentity.what field has two values:
CommunicationCommunicationRequestTask is queried for the Practitioner as part of the agent.who of the AuditEventTask status is set to COMPLETEDCareTeam reads the message, the message is marked as read for all the members in the CareTeam.The diagram below displays the team-to-team messaging flow, including thread creation and responses from both teams. {::nomarkdown}
{:/}
The following walkthrough shows a concrete example with Apotheek de Pil (Pharmacy A) and Huisarts Amsterdam (Clinic B).
A pharmacist (A.P. Otheeker) from Apotheek de Pil sends a message to Huisarts Amsterdam about a patient's medication — see Pharmacy-to-Clinic for the full CommunicationRequest.
The OZO FHIR Api creates a Task (status requested) for each Clinic B member — see Notify-Manu-van-Weel for an example of a Task resource.
Notifications fired:
CommunicationRequest subscription → Clinic B notified of new threadTask?status=requested subscription → each Clinic B practitioner notified of unread threadDr. Manu van Weel from the clinic reads the message. The OZO platform creates an AuditEvent — see Manu-Read-Messages for a similar example.
The OZO FHIR Api marks the Tasks as completed. Because this is a team message, all Clinic B Tasks are completed (team-wide read):
completed (was: requested)completed (was: requested, team-wide read)completed (was: requested, team-wide read)Manu then replies. The reply goes to the pharmacy team (read from CommunicationRequest.extension[senderCareTeam]) — see Clinic-Response-to-Pharmacy for the full Communication.
The OZO FHIR Api creates/updates Tasks for Pharmacy A members:
requestedrequestedNotifications fired:
Communication subscription → Pharmacy A practitioners notified of new messageTask?status=requested subscription → each Pharmacy A practitioner notified of unread messageA.P. Otheeker has not read the reply yet (Task still REQUESTED). Pieter de Vries reads it and responds, demonstrating that any team member can participate — see Pharmacy-Followup-by-Pieter for the full Communication.
The OZO FHIR Api updates Tasks:
Pharmacy A Tasks:
completed (was: requested, team-wide read)completed (Pieter is the sender)Clinic B Tasks:
requested (was: completed)requested (was: completed)requested (was: completed)Notifications fired:
Communication subscription → Clinic B practitioners notified of new message (always fires)Task?status=requested subscription → each Clinic B practitioner notified (status changed from COMPLETED to REQUESTED AND focus updated to new Communication)Note: Even if Clinic B had not yet read the previous message (Tasks still REQUESTED), the
focusupdate would still create a new Task version and fire the subscription. Thefocusfield eliminates the no-op scenario.
Find messages for my team (via thread membership):
GET /Communication?part-of:CommunicationRequest.recipient=CareTeam/Pharmacy-A&_include=Communication:based-on
Find all messages in a thread:
GET /Communication?based-on=CommunicationRequest/thread-id&_sort=sent
Find messages I sent:
GET /Communication?sender=Practitioner/my-id
Find threads initiated by my team:
GET /CommunicationRequest?_has:Extension:url=http://ozoverbindzorg.nl/fhir/StructureDefinition/ozo-sender-careteam&sender-careteam=CareTeam/Pharmacy-A
For detailed analysis of the addressing solution, see FHIR Addressing Analysis.