OZO FHIR implementation guide
0.8.4 - ci-build

OZO FHIR implementation guide - Local Development build (v0.8.4) built by the FHIR (HL7® FHIR® Standard) Build Tools. See the Directory of published versions

Interaction - Team-to-Team Messaging

Changelog

Changes to this page per IG version. The full project history is on the Changelog page.

Version Date Change
0.8.4 2026-10-01 Subscription criteria Task?id, Communication?id and CommunicationRequest?id replaced by Task?, Communication? and CommunicationRequest?. id is not a search parameter (the resource id parameter is _id); HAPI only accepted the old form because it drops parameters without a value. Note on the criteria form added. The team messaging model itself is under review; see RFC - Team Messaging Model.
0.8.3 2026-10-01 Sender's team tightened to the organizational recipient teams (no subject) that list the sender as participant; a patient care team never counts, so team-wide read never applies to a patient network. extension[senderCareTeam] must be an organizational team the requester participates in. inResponseTo marked as optional in the reply steps. It is a quote-reply link between messages; the thread link is partOf and the OZO FHIR Api does not use inResponseTo. Read receipt: two entity entries instead of "entity.what has two values"; the CommunicationRequest entity is required, the Communication entity optional. Note added that "mark as unread" is not part of the model.
0.8.0 2026-09-17 CommunicationRequest.recipient lists both teams, including the initiating team from extension[senderCareTeam] (invariant ozo-cr-sender-careteam-in-recipient). Task handling described per sender's team (team-wide read). Read receipts use AuditEvent type access (system iso-21089-lifecycle). Query patterns use part-of instead of based-on; new sender-careteam search parameter. Sequence diagram updated.
0.7.6 2026-04-02 Task?status=requested is the only required subscription; Communication?id and CommunicationRequest?id are optional. Explains how Task.focus (introduced in 0.7.5) makes the Task subscription fire on every new message.
0.7.1 2026-03-30 Added the Task?id note for platforms that also want to detect read receipts (REQUESTED → COMPLETED).
0.6.2 2026-03-27 Communication.recipient is no longer set on replies; thread participants live on CommunicationRequest.recipient. Team query pattern changed to part-of:CommunicationRequest.recipient.
0.6.1 2026-03-27 Inline pseudo-code replaced by links to the example resources. Added the "Subscription behavior" section and the Pharmacy A / Clinic B walkthrough with Tasks, AuditEvents and notifications per step.
0.6.0 2026-03-27 Page created, split off from Individual Messaging. Introduces OZOOrganizationalCareTeam, the senderCareTeam extension pattern and the team-to-team sequence diagram.

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: CareTeams without a patient subject, linked to a managing Organization.

For individual messaging (RelatedPerson ↔ Practitioner), see Individual Messaging.

Key Concepts

  1. Both teams are recipients: CommunicationRequest.recipient lists every team participating in the thread: the addressed team (Team B) and the initiating team (Team A). The AAA proxy scopes access to threads, messages and Tasks on recipient (see AAA Proxy). Without its own entry the initiating team could not read the thread it started, and the OZO FHIR Api could not address its members. The profile enforces this with the invariant ozo-cr-sender-careteam-in-recipient.

  2. CareTeam as Reply-To Address: The CommunicationRequest uses the senderCareTeam extension to mark which of the recipient teams initiated the thread. This is needed because FHIR R4 CommunicationRequest.sender does not allow CareTeam references. The extension:
    • Identifies the initiating team (reply-to address) of the conversation
    • Grants team-level authorization for message management
    • Enables the shared inbox pattern
  3. Individual Auditability: All sender fields remain individuals:
    • 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)
    • Every action is traceable to a specific person
  4. Sender's team: For Task handling, the OZO FHIR Api resolves all recipient CareTeams of the thread and determines the sender's team: every recipient CareTeam that is an organizational team (OZOOrganizationalCareTeam) and lists the sender as a participant. The OZO FHIR Api tells the two CareTeam types apart by subject: an organizational team has none, a patient care team (OZOCareTeam) always has one. A patient care team is never the sender's team, even when the sender is a participant: team-wide read applies to organizational teams only, so threads in a patient network keep a read state per person (see Individual Messaging). A recipient team that does not list the sender is not the sender's team either. At thread creation the sender's team is the team in extension[senderCareTeam], which must be an organizational team the requester is a participant of (the OZO FHIR Api rejects the thread otherwise). Members of the sender's team are treated as having read the message (team-wide read); members of the other recipient team(s) get an unread Task.

Roles

