Guides
Task-oriented recipes against the real API. Each stands alone. Jump to the one that matches what you're doing. For the why behind the behavior, follow the links into Core Concepts.
Read lab results
msg.observations() returns every OBX in document order as a typed Observation. The coded fields
(identifier, units) are CWE composites: .identifier is the code, .text the human-readable
name.
import { parseHL7 } from "@cosyte/hl7";
const raw = [
"MSH|^~\\&|LAB|MAIN|EHR|REF|20260419143000||ORU^R01^ORU_R01|EX00002|P|2.5",
"PID|1||MRN67890^^^HOSP^MR||Smith^Alice^B||19750620|F",
"OBR|1|ORD-EX-1|FLR-EX-1|CBC^Complete Blood Count^L|||20260419140000",
"OBX|1|NM|WBC^White Blood Cells^LN||7.5|10*3/uL|4.5-11.0|N|||F",
"OBX|2|NM|HGB^Hemoglobin^LN||14.2|g/dL|13.5-17.5|N|||F",
].join("\r");
const observations = parseHL7(raw).observations();
observations.length; // => 2
const [wbc] = observations;
wbc.identifier.identifier; // => "WBC"
wbc.identifier.text; // => "White Blood Cells"
wbc.value; // => 7.5
wbc.units?.identifier; // => "10*3/uL"
wbc.referenceRange; // => "4.5-11.0"
Numeric OBX-5 values arrive typed as number (here 7.5), not strings: a fidelity detail, not a
string you have to parseFloat yourself.
Apply a vendor profile
Real feeds carry vendor quirks. Apply a built-in profile as the second argument to parseHL7, or
declare your own with defineProfile():
import { parseHL7, profiles } from "@cosyte/hl7";
const raw =
"MSH|^~\\&|EPIC|MAIN|LIS|REF|20260419101500||ADT^A01^ADT_A01|EX00001|P|2.5\r" +
"PID|1||MRN12345^^^HOSP^MR||Doe^John^Q||19800115|M";
const msg = parseHL7(raw, profiles.epic);
msg.patient.mrn; // => "MRN12345"
Built-ins ship for Epic, Cerner, Meditech, Athena, a generic lab, and the Visage 7 and Philips Vue
PACS imaging systems, each authored through the same public defineProfile() API you'd use
yourself. Start from the profile starter kit in the
package's examples/ to publish your own as a standalone package.
Build an ACK
buildAck turns an inbound message into a spec-clean acknowledgement, echoing the correlation id and
swapping sender/receiver. msg.toString() is always spec-clean regardless of how quirky the input
was:
import { parseHL7, buildAck } from "@cosyte/hl7";
const inbound = parseHL7(raw);
const ack = buildAck(inbound, { code: "AA" }); // AA | AE | AR
ack.toString(); // MSH|^~\&|<receiver>|...|ACK|... + MSA|AA|<control-id>
For MLLP transport framing and ACK correlation over the wire, see the sibling package
@cosyte/mllp, which adapts over this same buildAck primitive.
Build a message from scratch
buildMessage is the outbound counterpart to parseHL7: give it the message metadata, append
segments with positional field arrays (index 0 is the segment name slot, so leading ""s skip to
the field you want), then serialize:
import { buildMessage } from "@cosyte/hl7";
const msg = buildMessage({
type: "ADT^A01",
version: "2.5",
sendingApp: "CLINIC",
sendingFacility: "MAIN",
receivingApp: "LAB",
receivingFacility: "REF",
}).addSegment("PID", ["", "", "MRN12345"]); // PID-3 = MRN12345
msg.get("PID.3"); // => "MRN12345"
msg.toString() then emits spec-clean HL7: correct delimiters, escaping, and an auto-generated MSH
control id. Field array elements are raw field values: any delimiter character inside one (e.g. a
^ in a name) is escaped as data, so pass pre-structured composites when you need components.
Author a message from typed objects
buildAdt and buildOru are the typed, high-level counterparts of the read helpers (msg.patient,
msg.observations): pass structured values (an XPN name, CX identifiers, a TS timestamp) and
the builder assembles the required segments (MSH + EVN + PID + PV1 for ADT; MSH + PID + OBR + OBX for
ORU) with correct ^/&/~ structure. No hand-assembly of delimiters, and any delimiter embedded in
a value is escaped, never injected. The result is spec-clean and re-parses with zero warnings.
import { buildAdt, parseHL7 } from "@cosyte/hl7";
const msg = buildAdt("A01", {
sendingApp: "CLINIC",
receivingApp: "LAB",
patient: {
identifiers: { idNumber: "MRN12345", identifierTypeCode: "MR" },
name: { familyName: "Test", givenName: "Ann" }, // a "^" here would be escaped, not injected
birthDateTime: "19880705",
administrativeSex: "F",
},
visit: { patientClass: "I" },
});
const round = parseHL7(msg.toString());
round.patient?.mrn; // => "MRN12345"
round.patient?.familyName; // => "Test"
round.warnings.length; // => 0
Builders never fabricate: only values you supply are emitted, an omitted optional field stays
absent, and a required-but-absent input (patient, or at least one ORU observation) is a typed
TypeError, never a guessed value. For a lower-level typed set on an existing message, use
msg.setComposite(path, kind, value) (e.g. msg.setComposite("PID.5", "XPN", { familyName: "Doe" })),
or encodeComposite(kind, value) to build a field directly.
Next
- Troubleshooting: warnings vs. errors, strict mode, charset, and batches.
- API Reference: every export, generated from source.