What it does
A lab system (LIS) sends results to the EHR and wants to know at once whether each one was filed. Two teams also act on results: the on-call pager on critical values, and infection control on positive cultures. The route results is pass-through, with the EHR as its primary destination: the LIS waits for the EHR's own ACK. The pager and infection control get a copy of what the EHR accepted, through queues of their own.
| Destination | Gets | Filter |
|---|---|---|
ehr (primary) | every result, while the LIS waits; its ACK is the LIS's answer | none: a primary gets every message |
pager | results with a critical value, once the EHR filed them | an order with OBX-8 HH or LL |
infection-control | positive cultures, once the EHR filed them | a BCX or UCX order whose own OBX-8 is A |
Both filters use a segment group: an OBR and the segments after it, up to the next order's ORC. The conditions must hold in the same order, so a negative blood culture next to an abnormal urinalysis isn't a positive culture. You need the gateway image 0.5.0 or later.
The sample is in 12-results-to-ehr-and-alerts in the examples repository.
Diagrams

architecture.mmd
data-flow.mmd
sequence.mmdThe configuration
12-results-to-ehr-and-alerts/gateway.toml
# Sample 12: lab results to the EHR, while the LIS waits; critical results and positive cultures copied
# to the people who act on them
#
# Source The lab system (LIS) sends ORU^R01 results over MLLP (:7890).
# Pass-through The EHR is the primary destination: the LIS waits for the EHR's own ACK, so it
# knows the result was filed (or why not) and resends what wasn't.
# Copies What the EHR accepted (AA) is copied, through queues of their own, to:
# pager critical results (OBX-8 HH or LL), with the order they belong to
# infection-control positive cultures: a culture order (OBR-4.1 BCX or UCX) whose own result
# is abnormal (OBX-8 A)
#
# The copies' filters use segment groups: an OBR and the segments after it, up to the next ORC, so the
# conditions are checked against one order and its own results, not the whole message.
node_id = "gateway-1"
[admin]
listen = "0.0.0.0:8080"
token = "${HL7EDGE_ADMIN_TOKEN}"
allow_payload_view = true
# The copies wait in queues here.
[store]
backend = "embedded"
path = "/var/lib/hl7-edge-gateway"
[[route]]
name = "results"
mode = "passthrough"
source = { type = "mllp", listen = "0.0.0.0:7890" }
[[route.destinations]]
name = "ehr"
type = "mllp"
address = "ehr:6661"
primary = true # its ACK goes back to the LIS; a primary gets every message, so it has no filter
[[route.destinations]]
name = "pager"
type = "http"
url = "http://pager:8000/alerts"
headers = { Authorization = "Bearer ${NOTIFY_API_TOKEN}" }
store_and_forward = { retry = { initial = "2s", max = "30s", multiplier = 2.0 } }
# One order (OBR, up to the next ORC) with a critical result.
filter = { message_type = ["ORU^R01"], group = { segment = "OBR", until = ["ORC"], fields = { "OBX-8" = ["HH", "LL"] } } }
[[route.destinations]]
name = "infection-control"
type = "http"
url = "http://infection-control:8000/alerts"
headers = { Authorization = "Bearer ${NOTIFY_API_TOKEN}" }
store_and_forward = { retry = { initial = "2s", max = "30s", multiplier = 2.0 } }
# A culture order whose own result is abnormal. Without the group, a negative culture next to an
# abnormal urinalysis would match: OBR-4.1 BCX in one order, OBX-8 A in another.
[route.destinations.filter]
message_type = ["ORU^R01"]
[route.destinations.filter.group]
segment = "OBR"
until = ["ORC"]
fields = { "OBR-4.1" = ["BCX", "UCX"], "OBX-8" = ["A"] }
12-results-to-ehr-and-alerts/compose.yaml — the pager and infection control are one stand-in, run twice
# Sample 12: lab results to the EHR (pass-through), copied to the on-call pager and infection control.
# docker compose up -d
# docker compose run --rm send … send HL7 files to the gateway over MLLP (as the LIS)
name: hl7edge-sample-12
services:
gateway:
image: lambogenius/hl7-edge-gateway:${GATEWAY_VERSION:-0.5.0}
env_file: .env
ports:
- "7890:7890" # results from the LIS (MLLP)
- "8080:8080" # admin console
volumes:
- ./gateway.toml:/etc/hl7-edge-gateway/hl7-edge-gateway.toml:ro
- queue:/var/lib/hl7-edge-gateway
depends_on: [ehr, pager, infection-control]
# The EHR's results interface, over MLLP.
ehr:
image: python:3.12-slim
command: ["python", "-u", "/app/receiver.py"]
volumes: ["./ehr:/app:ro"]
# Two notification services, the same stand-in: POST /control/down and /control/up switch an outage.
pager:
image: python:3.12-slim
command: ["python", "-u", "/app/app.py"]
env_file: .env
environment: { SERVICE: pager }
ports: ["8001:8000"]
volumes: ["./notify:/app:ro"]
infection-control:
image: python:3.12-slim
command: ["python", "-u", "/app/app.py"]
env_file: .env
environment: { SERVICE: infection-control }
ports: ["8002:8000"]
volumes: ["./notify:/app: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", "./filters:/filters:ro", "./gateway.toml:/gateway.toml:ro"]
profiles: ["tools"]
volumes:
queue:
Step by step
- 1Start it.
cd 12-results-to-ehr-and-alerts cp .env.example .env # add your license docker compose up -dIn the console at
http://localhost:8080, the routeresultsis pass-through:ehris marked primary ("while the sender waits"), and each copy has a row with its filter, state and queue. - 2The LIS sends four results.
docker compose run --rm send --to gateway:7890 /messages/oru-normal.hl7 /messages/oru-critical-potassium.hl7 \ /messages/oru-positive-culture.hl7 /messages/oru-negative-culture-abnormal-urine.hl7 1 2 ms MSA|AA|RES-1201|filed 2 1 ms MSA|AA|RES-1202|filed 3 1 ms MSA|AA|RES-1203|filed 4 1 ms MSA|AA|RES-1204|filed docker compose logs pager infection-control pager-1 | pager: RES-1202 patient 100142: GLU: GLU 102 mg/dL H; K: K 6.9 mmol/L HH infection-control-1 | infection-control: RES-1203 patient 100143: BCX: ORG STAAUR^Staphylococcus aureus AEach ACK is the EHR's own ("filed"). RES-1202's potassium is critical, so the pager gets it. RES-1203 is a positive blood culture. RES-1201 is normal, and RES-1204 has a blood culture with no growth next to an abnormal urinalysis: no culture is positive.