This IG distinguishes the following roles when processing team-to-team messages:

  • Team A (initiating team), e.g. a pharmacy, operates through the OZO platform.
  • Team B (receiving team), e.g. a clinic, operates through the OZO platform.
  • The OZO FHIR Api that executes actions triggered by CRUD actions on CommunicationRequest, Communication, Task and AuditEvent.

Both teams use the OZO platform. Unlike individual messaging, there is no OZO client involved: both sides are practitioners.

Prerequisite, Subscriptions

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? (every Task change) instead if the platform also needs to detect read receipts (REQUESTED → COMPLETED transitions).

Optional additional subscriptions:

  • Communication?, 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?, optional. Only needed if you care about thread lifecycle events (creation, revoked, completed) separately from messages.

Criteria form: Subscription.criteria is a FHIR search string. A criteria without parameters is written as Task?, the resource type followed by an empty parameter list. HAPI FHIR requires the ?; a bare Task is rejected with "must be in the form {Resource Type}?[params]". Earlier versions of this page used Task?id, Communication?id and CommunicationRequest?id. id is not a search parameter (the resource id parameter is _id); HAPI only accepted that form because it drops parameters without a value. Replace it with the empty form.

Notify-then-pull pattern

In the Netherlands, healthcare data must not be pushed in subscription notifications. All subscriptions use the notify-then-pull pattern:

  1. The FHIR server sends an empty notification (no resource payload) to the subscriber's endpoint
  2. The subscriber pulls the changed resource by performing a FHIR read or search

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.

Example Subscription resources

