lumnik. / scenarios All scenarios · Français

HL7 v2: the analyser result above its range, flagged or not

Goal Read the HL7 v2 messages two analysers drop for the LIMS, and be told when a result is above its reference range.

Two chemistry analysers, HL7 v2.5.1 ORU^R01: segments ended by a lone carriage return.

segment
MSH|^~\&|CHEM-01|QC-LAB|LIMS|QC|20261105090700||ORU^R01|CHEM-01-1001|P|2.5.1\r
segment
PID|1||S-2026-1111^^^QC||LOT 26-0451\r
segment
OBR|1||S-2026-1111|CHEM^Chemistry panel|||20261105090700\r
segment
OBX|1|NM|GLU^Glucose||5.25|mmol/L|3.9-6.1|N|||F|||20261105090700\r
segment
OBX|2|NM|LAC^Lactate||1.43|mmol/L|0.5-2.2|N|||F|||20261105090700\r
segment
OBX|3|NM|CREA^Creatinine||82.4|umol/L|45-110|N|||F|||20261105090700\r
segment
OBX|4|NM|K^Potassium||4.17|mmol/L|3.5-5.1|N|||F|||20261105090700\r

evidence: replay.log, message-1.txt, message-rows.txt, count-read.txt, out-of-range.txt, inbox-batch.json · transform v1

manifests/10-analyser-results.yaml
apiVersion: connectors.lumnik.io/v1kind: Connectormetadata:  name: scenario-hl7-a  connector_type: csv-file  scopes: [qc-chem-a]spec:  transport:    kind: local    path: /tmp/lumnik-scenario/hl7-analyser-results/a/*.hl7   # the folder the analysers drop their batches in  parser:    lines: true                    # one segment per row; HL7's lone carriage return ends a segment  after_process: none  mapping:    cell:      - regex-extract: { column: line, pattern: "^([A-Z][A-Z0-9]{2})\\|", groups: { 1: segment } }      # MSH: the analyser (MSH-3) and the message's control id (MSH-10)      - regex-extract: { column: line, pattern: "^MSH\\|[^|]*\\|([^|]*)\\|(?:[^|]*\\|){6}([^|]*)\\|", groups: { 1: analyser, 2: message_id } }      # PID: the sample (PID-3, first component) and the lot (PID-5)      - regex-extract: { column: line, pattern: "^PID\\|[^|]*\\|\\|([^^|]*)[^|]*\\|\\|([^|]*)", groups: { 1: sample, 2: lot } }      # OBX: the test, the value, the unit, the reference range, the flag, the time of observation      - regex-extract:          column: line          pattern: "^OBX\\|(\\d+)\\|NM\\|([^^|]*)\\^([^|]*)\\|\\|([^|]*)\\|([^|]*)\\|([^-|]*)-([^|]*)\\|([^|]*)\\|(?:[^|]*\\|){5}(\\d{14})"          groups: { 1: obx_seq, 2: test_code, 3: test_name, 4: value, 5: unit, 6: range_low, 7: range_high, 8: flag, 9: observed_at }      # the message's analyser and sample, on every segment of the message      - fill-down: { columns: [analyser, message_id, sample, lot], reset-when: { column: segment, in: [MSH] } }      - parse-number: { column: value }      - parse-number: { column: range_low }      - parse-number: { column: range_high }      - parse-date: { column: observed_at, formats: ["yyyyMMddHHmmss"] }
manifests/20-above-range.yaml
apiVersion: apps.lumnik.io/v1kind: Processmetadata:  name: scenario-hl7-above-range-aspec:  scope: qc-chem-a  on: { event: data.row.changed, table: connector.t_scenario_hl7_a }  when:    - segment=OBX    - "value>{range_high}"           # above the range the analyser sent with the result, whatever its flag  emit:    type: decision.above-range-a    message: "{sample} ({lot}) {test_name}: {value} {unit}, above {range_low}-{range_high} on {analyser}, flag '{flag}'"    key: sample
$ lm connector run scenario-hl7-a
run scheduled: 2b726035-44f7-4408-87a8-b0f7e0a3b8c9

$ lm run get 2b726035-44f7-4408-87a8-b0f7e0a3b8c9
ID        CONNECTOR       ENDPOINT  STATUS     IN  OUT  SKIPPED  DEAD-LETTERS  STARTED           ENDED             EVALUATED
2b726035  scenario-hl7-a  default   Completed  56  56   0        0             2026-11-06 16:57  2026-11-06 16:57  yes
• 1 file(s) unchanged since they were last read — not read again

$ lm inbox --process scenario-hl7-above-range-a
ID     WHEN      PROCESS                     MESSAGE                                                                             ENTITY_KEY
24534  just now  scenario-hl7-above-range-a  S-2026-1126 (LOT 26-0456) Creatinine: 162 umol/L, above 45-110 on CHEM-02, flag ''  S-2026-1126
24533  just now  scenario-hl7-above-range-a  S-2026-1123 (LOT 26-0453) Lactate: 3.1 mmol/L, above 0.5-2.2 on CHEM-01, flag 'H'   S-2026-1123

Limits

  • The table holds every segment: a View or a rule reads segment=OBX. No hook drops a row after fill-down.
  • Conditions are ANDed: below the range is a second Process.
  • Only NM results are read; a text (ST) or coded (CE) OBX keeps its line without columns.
  • Files, not MLLP: the analysers write to a folder, the hub does not listen on a socket.
  • Synthetic samples, lots and results.

Laboratory · HL7 v2

The analysers speak HL7 v2.

One message per sample, one segment per line.

ORU^R01, MSH · PID · OBR · OBX

The problem

One analyser sends no flag.

  • Its creatinine at 162 µmol/L goes unnoticed.
  • The range was in the message all along.

What is not changed

The analysers stay as they are.

  • Their HL7 files, in their folder.
  • No interface engine, no code.

What had to be written

fill-down: { columns: [sample], reset-when: MSH }
when: [segment=OBX, "value>{range_high}"]

One regex per segment, one condition.

  • csv-file: the segments as rows.
  • A Process: the range, not the flag.

The result

Two decisions, one without a flag.

  • Lactate 3.1, flagged H.
  • Creatinine 162, flag empty.

Try it

The scenario replays locally.

Every step is published with its output.

This scenario runs on lumnik Open, free and open source.

Try lumnik Open →