# HL7 MLLP tutorial - Route clinical messages

Route HL7 MLLP messages between clinical systems.

[Video: Channel with destinations](https://zatosource-production.b-cdn.net/docs/gfx/channels/hl7-mllp-create-destinations.webm?v=1791656191)

Dashboard menu: Connections > Channels > HL7 > MLLP

With Zato, you can route HL7 MLLP messages between clinical systems in one of two ways, and this tutorial will show you both.

![Demo config](https://zatosource-production.b-cdn.net/docs/gfx/demo-config/index.webp?v=1788037328)

Dashboard menu: System > Demo config

**No-code routing**

## When to use no-code routing {#when-to-use-no-code-routing}

- You pick the receiving destinations in the Dashboard - no code required
- You can assign matching criteria in the Dashboard, e.g. routing only messages whose MSH-3 field is such and such
- All destinations receive the same message in a fan-out fashion, in their own format, e.g. FHIR destinations receive FHIR bundles
- If needed, Python code can be still used to work with the messages before they are delivered

```yaml
name: No-code routing
actors:
  - id: sender
    label: Sending application
  - id: channel
    label: Main HL7 Feed channel
  - id: ehr
    label: EHR
  - id: fhir
    label: FHIR server
  - id: archive
    label: REST archive
steps:
  - from: sender
    to: channel
    label: An ADT admission arrives on port 11553
    align: left
  - from: channel
    to: sender
    kind: return
    label: AA returns at once, not waiting for delivery
    align: left
  - from: channel
    to: ehr
    label: Delivered over MLLP, exactly as it arrived
  - from: channel
    to: fhir
    label: Converted to a FHIR bundle automatically
  - from: channel
    to: archive
    label: Sent as the body of an HTTP request
  - from: archive
    to: channel
    kind: return
    label: Each acknowledgment is stored in the audit log
```

## Setting up Zato {#setting-up-zato}

1. Install Zato through [Docker](https://zato.io/docs/admin/guide/install/docker.html) and open the Dashboard at [http://localhost:8183](http://localhost:8183)
2. Confirm the HL7 demo config is imported under System ▹ Demo config Show me where

## Accept and route incoming messages {#accept-and-route-incoming-messages}

In Zato, incoming messages are accepted through channels, e.g. MLLP channels, REST channels, and others.

So, with the Dashboard open, go to Connections ▹ Channels ▹ HL7 ▹ MLLP, click Create a new MLLP channel and complete the wizard:

1. **Name:** Main HL7 Feed
2. **Default:** on - a default channel receives every message that no other channel matches
3. On the second step, open **Incoming messages go to** and pick all three demo connections
4. Go to the review page and click **Save**

Clicking "Save" created a new MLLP channel which waits for incoming messages now on `localhost:11553`, and note that we're skipping many customization options you can set, they're documented in their own chapters.

## Send a test message {#send-a-test-message}

Send this ADT message to localhost:11553 with any MLLP client:

```hl7
MSH|^~\&|WARD_APP|EAST_WING|EHR|MAIN_HOSPITAL|20260315120000||ADT^A01^ADT_A01|MSG00001|P|2.9
EVN|A01|20260315120000
PID|1||12345^^^FAC^MR||SMITH^JOHN^A||19800115|M
PV1|1|I
```

What happens next is that the MLLP client receives `AA` back - indicating the channel accepted the message for delivery, and then the delivery to the destinations takes place.

Because the acknowledgment is returned before the message is delivered, it does not depend on how long delivery takes.

But how is such an incoming message transformed? In what format is it delivered to the destinations?

The answer depends on what kind of destination it is. If it's MLLP, it will be delivered as is. For FHIR, it will be automatically converted into a FHIR bundle. For REST, it will be sent as the body of an HTTP request, with the message exactly as it arrived.

You can click below to see both - the FHIR bundle the test message converts into and the HTTP request the REST archive receives:

> **The FHIR bundle (JSON)**
>
> ```json
> {
>   "resourceType": "Bundle",
>   "type": "transaction",
>   "entry": [
>     {
>       "request": {
>         "method": "POST",
>         "url": "MessageHeader"
>       },
>       "fullUrl": "urn:uuid:07c9c1d7-d6e4-534a-9ded-772f995bc215",
>       "resource": {
>         "resourceType": "MessageHeader",
>         "eventCoding": {
>           "system": "http://terminology.hl7.org/CodeSystem/v2-0003",
>           "code": "A01"
>         },
>         "source": {
>           "name": "WARD_APP",
>           "endpoint": "urn:zato:hl7v2:authority:WARD_APP"
>         },
>         "destination": [
>           {
>             "name": "EHR",
>             "endpoint": "urn:zato:hl7v2:authority:EHR"
>           }
>         ],
>         "focus": [
>           {
>             "reference": "urn:uuid:26e8a78d-2605-5c33-afcf-5b4409d14be5"
>           }
>         ]
>       }
>     },
>     {
>       "request": {
>         "method": "POST",
>         "url": "Patient"
>       },
>       "fullUrl": "urn:uuid:26e8a78d-2605-5c33-afcf-5b4409d14be5",
>       "resource": {
>         "resourceType": "Patient",
>         "identifier": [
>           {
>             "value": "12345",
>             "system": "urn:zato:hl7v2:authority:FAC",
>             "type": {
>               "coding": [
>                 {
>                   "system": "http://terminology.hl7.org/CodeSystem/v2-0203",
>                   "code": "MR"
>                 }
>               ]
>             }
>           }
>         ],
>         "name": [
>           {
>             "family": "SMITH",
>             "given": [
>               "JOHN",
>               "A"
>             ]
>           }
>         ],
>         "birthDate": "1980-01-15",
>         "gender": "male"
>       }
>     },
>     {
>       "request": {
>         "method": "POST",
>         "url": "Encounter"
>       },
>       "fullUrl": "urn:uuid:d52ca6a2-5f4c-51d3-871e-8f2f5777074c",
>       "resource": {
>         "resourceType": "Encounter",
>         "subject": {
>           "reference": "urn:uuid:26e8a78d-2605-5c33-afcf-5b4409d14be5"
>         },
>         "class": {
>           "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode",
>           "code": "IMP"
>         },
>         "status": "in-progress"
>       }
>     }
>   ],
>   "identifier": {
>     "value": "MSG00001"
>   },
>   "timestamp": "2026-03-15T12:00:00+00:00"
> }
> ```

> **The REST request (HTTP)**
>
> ```http
> POST /hl7/archive HTTP/1.1
> Host: 10.152.5.17
> Content-Type: text/plain
> Content-Length: 171
>
> MSH|^~\&|WARD_APP|EAST_WING|EHR|MAIN_HOSPITAL|20260315120000||ADT^A01^ADT_A01|MSG00001|P|2.9
> EVN|A01|20260315120000
> PID|1||12345^^^FAC^MR||SMITH^JOHN^A||19800115|M
> PV1|1|I
> ```

## Audit log and monitoring {#audit-log-and-monitoring}

Each channel has an [audit log](https://zato.io/docs/dev/healthcare/audit-log.html) populated with all the messages it receives, along with information what happened next.

This lets you see what actually took place, when, and why, giving you complete observability into the environment, and letting you answer questions about your interfaces.

## That's it, job well done {#thats-it-job-well-done}

And that's it really, that's all there is to it. You create a channel, select your destinations, and the platform does the rest. Naturally, there are many ways to customize the process, documented below, but in a nutshell, it's as easy as that.

**Python code routing**

## When to use Python code routing {#when-to-use-python-code-routing}

- Use it when the decision depends on what is in the message - the channel invokes your service with each message and your code selects its destination
- You can still assign routing criteria and other options in the Dashboard
- Python code can transform the messages into any format you need, with complete control over where each one is delivered

```yaml
name: Python code routing
actors:
  - id: sender
    label: Sending application
  - id: channel
    label: HL7 Message Router channel
  - id: service
    label: Your routing service
  - id: ehr
    label: Receiving EHR
steps:
  - from: sender
    to: channel
    label: An ORU result arrives on port 11553
    align: left
  - from: channel
    to: service
    label: Invoked with a typed Python object
  - from: service
    to: ehr
    label: Your code forwards the result
    items:
      - The message code from MSH-9 determines the target
      - self.mllp sends over MLLP
  - from: ehr
    to: service
    kind: return
    label: result.ack_code is the acknowledgment code
  - from: service
    to: channel
    kind: return
    label: Processing completes
  - from: channel
    to: sender
    kind: return
    label: AA returns
    align: left
```

## Setting up Zato {#setting-up-zato_1}

1. Install Zato through [Docker](https://zato.io/docs/admin/guide/install/docker.html) and log into the Dashboard at [http://localhost:8183](http://localhost:8183)
2. Make sure the HL7 demo config is imported under System ▹ Demo config Show me where

## Write the routing service {#write-the-routing-service}

In Zato, incoming messages are handled by services - Python classes that a channel invokes with each message it accepts.

So, with the Dashboard open, go to the IDE, create a file called `hl7_api.py`, paste the code below and click **Deploy**:

```python
# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

# #####################################################################
# #####################################################################

if 0:
    from zato.hl7v2.base import HL7Message

# #####################################################################
# #####################################################################

class MessageHandler(Service):
    """ Routes each incoming HL7 message by its type.
    """

    def handle(self) -> 'None':

        # The decision needs one field - the message code from MSH-9 ..
        msg:'HL7Message' = self.request.input
        message_type = msg.msh.message_type.message_code

        # .. ORU results are forwarded to the EHR ..
        if message_type == 'ORU':
            connection_name = 'demo.hl7.forward.ehr'
            result = self.mllp[connection_name].send(msg)
            self.logger.info(f'EHR replied {result.ack_code}')

        # .. and everything else stays where it is.
        else:
            self.logger.info(f'Not forwarding {message_type}')
```

Once deployed, the service is available under the name `hl7-api.message-handler` - derived from the file and class names - and what it needs now is a channel to invoke it.

## Create the channel {#create-the-channel}

Go to Connections ▹ Channels ▹ HL7 ▹ MLLP, click Create a new MLLP channel and complete the wizard, this time picking a service instead of destinations:

1. **Name:** HL7 Message Router
2. On the second step, open **Service invoked** and pick `hl7-api.message-handler`
3. Go to the review page and click **Save**

[Video: New MLLP channel](https://zatosource-production.b-cdn.net/docs/gfx/channels/hl7-mllp-create.webm?v=1791648491)

Dashboard menu: Connections > Channels > HL7 > MLLP

Clicking "Save" created a channel that waits for incoming messages on `localhost:11553` and invokes your service with each one. A channel with no [match rules](https://zato.io/docs/dev/healthcare/hl7v2/mllp/config.html#match-criteria) matches every message, so the router receives everything that arrives - even if the no-code channel from the other tab exists too, because a default channel only gets what nothing else matched.

## Send a test message {#send-a-test-message_1}

Send this ADT admission to localhost:11553 with any MLLP client:

```hl7
MSH|^~\&|WARD_APP|EAST_WING|EHR|MAIN_HOSPITAL|20260315120000||ADT^A01^ADT_A01|MSG00001|P|2.9
EVN|A01|20260315120000
PID|1||12345^^^FAC^MR||SMITH^JOHN^A||19800115|M
PV1|1|I
```

The client receives `AA` back and the server log confirms your code made its decision:

```text
INFO - Not forwarding ADT
```

But an admission is what this service leaves alone, so how about a message it does forward? Send a lab result next:

```hl7
MSH|^~\&|LAB_APP|EAST_WING|EHR|MAIN_HOSPITAL|20260315124500||ORU^R01^ORU_R01|MSG00002|P|2.9
PID|1||12345^^^FAC^MR||SMITH^JOHN^A||19800115|M
OBR|1|845439^LAB||1554-5^GLUCOSE^LN|||20260315123000
OBX|1|NM|1554-5^GLUCOSE^LN||182|mg/dL|70-105|H|||F
```

This time your code sends the message on to `demo.hl7.forward.ehr` and logs the acknowledgment the receiving system replied with:

```text
INFO - EHR replied AA
```

## And that's all it takes {#and-thats-all-it-takes}

You deploy a service, point a channel at it, and where each message goes is a decision your Python code makes. The audit log and everything else from no-code routing remain available with code routing, and the chapters below cover what more channels can do.

## What you built {#what-you-built}

- A channel that delivers an incoming HL7 feed to destinations picked in the Dashboard, without code
- A routing service that reads one MSH field and routes each message accordingly
- A verified path for each - the client's acknowledgment, the audit log's flow view and the server log

## What next {#what-next}

- [Connect your AI](https://zato.io/docs/ai/connect-your-copilot.html) to ask more questions about Zato and to build your interfaces
- Play around with the demo config, point it to your own destinations, add more input matching conditions
- Work in Python on more advanced transformations and routing conditions
- At any time, you can go to System ▹ Demo config, slide the slider off to remove the demo config, and slide it back on to import it again, so don't worry about breaking anything

## What else channels can do {#what-else-channels-can-do}

- [Routing to selected destinations only](https://zato.io/docs/dev/healthcare/hl7v2/mllp/index.html) - A service picks which destinations receive a given message
- [Match rules](https://zato.io/docs/dev/healthcare/hl7v2/mllp/config.html#match-criteria) - Select a channel by message type, sending application or facility
- [REST bridge](https://zato.io/docs/dev/healthcare/hl7v2/mllp/config.html#rest-options) - Receive the same messages over HTTP
- [Typed parsing and navigation](https://zato.io/docs/dev/healthcare/hl7/v2/index.html) - `msg.pid.patient_name.family_name` instead of positional offsets
- [Batch files](https://zato.io/docs/dev/healthcare/hl7/v2/batch/index.html) - Files wrapped in FHS and BHS headers
- [Acknowledgments](https://zato.io/docs/dev/healthcare/hl7v2/mllp/acknowledgments.html) - What AA, AE and AR mean and where error details appear
- [Deduplication, tolerance and encoding](https://zato.io/docs/dev/healthcare/hl7v2/mllp/config.html#tolerance-and-message-fixups) - Handle duplicated messages and senders that deviate from the standard
- [Enmasse YAML](https://zato.io/docs/admin/enmasse-reference.html#hl7-mllp-channels) - The same channels defined declaratively, in version control
- [The audit log](https://zato.io/docs/dev/healthcare/audit-log.html) - Every message with its acknowledgment, searchable by patient identifier
