Skip to main content

Demo: HL7 LLP to Scripting to LLP

The most common healthcare interface shape: receive HL7 v2 over MLLP, transform it, forward it to another system, and handle both acknowledgments.

Source LLP → Transform Custom → Destination LLP
:2575 map ADT remote :2576

1. Source LLP node — Receive ADT

Drag LLP from the Source group of the palette.

FieldValue
Listen Port2575
Acknowledgment ModeDefault
Message EncodingMatch the sending system
LLP DelimitersNormal LLP
Use SSLOff, unless the sender uses TLS

With Default, Linkiir generates the ACK for you and the node needs no script. Switch to Custom only when the sender requires specific ACK codes or application-level validation before acknowledging — then supply an ACK script whose main(Data) returns the ACK text.

:::caution Decide what your ACK promises A Default ACK confirms the message was received and accepted into the workflow. It does not confirm the downstream system got it. If your sender treats an ACK as proof of end-to-end delivery, that expectation needs correcting, or the interface needs a Custom ACK that waits for more. :::

Set Message Encoding to match the sender. A mismatch shows up as corrupted names and accented characters, often only for a minority of patients — which makes it easy to miss during testing.


2. Transform Custom node — Map ADT

Drag Custom from the Transform group. It has no configuration fields of its own — its Lua script is created with the node.

Connect Receive ADT to this node. Place your HL7 schema, adt.json, in the node's directory.

local linkiir = require("linkiir")

function main(Data)
local MsgIn, MsgType = linkiir.data.extract{
schema = "adt.json",
data = Data,
type = "hl7",
}

-- Only forward the message types this interface handles.
if MsgType ~= "ADT" then
print("Skipping message type " .. tostring(MsgType))
return
end

-- Require the patient identifier before going further.
local mrn = MsgIn.PID[3][1][1]:value()
if mrn == nil or mrn == "" then
error("PID-3 patient identifier is missing")
end

-- Copy the inbound message, then adjust only what changes.
local MsgOut = linkiir.data.create{
schema = "adt.json",
name = MsgType,
type = "hl7",
}
MsgOut:map(MsgIn)

local family = MsgIn.PID[5][1][1][1]:value()
MsgOut.PID[5][1][1][1] = string.upper(family or "")

linkiir.flow.push{ data = MsgOut:text(), key = mrn }
end

What each part does

LinePurpose
extractParses the message into a navigable tree and returns its type
MsgType ~= "ADT"Filters by returning without pushing — normal operation, not an error
mrn == "" checkRaises for a genuinely broken message the sender must fix
create then mapCopies everything that matches, so only changed fields need writing
PID[5][1][1][1]Segment → field → repeat → component → sub-component
key = mrnGives the message a meaningful correlation key for log search

Raising an error here rather than skipping the message is deliberate: a message missing a patient identifier signals a problem with the feed, not one bad record. Stopping preserves order and makes the problem visible immediately instead of letting hundreds of messages fail unnoticed. See Error Handling and Retry.

Guard every field you read

PID-5 is present in virtually every real ADT, so string.upper(family or "") looks like defensive noise — until one sender omits it and the node stops on a nil. Guard each field you navigate into, or accept a default:

local given = MsgIn.PID[5][1][2][1]:value() or ""

3. Destination LLP node — Send ADT

Drag LLP from the Destination group.

FieldValue
Remote HostThe receiving system's hostname
Remote Port2576
Wait for AckOn
Ack Timeout30000 milliseconds, or whatever the receiver's contract states
Ack VerificationOriginal ID and message type
Ack Error HandlingStop channel
Persistent ConnectionMatch the receiver's expectation
Attempt to ReconnectYes, with limit, with a count and interval
Use SSLMatch the receiver — this node defaults to on

Connect Map ADT to this node. It has no script.

The settings that matter most

Ack Verification — Original ID and message type. The ACK must match the message that was sent. Without this, an ACK for one message can be credited to another, and you will believe messages were delivered that were not.

Ack Error Handling — Stop channel. A negative ACK means the receiver rejected the message and a person needs to decide what happens next. Queue message retains rejections and keeps going; Discard message drops them. For clinical data, start with Stop channel.

Wait for Ack — on. Without it, a message counts as delivered once written to the socket, which tells you nothing about whether the receiver accepted it.

Use SSL — check it. This node defaults to TLS on, while Source LLP defaults to off. Sending to a receiver expecting plain MLLP with Use SSL left on produces a connection that opens and immediately drops.

:::note Remote Host defaults to localhost That is a valid value, so a node you forgot to configure starts cleanly and sends nowhere useful. Set it deliberately, and re-check it after importing this project into another environment. :::

See Destination Nodes for the full field list.


4. Test the script before starting

Add samples to Map ADT containing HL7 messages. Use clearly synthetic identifiers:

MSH|^~\&|SENDER|FACILITY|RECEIVER|FACILITY|20260805120000||ADT^A01|MSG00001|P|2.5
EVN|A01|20260805120000
PID|1||TEST000001^^^FACILITY^MR||TEST^PATIENT^A||19700101|M
PV1|1|I|WARD^101^1

Keep a sample for each of these:

SampleConfirms
Normal ADTThe mapping works
A message type other than ADTThe filter returns quietly
PID-3 absentThe error is raised as intended
PID-5 absentThe guard holds
Repeating PID-3You read the repeat you meant to
Accented characters in the nameEncoding survives the round trip

Click Run Test, then set a breakpoint on the PID[5] line and use Debug to inspect MsgIn and MsgOut side by side. In the variables tree, turn Hide empty off when you want to confirm a field really is empty rather than mis-navigated.


5. Start and validate

  1. Start the workflow and wait for all three nodes to reach RUNNING.
  2. Send an MLLP-framed ADT to port 2575 from your test sender.
  3. Confirm the sender received an ACK.
  4. Confirm the receiving system got the message and returned its ACK.
  5. Open Logs, search the MRN or the correlation ID, and confirm records at all three nodes.

The payload at Send ADT should show the surname uppercased. Comparing the archived payload at Receive ADT against the one at Send ADT is the quickest way to verify a mapping against real traffic.


Troubleshooting

SymptomCheck
Sender connects but the message never completesLLP framing. Leave LLP Delimiters on Normal LLP unless the sender documents otherwise.
Connection opens and immediately dropsTLS mismatch. Use SSL defaults on for Destination LLP and off for Source LLP.
Names arrive corruptedMessage Encoding on the source does not match the sender.
Map ADT goes to ERRORED on a nil valueA field your script assumed was present is absent. Add it as a sample and guard it.
Nothing reaches the destinationConfirm the nodes are connected in the workflow, and that MsgType really is ADT.
Destination times out waiting for an ACKAck Timeout too short, receiver not answering, or a network device closing an idle Persistent Connection.
Messages stop after one rejectionExpected with Ack Error Handling set to Stop channel. Read the ACK detail in the logs, resolve it, then restart.
Receiver reports duplicatesDelivery is at-least-once, so a redelivery is possible. Have the receiver deduplicate on message control ID.

Next