What it does
ADT events come from MEDITECH over MLLP, and from a partner clinic's registration system over HTTP. Three systems need them, so the route adt-feed has two sources and three destinations, each with a queue of its own:
| Destination | Gets | Through |
|---|---|---|
lab | ADT^A01, A04 and A08 for inpatients and emergency patients (PV1-2 is I or E) | the lab's REST API, with a bearer token |
his | every event | MLLP; a patient the HIS doesn't know (AR) goes to this destination's dead letters |
archive | every event | SFTP, as HL7 batch files of up to 5 events, closed after 20 s at most |
When the lab API is down, the HIS and the archive keep getting events, and the lab catches up when it is back. You need the gateway image 0.5.0 or later.
The sample is in 11-one-feed-many-systems in the examples repository.
Diagrams

architecture.mmd
data-flow.mmd
sequence.mmdThe configuration
11-one-feed-many-systems/gateway.toml
# Sample 11: one ADT feed to three systems, from two senders
#
# Sources MEDITECH sends ADT over MLLP (:7890); a partner clinic's registration system posts
# ADT over HTTP (:8443, with its API key). Both feed the same route, in arrival order.
# Destinations Each has a queue of its own, so one that is down holds up no other.
# lab The lab's REST API wants only inpatient and emergency admissions: ADT^A01, A04 and
# A08 with patient class (PV1-2) I or E.
# his The downstream HIS gets every ADT event over MLLP.
# archive Every event also goes to the archive's SFTP server, in HL7 batch files.
#
# Store-and-forward: the senders get AA once the message is stored for every destination it goes to.
node_id = "gateway-1"
[admin]
listen = "0.0.0.0:8080"
token = "${HL7EDGE_ADMIN_TOKEN}"
allow_payload_view = true
[store]
backend = "embedded"
path = "/var/lib/hl7-edge-gateway"
[[route]]
name = "adt-feed"
mode = "store_and_forward"
sources = [
{ type = "mllp", listen = "0.0.0.0:7890" },
# In production put HTTPS (tls = { cert = …, key = … }) in front of the partner.
{ type = "http", listen = "0.0.0.0:8443", path = "/hl7/adt", auth = { type = "api_key", keys = { lakeside-clinic = "${PARTNER_API_KEY}" } } },
]
[route.store_and_forward]
retry = { initial = "2s", max = "30s", multiplier = 2.0 }
[[route.destinations]]
name = "lab"
type = "http"
url = "http://lab-api:8000/hl7/admissions"
headers = { Authorization = "Bearer ${LAB_API_TOKEN}" }
# Every field named must match; the values of one field are alternatives. PV1-2 is the patient class.
filter = { message_type = ["ADT^A01", "ADT^A04", "ADT^A08"], fields = { "PV1-2" = ["I", "E"] } }
[[route.destinations]]
name = "his"
type = "mllp"
address = "his:6661"
# A patient the HIS doesn't know (AR) goes to this destination's dead letters; the others are unaffected.
store_and_forward = { on_reject = "dead_letter", retry = { initial = "2s", max = "30s", multiplier = 2.0 } }
[[route.destinations]]
name = "archive"
type = "sftp"
host = "sftp"
username = "hospital"
private_key = "/keys/gateway_ed25519"
known_hosts = "/keys/known_hosts"
directory = "inbox"
batch_format = "hl7_batch"
file_name = "adt_{timestamp}.hl7"
batch = { max_messages = 5, max_wait = "20s" }
11-one-feed-many-systems/compose.yaml — the lab API is sample 01's, and the SFTP keys are made as in sample 10
# Sample 11: one ADT feed (MEDITECH and a partner clinic) to three systems: a lab API, a downstream
# HIS and an SFTP archive.
# docker compose up -d
# docker compose run --rm send … send HL7 files to the gateway over MLLP (as MEDITECH)
name: hl7edge-sample-11
services:
gateway:
image: lambogenius/hl7-edge-gateway:${GATEWAY_VERSION:-0.5.0}
env_file: .env
ports:
- "7890:7890" # ADT from MEDITECH (MLLP)
- "8443:8443" # ADT from the partner clinic (HTTP POST /hl7/adt)
- "8080:8080" # admin console
volumes:
- ./gateway.toml:/etc/hl7-edge-gateway/hl7-edge-gateway.toml:ro
- keys:/keys:ro
- queue:/var/lib/hl7-edge-gateway
depends_on: [lab-api, his, sftp]
# The lab's REST API (the same stand-in as sample 01): POST /control/down and /control/up switch an outage.
lab-api:
image: python:3.12-slim
command: ["python", "-u", "/app/app.py"]
env_file: .env
ports: ["8000:8000"]
volumes: ["./lab-api:/app:ro"]
# The downstream HIS, over MLLP.
his:
image: python:3.12-slim
command: ["python", "-u", "/app/receiver.py"]
volumes: ["./his:/app:ro"]
# The archive's SFTP server, and the keys the gateway signs in with (the same setup as sample 10).
sftp:
image: atmoz/sftp:alpine
command: ["hospital:${LAB_SFTP_PASSWORD}:1001:100:inbox"]
ports: ["2222:22"]
volumes:
- keys:/keys:ro
- ./sftp/install-keys.sh:/etc/sftp.d/install-keys.sh:ro
depends_on:
keys: { condition: service_completed_successfully }
keys:
image: alpine:3.20
command: ["sh", "/scripts/make-keys.sh"]
volumes:
- keys:/keys
- ./sftp:/scripts:ro
# Tools, not started by `up`.
send:
image: lambogenius/hl7-edge-gateway:${GATEWAY_VERSION:-0.5.0}
entrypoint: ["hl7-edge-gateway", "send"]
volumes: ["./messages:/messages:ro"]
profiles: ["tools"]
volumes:
keys:
queue:
Step by step
- 1Start it.
cd 11-one-feed-many-systems cp .env.example .env # add your license docker compose up -dIn the console at
http://localhost:8080, the routeadt-feedshows its two sources, and a row for each destination with its filter, state and queue. - 2MEDITECH sends four ADT events.
docker compose run --rm send --to gateway:7890 /messages/adt-a01-inpatient.hl7 /messages/adt-a04-emergency.hl7 \ /messages/adt-a08-outpatient.hl7 /messages/adt-a03-discharge.hl7 docker compose logs his his adt: AA ADT-9001 ADT^A01^ADT_A01 patient 100131 (filed) his adt: AA ADT-9002 ADT^A04^ADT_A01 patient 100132 (filed) his adt: AA ADT-9003 ADT^A08^ADT_A01 patient 100133 (filed) his adt: AA ADT-9004 ADT^A03^ADT_A01 patient 100131 (filed) curl http://localhost:8000/received # the lab: ADT-9001 and ADT-9002 onlyThe HIS gets all four, in order. The lab gets the inpatient A01 and the emergency A04: ADT-9003 is an outpatient (PV1-2 is O), and ADT-9004 is an A03.
- 3The partner clinic posts an event over HTTP.
curl -H "X-API-Key: <PARTNER_API_KEY from .env>" -H "Content-Type: x-application/hl7-v2+er7" \ --data-binary @messages/adt-a04-partner.hl7 http://localhost:8443/hl7/adt MSH|^~\&|HL7EDGE|GATEWAY|LAKESIDE_REG|LAKESIDE_CLINIC|…||ACK^A04|…|T|2.5.1 MSA|AA|ADT-9005ADT-9005 joins the same queues, after MEDITECH's events: the HIS and the lab get it, and it is the fifth event in the archive's batch file, which closes it.