A pass-through route: the EHR answers while the LIS waits, and each copy has its filter and queue. - 3See why RES-1204 isn't a positive culture.
The
filtercommand checks a filter against messages and sends nothing. The same conditions, without and with a group:12-results-to-ehr-and-alerts/filters/positive-culture-group.toml# Both conditions in the same order: an OBR and the segments after it, up to the next ORC. [group] segment = "OBR" until = ["ORC"] fields = { "OBR-4.1" = ["BCX", "UCX"], "OBX-8" = ["A"] }docker compose run --rm --entrypoint hl7-edge-gateway send \ filter -f @/filters/positive-culture-flat.toml /messages/oru-negative-culture-abnormal-urine.hl7 RES-1204 ORU^R01 MATCH yes OBR-4.1 BCX|UCX found BCX, UA yes OBX-8 A found N, A, H docker compose run --rm --entrypoint hl7-edge-gateway send \ filter -f @/filters/positive-culture-group.toml /messages/oru-negative-culture-abnormal-urine.hl7 RES-1204 ORU^R01 NO MATCH no group OBR until ORC: at least one where no OBR #1 yes OBR-4.1 BCX|UCX found BCX no OBX-8 A found N no OBR #2 no OBR-4.1 BCX|UCX found UA yes OBX-8 A found A, HWithout the group, BCX and A are both somewhere in the message. With it, they must be in the same order. The console's Filter tester shows the same for every destination of the route:

The Filter tester: each group, with what was found in it. - 4A critical result for a patient the EHR doesn't know.
docker compose run --rm send --to gateway:7890 /messages/oru-critical-unknown-patient.hl7 1 1 ms MSA|AR|RES-1205|patient not found: 200512The LIS gets the EHR's AR. RES-1205 matches the pager's filter, but it isn't copied: copies are made only of what the primary accepted, so nobody is paged for a result that isn't in the chart.
- 5The EHR is down.
docker compose stop ehr docker compose run --rm send --to gateway:7890 /messages/oru-critical-glucose.hl7 1 4001 ms MSA|AE|RES-1206|ehr:6661: connection failed: … docker compose start ehr docker compose run --rm send --to gateway:7890 /messages/oru-critical-glucose.hl7 1 2 ms MSA|AA|RES-1206|filedThe LIS gets AE and resends. Only the resend, once filed, is copied to the pager.
- 6The pager is down.
curl -X POST http://localhost:8001/control/down docker compose run --rm send --to gateway:7890 /messages/oru-critical-during-pager-outage.hl7 1 2 ms MSA|AA|RES-1207|filedRES-1207 has a critical potassium and a positive urine culture. The LIS gets AA at once, and infection control gets its copy; the pager's copy waits in its queue and is retried.

The pager is down: its copy of RES-1207 waits in its queue; the EHR and infection control aren't held up. curl -X POST http://localhost:8001/control/up, and the pager catches up.
The LIS waits for the EHR, so set its ACK timeout above the gateway's request_timeout for the EHR (30 s by default). OBX-8 flags differ between labs; list the ones yours sends, and check the filters on real results with hl7-edge-gateway filter -c before going live.
Or run every step with ./demo.sh (Linux, macOS, Git Bash) or ./demo.ps1 (PowerShell). Clean up with docker compose down -v. More in Filters and Several sources and destinations.