EDIFACT in healthcare tutorial - Receive lab results and send letters
Receive laboratory results, route clinical messages by type and send letters to GPs - healthcare EDIFACT from Python services.
With Zato, you can receive, route and send healthcare EDIFACT from Python services, and this tutorial will show you how - from a lab result coming in to a letter to a GP going out.
The example messages come from the Dutch healthcare dialect that ships with the platform, and every step works the same with any dialect you define.
Setting up Zato
- Install Zato through Docker
- In your browser, go to http://localhost:8183 and log into the Dashboard
Receive a lab result
Write the service
In Zato, incoming messages are handled by services - Python classes that a channel invokes with each message it accepts.
With the Dashboard open, go to the IDE, create a file called edifact_api.py, paste the code below and click Deploy:
# -*- coding: utf-8 -*-
# Zato
# Importing a dialect package registers its message classes -
# here, the Dutch healthcare dialect shipped as an example.
import zato.edifact.nl
from zato.edifact import parse_edifact
from zato.server.service import Service
# #####################################################################
# #####################################################################
class LabResults(Service):
""" Receives laboratory results sent as EDIFACT interchanges.
"""
def handle(self) -> 'None':
# The wire text, as the channel delivered it ..
raw = self.request.input
# .. one call parses the envelope and the message in it ..
interchange = parse_edifact(raw)
msg = interchange.message
# .. the UNB header names the sending laboratory ..
sender = interchange.header.sender.identification
message_type = msg.unh.identifier.message_type
self.logger.info(f'Received {message_type} from {sender}')
# .. and each specimen carries its determinations.
for material in msg.materials:
for determination in material.determinations:
value = f'{determination.result} {determination.unit}'
self.logger.info(f'{determination.determination}: {value}')
The moment it deploys, the service is available under the name edifact-api.lab-results - derived from the file and class names - and what it needs now is a channel to invoke it.
Create the channel
The interchange text arrives over the carrier the trading partners agreed on - a mailbox network, a file drop or an HTTP endpoint - and here a REST channel accepts it. Mailbox polling and file drops are covered in transport patterns.
Go to Connections ▹ Channels ▹ REST, click Create a new REST channel and fill in the form:
- Name: EDIFACT Inbox
- URL path: /edifact
- Service: edifact-api.lab-results
- Security: No security definition
- Click OK
Send a test message
Save this MEDLAB interchange - one blood specimen with three hematology determinations - as lab-result.edi:
UNB+UNOA:1+500000101+500000201+260831:0930+2601'
UNH+2601+MEDLAB:1'
ZKH+Streeklab Rijnmond+Wytemaweg:80::Rotterdam:3015CN+?+31107033800'
PID+1975:03:18+V+Dijk:van der:Peters:de::M.++BSN999990019'
AFD+Klinische chemie'
ARA+Dr. E. Verhoeven'
DET+26:08:30+14:15'
IDE+C+26082254+bloed'
SEC+HEMATOLOGIE'
BEP+1+Hemoglobine+8.6++mmol/l+N+8.5+11.0'
BEP+1+Hematocriet+0.42++l/l+N+0.40+0.50'
BEP+1+Leukocyten+12.4++10*9/l+H+4.0+10.0'
UNT+12+2601'
UNZ+1+2601'
Post it to the channel:
The server log shows what your code read - the sender from UNB and every determination with its value and unit:
INFO - Received MEDLAB from 500000101
INFO - Hemoglobine: 8.6 mmol/l
INFO - Hematocriet: 0.42 l/l
INFO - Leukocyten: 12.4 10*9/l
Note that the ?+ escape in the laboratory's phone number was unescaped for you, and the MEDLAB:1 identity in UNH is what resolved the message to its typed class.
Navigate messages with typed access
Every segment the message class declares is an attribute, and so is every element and component - demographics and laboratory details read like plain Python:
msg = interchange.message
# Composites have typed components
family_name = msg.pid.patient_name.married_name # 'Dijk'
born_year = msg.pid.date_of_birth.year # '1975'
# The sending laboratory and its department
lab = msg.hospital.institution_name # 'Streeklab Rijnmond'
department = msg.department.department # 'Klinische chemie'
Repeating structures are lists - a lab result has one group per specimen, each with its own determinations, and the reference ranges travel with every value:
for material in msg.materials:
for determination in material.determinations:
# The laboratory flagged this value as above range
if determination.normality_indicator == 'H':
name = determination.determination
value = f'{determination.result} {determination.unit}'
limit = determination.upper_limit
self.logger.info(f'Above range: {name} {value}, upper limit {limit}')
The test message has one such value, and the log points it out:
The full access model is described in EDIFACT field access.
Route by message type
Lab results are not the only traffic - letters, radiology reports and specialist letters travel the same way, and the UNH identity tells them apart, so a routing service reads it and passes each message to its handler:
class Router(Service):
""" Passes each message of an interchange to its handler.
"""
def handle(self) -> 'None':
# Message types and the services that handle them ..
handlers = {
'MEDLAB': 'edifact-api.lab-results',
'MEDVRI': 'edifact-api.letters',
}
# .. one interchange may carry several messages ..
interchange = parse_edifact(self.request.input)
for msg in interchange.messages:
# .. the UNH identity decides where each one goes ..
message_type = msg.unh.identifier.message_type
if service_name := handlers.get(message_type):
_ = self.invoke(service_name, msg=msg)
# .. and unknown types are still parsed into fully navigable messages.
else:
self.logger.info(f'Skipping {message_type}')
Round trips are byte-exact
The systems that receive healthcare EDIFACT are often decades old and strict about their input - an import job that has parsed the same layout since the nineties rejects an interchange whose separators changed or whose empty elements moved. For this reason, a parsed interchange serializes back to its exact wire bytes, and a message a handler passes on arrives as the laboratory produced it. When one field has to change, a corrected patient number for instance, assign to it and only that segment is re-serialized:
interchange = parse_edifact(raw)
# The wire text, reproduced byte for byte
wire_text = interchange.serialize()
# One assignment, one changed segment - the rest stays as received
msg = interchange.message
msg.pid.sex = 'M'
modified = interchange.serialize()
Send a letter to a GP
Messages go the other way too - a treatment update to the patient's GP is a MEDVRI message, built with the same typed classes that parsing uses.
The letter needs a way out first - an outgoing REST connection pointed at the mailbox provider. Go to Connections ▹ Outgoing ▹ REST, click Create a new outgoing REST connection and fill in the form:
- Name: EDI Mailbox
- Host: https://mailbox.example.com
- URL path: /messages
- Security: No security definition
- Click OK
Now add this service to edifact_api.py and deploy it:
# Zato
from zato.edifact.nl import MEDVRI
from zato.edifact.nl.segments import TXT
# #####################################################################
# #####################################################################
class SendLetter(Service):
""" Builds a letter to a GP and sends it to the mailbox network.
"""
def handle(self) -> 'None':
# A new, empty letter ..
letter = MEDVRI()
# .. its UNH header carries the reference and the message identity ..
letter.unh.reference_number = '8001'
letter.unh.identifier.message_type = 'MEDVRI'
letter.unh.identifier.version = '1'
# .. the sending practitioner and institution ..
letter.sender.person_name = 'E. Vermeer'
letter.sender.institution_name = 'GGZ De Linde'
# .. the date the letter was written ..
letter.det.date.year = '26'
letter.det.date.month = '08'
letter.det.date.day = '31'
# .. the patient it concerns ..
letter.pid.date_of_birth.year = '1975'
letter.pid.date_of_birth.month = '03'
letter.pid.date_of_birth.day = '18'
letter.pid.sex = 'V'
letter.pid.patient_name.married_name = 'Dijk'
letter.pid.patient_name.married_name_prefix = 'van der'
# .. the letter's text, one TXT segment per line ..
line_1 = TXT()
line_1.text = 'Dear colleague'
line_2 = TXT()
line_2.text = 'Your patient has been in our care for six months now'
letter.text = [line_1, line_2]
# .. the UNT trailer closes the message ..
letter.unt.segment_count = '7'
letter.unt.reference_number = '8001'
# .. serialization produces the wire text ..
wire_text = letter.serialize()
# .. the connection created a moment ago is looked up by name ..
conn = self.rest['EDI Mailbox']
# .. and one POST delivers the letter to the mailbox.
_ = conn.post(self.cid, wire_text)
self.logger.info(f'Sent letter {letter.unh.reference_number}')
This is the wire text the mailbox receives:
UNH+8001+MEDVRI:1'
GGA+E. Vermeer++GGZ De Linde'
DET+26:08:31'
PID+1975:03:18+V+Dijk:van der'
TXT+Dear colleague'
TXT+Your patient has been in our care for six months now'
UNT+7+8001'
Note that unlike the lab result from earlier, this wire text has no UNB-UNZ envelope - mailbox networks often carry bare messages and parse_edifact accepts both forms.
Convert between formats
The lab result received earlier does not have to stay EDIFACT - to post it to a REST API or store it in a reporting database, convert it to a dict or JSON:
interchange = parse_edifact(raw)
data = interchange.to_dict()
json_text = interchange.to_json(indent=2)
# Skip empty elements for compact output
compact = interchange.to_json(include_empty=False)
And that's the whole loop
A REST channel accepts the interchange, typed access reads it, and the same classes build what goes back out. The chapters below cover the details whenever you need more.
What you built
- A REST channel that accepts EDIFACT interchanges and invokes your service with each one
- Typed access - patient demographics, specimens and determinations as named Python attributes
- A router that reads the UNH identity and passes each message type to its handler
- A MEDVRI letter built from typed classes, serialized and sent out over an outgoing connection
- Byte-exact round trips - forwarded messages arrive as the sender produced them
- JSON conversion -
to_dictandto_jsonfor REST APIs, queues and reporting stores
What next
- Connect your AI to ask more questions about Zato and to build your interfaces
- Point the channel at your own dialect and transports - define the message classes and everything on this page works unchanged
- Receive HL7 v2 alongside EDIFACT with the HL7 MLLP tutorial
Where to go next
| Feature | What it does |
|---|---|
| The EDIFACT engine | Parsing, building and serialization in one place |
| Message parsing | Envelopes, separators and round trips in depth |
| Field access | Typed segments, composites and repeating groups |
| Dialects and profiles | Define and register your own message types |
| Transport patterns | Mailboxes, file drops and REST carriers |
| EDIFACT in healthcare | Where EDIFACT remains in use in healthcare |
Schedule a meaningful demo
Book a demo with an expert who will help you build meaningful systems that match your ambitions
"We evaluated 12 integration platforms and Zato was the only one to score 100%."
Philip Zuñiga, Assistant Professor, University of the Philippines