One route, two sources, three destinations: each destination shows its filter, state and queue. - 4The lab API goes down.
curl -X POST http://localhost:8000/control/down docker compose run --rm send --to gateway:7890 /messages/adt-a01-during-outage.hl7The HIS and the archive get ADT-9006 at once; it waits in the lab's queue only, and the lab retries.

The lab is down: ADT-9006 waits in its queue, while the HIS and the archive already have it. curl -X POST http://localhost:8000/control/up, and the lab catches up. - 5
- 6See why a destination gets an event, or not.
The
filtercommand checks the configuration's filters and shows, condition by condition, where each message would go. It sends nothing, and doesn't need.env:docker compose run --rm -v ./gateway.toml:/gateway.toml:ro --entrypoint hl7-edge-gateway send \ filter -c /gateway.toml /messages/adt-a01-inpatient.hl7 /messages/adt-a08-outpatient.hl7 /messages/adt-a08-outpatient.hl7 ADT-9003 ADT^A08 route adt-feed: to his, archive lab doesn't get it yes message_type ADT^A01|ADT^A04|ADT^A08 found ADT^A08 no PV1-2 I|E found O his gets it (no filter) archive gets it (no filter)The console's Filter tester does the same for pasted messages. See Filters.
Or run every step with ./demo.sh (Linux, macOS, Git Bash) or ./demo.ps1 (PowerShell). Clean up with docker compose down -v. More in Several sources and destinations.
