Skip to main content
Version: v0.1.0

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.

Custom segment field names are checked by the compiler​

A profile's customSegments declaration names the fields of your Z-segments, and reading one back by name goes through seg.get(name). When the profile is one the compiler can see, the names it declares for that segment type are the only ones get accepts, so a typo is a build failure rather than a blank column on a report:

import { defineProfile, parseHL7 } from "@cosyte/hl7";

const vendor = defineProfile({
name: "vendor",
customSegments: {
ZDP: { fields: { departmentCode: 3, departmentName: 4 } },
ZRS: { fields: { resultStatus: 1 } },
},
});

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\r" +
"ZDP|||CARDIOLOGY|Cardiology Department";

const msg = parseHL7(raw, vendor);

msg.part("ZDP")?.get("departmentCode")?.value; // => "CARDIOLOGY"
// msg.part("ZDP")?.get("departmentCod"); // does not compile: not a declared name
// msg.part("ZDP")?.get("resultStatus"); // does not compile: that name belongs to ZRS

Nothing about the read changed: a declared name whose position the message did not carry is still undefined, so the result stays Field | undefined. And the check only tightens where the declaration is visible. No profile, a value you typed as Profile, a profile registered with setDefaultProfile, a segment type the profile does not declare, or a walk over msg.allSegments() all keep accepting any string, exactly as before.

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​

The typed builders are the high-level counterparts of the read helpers (msg.patient, msg.observations, msg.orders, …): pass structured values (an XPN name, CX identifiers, a TS timestamp) and the builder assembles the segments the message type requires 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.

buildermessageread it back with
buildAdt(event, init)ADT admit, discharge, transfer, merge, move and identifier changemsg.patient, msg.visit, msg.identityEvents()
buildOru(init)ORU^R01 observation resultmsg.observations()
buildOrm(init)ORM^O01 general ordermsg.orders()
buildSiu(event, init)SIU scheduling notificationmsg.appointments()
buildMdm(event, init)MDM clinical documentmsg.documents()
buildDft(event, init)DFT detail financial transactionmsg.charges()
buildVxu(init)VXU^V04 vaccination record updatemsg.immunizations()
buildAck(inbound, options)ACK acknowledgmentinterpretAck(msg)
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

Every family works the same way. A scheduling notification, for example, takes the appointment and its resource groups and reads back through msg.appointments():

import { buildSiu, parseHL7 } from "@cosyte/hl7";

const msg = buildSiu("S12", {
sendingApp: "SCHEDULING",
receivingApp: "EHR",
appointment: {
fillerAppointmentId: "FL-2002",
startDateTime: "20260801090000",
endDateTime: "20260801093000",
fillerStatusCode: { identifier: "Booked" },
},
resourceGroups: [
{
setId: "1",
resources: [
{ kind: "location", code: { identifier: "OR-1" } },
{ kind: "personnel", person: { idNumber: "9990", familyName: "Welby" } },
],
},
],
});

const appt = parseHL7(msg.toString()).appointments()[0];
appt?.fillerAppointmentId; // => "FL-2002"
appt?.startDateTime?.raw; // => "20260801090000"
appt?.resources.length; // => 2

Builders never fabricate: only values you supply are emitted, an omitted optional field stays absent, and content the message type requires but the init does not carry is a typed TypeError, never a guessed value.

import { buildDft } from "@cosyte/hl7";

// A financial transaction message needs at least one transaction:
buildDft("P03", {
patient: { identifiers: { idNumber: "MRN12345" } },
charges: [],
});

Which message types can be authored​

SUPPORTED_BUILDER_MESSAGES publishes the (message code, trigger event) pairs the typed builders can author as structurally complete messages, and supportsBuilderMessage(code, event) answers for one pair. The set is derived from the same published message structures the parser checks against, so it never claims a pair whose structure needs a segment no typed init can supply.

import { SUPPORTED_BUILDER_MESSAGES, supportsBuilderMessage } from "@cosyte/hl7";

supportsBuilderMessage("SIU", "S12"); // => true
supportsBuilderMessage("ADT", "A20"); // => false
SUPPORTED_BUILDER_MESSAGES.some((m) => m.messageCode === "VXU"); // => true

A false answer is not a refusal. A builder that takes a trigger event still emits the content you supply for an event outside the set; the structure summary on the re-parsed message then reports what is missing, exactly as it does for a message that arrived from anywhere else.

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.