Skip to content
LambogeniusDocs

HL7 Edge Gateway

Results to the EHR, alerts by order

A pass-through route: the lab system waits for the EHR's own ACK, and what the EHR accepted is copied to a pager and infection control by segment-group filters.

Runs onWindows · Linux · macOS · DockerFree developer license for test traffic. Production traffic needs a paid license.

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.

DestinationGetsFilter
ehr (primary)every result, while the LIS waits; its ACK is the LIS's answernone: a primary gets every message
pagerresults with a critical value, once the EHR filed theman order with OBX-8 HH or LL
infection-controlpositive cultures, once the EHR filed thema 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 diagram of the 12-results-to-ehr-and-alerts sample
Architecture. What runs where, and how the systems connect. Source: architecture.mmd
Data flow diagram of the 12-results-to-ehr-and-alerts sample
Data flow. What happens to a message at each step, including the failure paths. Source: data-flow.mmd
Sequence diagram of the 12-results-to-ehr-and-alerts sample
Sequence. One run of the sample, message by message. Source: sequence.mmd

The 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

  1. 1
    Start it.
    cd 12-results-to-ehr-and-alerts
    cp .env.example .env            # add your license
    docker compose up -d

    In the console at http://localhost:8080, the route results is pass-through: ehr is marked primary ("while the sender waits"), and each copy has a row with its filter, state and queue.

  2. 2
    The 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 A

    Each 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.

    Route results, pass-through, with ehr primary (while the sender waits) and rows for pager and infection-control with their group filters
    A pass-through route: the EHR answers while the LIS waits, and each copy has its filter and queue.
  3. 3
    See why RES-1204 isn't a positive culture.

    The filter command 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, H

    Without 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:

    Filter tester on route results with RES-1204: ehr gets it; pager and infection-control don't, with each OBR group's conditions
    The Filter tester: each group, with what was found in it.
  4. 4
    A 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: 200512

    The 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.

  5. 5
    The 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|filed

    The LIS gets AE and resends. Only the resend, once filed, is copied to the pager.

  6. 6
    The 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|filed

    RES-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.

    pager retrying x6 with 1 queued and HTTP 503, while ehr has delivered 5 and infection-control is idle with 2 delivered
    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.

Before going live

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.