Subscription behavior

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? Visibility of messages sent by your own team members (sender's Task goes to COMPLETED). Optional A new Communication is created (POST).
CommunicationRequest? 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 focus field to reference the new Communication. This ensures Task?status=requested fires even when the task was already in REQUESTED status: the focus change creates a new resource version. The focus field also gives clients a direct pointer to the most recent unread message.

This means Task?status=requested is 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.

Create a new team thread

A practitioner from Team A creates a new thread addressed to Team B. The process looks as follows:

  • The OZO platform (on behalf of Team A practitioner) creates a new CommunicationRequest object, the following fields are set:
    • The requester is the Practitioner who initiates the conversation (for auditability)
    • The sender is the same Practitioner (individual sender)
    • The extension[senderCareTeam] is set to the CareTeam of Team A (initiating team, reply-to address). This must be an organizational CareTeam (no subject) the requester is a participant of; the OZO FHIR Api rejects the CommunicationRequest otherwise
    • The subject is the Patient reference
    • The recipient lists the CareTeam of Team B and the CareTeam of Team A (the same reference as in extension[senderCareTeam])
    • The status is set to ACTIVE
    • The payload contains the initial message
  • The OZO FHIR Api creates a Task for each member of every recipient CareTeam:
    • The status is set to REQUESTED for the members of Team B, and to COMPLETED for the members of Team A (the sender's team: the requester sent the message and the colleagues are covered by the team-wide read)
    • The intent is set to ORDER
    • The basedOn is set to the CommunicationRequest reference
    • The subject is set to the Patient reference
    • The owner is set to the individual CareTeam member
    • The focus is not set at thread creation (there is no initial Communication yet; the thread's initial message is on CommunicationRequest.payload)
  • The OZO platform (Team B side) receives the new CommunicationRequest and Task by Subscription:
    • The CommunicationRequest subscription notifies Team B of the new thread
    • The Task (status REQUESTED) tracks the unread state per team member
    • The thread appears in Team B's shared inbox for all team members

Respond to a team thread (Team B replies)

A practitioner from Team B responds to the thread. Replies are not addressed individually: the Communication only references the thread, and the thread's recipient list defines who participates.

  • The OZO platform (on behalf of Team B practitioner) creates a new Communication with the following fields:
    • The partOf is set to the reference of the CommunicationRequest
    • Optionally, inResponseTo references the specific earlier Communication this message replies to (a quote-reply). The OZO thread model does not need it: the message belongs to the thread via partOf, and the OZO FHIR Api does not use inResponseTo for Task handling. Platforms without a reply-to-message concept leave it out.
    • The sender is set to the Practitioner from Team B (individual auditability)
    • The payload consists of text and optionally attachments
    • The status is set to COMPLETED
    • Note: recipient is not set; thread participants are defined on the CommunicationRequest.
  • The OZO FHIR Api resolves the recipient CareTeams of the CommunicationRequest and determines the sender's team: the organizational recipient team(s) (no subject) in which Communication.sender is a participant (here Team B). Then:
    • For each member of the other recipient team(s) (here Team A):
      • An existing task is queried; depending on the result the following action is taken:
        • if a task exists:
          • The status is set to REQUESTED
          • The focus is updated to reference the new Communication (ensures the subscription fires even when status was already REQUESTED)
        • if a task does not exist, a new one is created with the following properties:
          • The status is set to REQUESTED
          • The intent is set to ORDER
          • The basedOn is set to the CommunicationRequest reference
          • The subject is set to the Patient reference
          • The owner is set to the individual CareTeam member
          • The focus is set to the new Communication reference
    • For each member of the sender's team (here Team B):
      • The existing task status is set to COMPLETED (a colleague has responded, team-wide read)
      • The focus is updated to reference the new Communication
  • The OZO platform (Team A side) receives a notification by Subscription:
    • The new message appears in Team A's shared inbox
    • Any practitioner from Team A can view the response
    • The Task?status=requested subscription fires: the Team A Tasks moved to REQUESTED and focus points to the new Communication, which creates a new version even when the status was already REQUESTED.

Follow-up from a different team member (Team A replies)

A different practitioner from Team A follows up on the thread. This demonstrates that any team member can participate in the conversation.

  • The OZO platform (on behalf of a different Team A practitioner) creates a new Communication with the following fields:
    • The partOf is set to the reference of the CommunicationRequest
    • inResponseTo is optional, as above
    • The sender is set to the different Practitioner from Team A (individual auditability; note this is a different person than the original requester)
    • The payload consists of text and optionally attachments
    • The status is set to COMPLETED
  • The OZO FHIR Api processes tasks the same way as described above. The sender is a participant of Team A, so Team A is the sender's team:
    • Team B members get REQUESTED tasks
    • Other Team A members get COMPLETED tasks (their colleague responded)

Marking messages as read

When a practitioner reads a message in a team thread:

  • The OZO platform creates an AuditEvent (a read receipt) with the following properties:
    • The type is set to http://terminology.hl7.org/CodeSystem/iso-21089-lifecycle|access. This is the only type the OZO FHIR Api treats as a read receipt; the REST audit events the proxy creates itself use type rest and are ignored here. See Manu-Read-Messages for an example.
    • The action is set to 'R'
    • The recorded field is set to the current timestamp
    • The agent.who field is set to the Practitioner who read the message
    • Two entity entries, each with one what (the profile allows entity 0..* and entity.what 0..1):
      • A reference to the CommunicationRequest. Required: the OZO FHIR Api looks up the Task with Task?based-on=<CommunicationRequest>&owner=<agent>; without this entry the read receipt is ignored.
      • A reference to the Communication that was read. Optional but recommended: the OZO FHIR Api only completes the Task when this is the newest message in the thread, so reading an older message does not clear a newer unread one; without this entry the thread is marked read unconditionally. It also records for the NEN7510 audit trail which message was viewed.
  • The OZO FHIR Api does the following:
    • The Task is queried for the Practitioner in agent.who of the AuditEvent and its status is set to COMPLETED
    • The reader's team is resolved the same way as the sender's team: the organizational recipient CareTeam(s) of the CommunicationRequest in which the reader is a participant. The Tasks of all other members of that team are set to COMPLETED as well (team-wide read). A patient care team among the recipients is left alone: its members keep their own Task.
  • When one practitioner of an organizational CareTeam reads the message, the message is marked as read for all the members of that CareTeam.

Note: "Mark as unread" is not part of the OZO messaging model. The read state lives in the Task (requested = unread, completed = read) and only the OZO FHIR Api changes it: on a read receipt, on a new message, and at thread creation. Clients have read-only access to Task, and there is no AuditEvent type that sets a Task back to requested. A platform that offers "mark as unread" keeps that state locally; it is not visible to other systems or, in team threads, to other team members.

Interaction diagram

The diagram below displays the team-to-team messaging flow, including thread creation and responses from both teams. {::nomarkdown}

OZO PlatformFHIR APIOZO PlatformOZO Platform(Team A)OZO Platform(Team A)FHIR APIFHIR APIOZO Platform(Team B)OZO Platform(Team B)Thread Creation (Team A initiates)Create CommunicationRequest(status: 'active')requester/sender = Practitioner A1extension[senderCareTeam] = CareTeam Arecipient = CareTeam B, CareTeam ACreate TasksCareTeam B members: 'requested'CareTeam A members: 'completed'(sender's team)Notify[Via Subscription, empty payload]Fetch CommunicationRequestThread appears inTeam B's shared inboxReply from Team BCreate Communicationsender = Practitioner B1partOf = CommunicationRequest(no recipient on Communication)Resolve recipient CareTeamssender's team = CareTeam BUpdate TasksCareTeam A members: 'requested'focus = new CommunicationComplete Tasksfor other CareTeam B members(team-wide read)Notify[Via Subscription, empty payload]Fetch CommunicationNew message inTeam A's shared inboxFollow-up from Different Team A MemberCreate Communicationsender = Practitioner A2(different team member)partOf = CommunicationRequestResolve recipient CareTeamssender's team = CareTeam AUpdate TasksCareTeam B members: 'requested'focus = new CommunicationComplete Tasksfor other CareTeam A members(team-wide read)Notify[Via Subscription, empty payload]Fetch CommunicationRead ReceiptCreate AuditEvent(type: iso-21089-lifecycle 'access', action: 'R')agent = Practitioner B1Complete Tasksfor all CareTeam B members(team-wide read)Message marked as readfor all CareTeam B membersNotify Task update[Via Subscription, empty payload]Fetch TaskMessage shown asread by Team B

{:/}


Example: Pharmacy to Clinic Communication

The following walkthrough shows a concrete example with Apotheek de Pil (Pharmacy A) and Huisarts Amsterdam (Clinic B).

Step 1: Pharmacy initiates thread

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. Both teams are listed as recipient: Clinic-B as addressed team and Pharmacy-A as initiating team (also referenced in extension[senderCareTeam]).

The OZO FHIR Api creates a Task for each member of both teams:

  • Clinic B (Manu van Weel, Mark Benson, Johan van den Berg): status requested; see Notify-Manu-van-Weel for an example of a Task resource
  • Pharmacy A (A.P. Otheeker, Pieter de Vries): status completed (sender's team)

Notifications fired:

  • CommunicationRequest subscription → both teams notified of the new thread (Pharmacy A is a recipient too)
  • Task?status=requested subscription → each Clinic B practitioner notified of unread thread

Step 2: Manu van Weel reads the message and replies

Dr. Manu van Weel from the clinic reads the message. The OZO platform creates a read receipt AuditEvent with type iso-21089-lifecycle|access; 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):

  • Task (for Manu van Weel): status → completed (was: requested)
  • Task (for Mark Benson): status → completed (was: requested, team-wide read)
  • Task (for Johan van den Berg): status → completed (was: requested, team-wide read)

Manu then replies with a Communication whose partOf points to the thread; see Clinic-Response-to-Pharmacy for the full resource. The OZO FHIR Api resolves the recipient teams of the thread: Manu is a participant of Clinic B, so Clinic B is the sender's team and Pharmacy A gets the unread Tasks.

Pharmacy A Tasks:

  • Task (for A.P. Otheeker): status → requested, focus → new Communication
  • Task (for Pieter de Vries): status → requested, focus → new Communication

Clinic B Tasks:

  • Task (for Mark Benson, Johan van den Berg): status stays completed (colleague responded), focus → new Communication

Notifications fired:

  • Communication subscription → Pharmacy A practitioners notified of new message
  • Task?status=requested subscription → each Pharmacy A practitioner notified of unread message

Step 3: Different pharmacy practitioner follows up

A.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. Pieter is a participant of Pharmacy A, so Pharmacy A is the sender's team.

The OZO FHIR Api updates Tasks:

Pharmacy A Tasks:

  • Task (for A.P. Otheeker): status → completed (was: requested, team-wide read)
  • Task (for Pieter de Vries): status → completed (Pieter is the sender)

Clinic B Tasks:

  • Task (for Manu van Weel): status → requested (was: completed)
  • Task (for Mark Benson): status → requested (was: completed)
  • Task (for Johan van den Berg): status → 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 focus update would still create a new Task version and fire the subscription. The focus field eliminates the no-op scenario.


Query Patterns

Communication links to its thread via partOf, so thread searches use the part-of search parameter. based-on is a different element and is not used in the OZO messaging model.

Find messages for my team (via thread membership):

GET /Communication?part-of:CommunicationRequest.recipient=CareTeam/Pharmacy-A

The AAA proxy applies this filter automatically for client access, so a plain GET /Communication returns the same result for a team member. _include is blocked for clients by the proxy; system access can add &_include=Communication:part-of to fetch the threads in the same call.

Find all messages in a thread:

GET /Communication?part-of=CommunicationRequest/thread-id&_sort=sent

Find messages I sent:

GET /Communication?sender=Practitioner/my-id

Find all threads my team participates in:

GET /CommunicationRequest?recipient=CareTeam/Pharmacy-A

Find threads initiated by my team:

GET /CommunicationRequest?sender-careteam=CareTeam/Pharmacy-A

sender-careteam is a custom search parameter defined by this IG on extension[senderCareTeam]; see ozo-communicationrequest-sender-careteam. The HAPI FHIR server needs the OZO package installed and existing threads reindexed once; see Installing OZO Package in HAPI FHIR Server.


Examples

Subscriptions

Messaging resources

Search parameters

For detailed analysis of the addressing solution, see FHIR Addressing Analysis.