Skip to content

HL7v2 profiles

HL7v2 version-specific profile data — segments, fields, datatypes, and tables — with LRU-cached loaders.

HL7v2 profile data for each version — event schemas, segments, fields, datatypes, tables, and code systems — with loaders, a runner that validates and groups a message's segments against its event schema, and a repair that reads a message that does not fit as the nearest message that does.

What it does

@glion/profiles is the data source for Glion's profile-aware plugins. It provides structured HL7v2 profile definitions for every supported version (2.1 through 2.8.2), loaded on demand, once per process. The annotation plugins (@glion/annotate-profile-*) and the profile lint rules (@glion/lint-profile-*) read from this package to enrich and validate HL7v2 messages against the HL7-published specifications.

Install

npm install @glion/profiles

Use

import { profiles, runner } from "@glion/profiles";

const fields = await profiles.fields.load("2.5", "MSH");
const msh9 = fields.bySequence.get(9);
msh9?.name; // => "Message Type"
msh9?.datatype; // => "MSG"

const schema = await profiles.events.load("2.5", "ADT_A04");
schema.id; // => "ADT_A01", the event schema ADT^A04 uses

runner(schema, ["MSH", "EVN", "PID", "PV1"]);
// => { type: "matched", groups: [0, 1, 2, 3] }

API

profiles

The profile stores: events, eventMaps, fields, datatypes, tables, and codeSystems.

Stores

MethodResolves
events.load(version, id)EventSchema, by event (ADT_A04) or schema ID (ADT_A01)
eventMaps.load(version)EventMap: the event schema ID of each event, such as ADT_A04 → ADT_A01
fields.load(version, segmentId)FieldDefinition
datatypes.load(version, datatypeId)DatatypeDefinition
tables.load(version, tableNumber)TableDefinition
codeSystems.load(codeSystemId)CodeSystemDefinition

An event schema events.load resolves is frozen, at every depth. load resolves undefined for a profile the version does not bundle, and rejects only when a bundled profile fails to load. Each profile loads once per process; later loads of it resolve the same value, and a load that fails is retried by the next call.

loadSegments(version)

Resolves the segment definitions of a version: each segment ID with its title, or undefined for a version not bundled.

An event schema of your own

An EventSchema is plain data, the shape the Event definition of hl7v2.schema.json describes: segment, group, and choice elements, each with optional (the standard's [ ]) and repeating (its { }). runner takes it as it takes a bundled one.

import type { EventSchema } from "@glion/profiles";

const schema: EventSchema = {
  id: "ADT_A01_SITE",
  elements: [
    { type: "segment", name: "MSH", optional: false, repeating: false },
    { type: "segment", name: "PID", optional: false, repeating: false },
    {
      type: "group",
      id: "VISIT",
      name: "VISIT",
      optional: true,
      repeating: false,
      elements: [
        { type: "segment", name: "PV1", optional: false, repeating: false },
        { type: "segment", name: "ZPV", optional: true, repeating: true },
      ],
    },
  ],
};

A schema of your own works wherever a bundled one does: runner and the definition option of @glion/lint-profile-segment-order. runner throws when the schema has no elements, a segment has no name, a group has no ID, no name, or no elements, a choice has no alternatives, or a choice alternative can match no segment.

runner(schema, segmentIds[, options])

Runs a message's segment IDs through an event schema: validates their order and groups them. Returns one of:

typeFieldsWhen
"matched"groups: the segment indexes nested in the groups the schema definesthe segments fit the schema
"mismatched"index: the first segment that does not fit; expected: the segment IDs valid therea segment the schema does not allow at its position
"incomplete"expected: the segment IDs that can come nextevery segment fits, but the schema requires more
import { profiles, runner } from "@glion/profiles";

const schema = await profiles.events.load("2.5", "ORU_R01");

runner(schema, ["MSH", "PID", "OBR", "OBX"]);
// { type: "matched", groups: [0, { id: "PATIENT_RESULT", name: "PATIENT_RESULT", children: [
//   { id: "PATIENT", name: "PATIENT", children: [1] },
//   { id: "ORDER_OBSERVATION", name: "ORDER_OBSERVATION", children: [2,
//     { id: "OBSERVATION", name: "OBSERVATION", children: [3] }] },
// ] }] }

runner(schema, ["MSH", "PID", "OBR", "MSH"]);
// { type: "mismatched", index: 3, expected: ["NTE", "OBX", …] }

expected is sorted; Hxx in a schema matches any segment ID and is listed as Hxx, and anyZSegment matches any segment ID that starts with Z and is listed as anyZSegment. Where the schema admits more than one grouping, the runner enters an optional element rather than skip it, repeats an element rather than leave it, and takes the earlier alternative of a choice. A group occurrence that holds no segment is left out. Runs in time proportional to the number of segments times the size of the schema. A schema is compiled on its first run and reused for later runs of the same object, so a schema must not change after its first run, and passing the same object for every message avoids compiling it again.

Z-segments

A Z-segment (a segment ID that starts with Z) that the schema does not name fits at any position, as HL7v2 allows local Z-segments in any message and segment group. It is grouped right after the segment before it, in that segment's group. A Z-segment the schema names is matched like any other segment. A reading that reaches an Hxx takes the Z-segment there rather than pass over it.

OptionTypeDefaultDescription
allowZSegmentsbooleantrueWhen false, a Z-segment the schema does not name is a mismatch, like any segment it does not allow.
runner(schema, ["MSH", "PID", "ZPI", "OBR", "OBX"]);
// { type: "matched", groups: [0, { id: "PATIENT_RESULT", name: "PATIENT_RESULT", children: [
//   { id: "PATIENT", name: "PATIENT", children: [1, 2] },
//   { id: "ORDER_OBSERVATION", name: "ORDER_OBSERVATION", children: [3,
//     { id: "OBSERVATION", name: "OBSERVATION", children: [4] }] },
// ] }] }

runner(schema, ["MSH", "PID", "ZPI", "OBR", "OBX"], { allowZSegments: false });
// { type: "mismatched", index: 2, expected: ["NK1", "NTE", "OBR", …] }

repair(schema, segmentIds[, options])

Reads a message's segment IDs as the message the event schema accepts with the fewest edits, and returns the edits in message order; none when the segments fit. An edit is a missing segment, one the schema requires and the message does not have, or an unexpected segment, one the message has and the schema does not allow there:

FieldMissing (type: "missing")Unexpected (type: "unexpected")
segmentThe segment ID, as the schema names itThe segment ID
indexWhere it would be inserted, as by segmentIds.splice(index, 0, segment); the number of segments at the endIts index in segmentIds
pathThe IDs of the groups it belongs in, outermost firstThe IDs of the groups of the segment before it, outermost first
import { profiles, repair } from "@glion/profiles";

const schema = await profiles.events.load("2.5", "ORU_R01");

repair(schema, ["MSH", "PID", "OBX", "OBX"]);
// [{ type: "missing", segment: "OBR", index: 2,
//    path: ["PATIENT_RESULT", "ORDER_OBSERVATION"] }]

repair(schema, ["MSH", "PID", "OBR", "OBX", "PV1", "OBX"]);
// [{ type: "unexpected", segment: "PV1", index: 4,
//    path: ["PATIENT_RESULT", "ORDER_OBSERVATION", "OBSERVATION"] }]

Of repairs with as many edits, repair returns one with the fewest unexpected segments; of those, the first in the runner's order, where at each segment the schema's reading comes first, then passing over a Z-segment, then the segment as unexpected in place of the schema's segment there, as missing, then a missing segment, then an unexpected one. A segment out of place is reported next to the segment it displaces: in ADT_A01, MSH PIDX PV reads as EVN missing and PIDX unexpected, then PID missing and PV unexpected, then PV1 missing at the end. MSH PID OBR OBX ORC OBX reads as an OBR missing before the last OBX, not as an unexpected ORC. A Z-segment the schema does not name costs no edit unless allowZSegments is false, as for runner. Runs in time and memory proportional to the number of segments times the size of the schema, and shares the compiled schema with runner.

Glossary

HL7v2 terms

TermMeaningIn a message
VersionThe HL7v2 version a message follows, such as 2.5.1. Every profile belongs to one version.MSH-12.1
Message typeWhat kind of message it is, such as ADT or ORU.MSH-9.1
Trigger eventWhat happened, such as A04 (patient registered).MSH-9.2
EventA message type and trigger event together, written ADT_A04.MSH-9.1 and MSH-9.2
Message structure IDThe ID of the event schema a message follows, such as ADT_A01. Several events share one.MSH-9.3
SegmentA line of a message, named by a three-character segment ID such as PID.Each segment
Field, componentA segment's fields, numbered from 1 (PID-3), and the components of a composite field (PID-3.1).
DatatypeThe type of a field or component, such as CX (composite) or ST (primitive).
TableA version's list of allowed codes for a field, such as table 0001 (Administrative Sex).
Code systemThe HL7 Terminology (UTG) form of a table, such as v2-0001, the same across versions.

Glion names

NameMeaning
ProfileThe data this package bundles about one thing in one version: an event schema, a segment's fields, a datatype, a table, or the version's segments. Code systems are the one kind without a version.
Event schema (EventSchema)The structure an event's messages follow: its segments in order, nested in groups and choices, as the standard defines it. Its id is the message structure ID.
Event map (EventMap)For each version, the event schema ID each event and each schema ID maps to, such as ADT_A04 → ADT_A01.
Element (EventSchemaElement)One node of an event schema: a segment (SegmentElement), a named group of elements (GroupElement), or a choice between alternatives (ChoiceElement).
Occurrence (Occurrence)Every element's optional (the standard's [ ]) and repeating (its { }). An element that is neither occurs exactly once.
Z-segmentA locally defined segment: its ID starts with Z. HL7v2 allows one in any message and segment group.
anyZSegmentA segment element that matches any Z-segment. The HL7 v2 XML schemas use it for a site's Z-segment, as in MFN_M01.
HxxA segment element that matches any segment ID.
Runner (runner)Runs a message's segment IDs through an event schema once: validates their order and groups them. Its result (RunnerResult) is matched, mismatched, or incomplete.
Repair (repair)Reads a message's segment IDs as the message an event schema accepts with the fewest edits (RepairEdit): missing segments (RepairMissing) and unexpected segments (RepairUnexpected). It returns the edits.
Groups (SegmentMatch, GroupMatch)What a matched result carries: each segment's index in the message, nested in the group occurrences (GroupMatch) it belongs to.
Store (profiles.events, profiles.fields, …)The loader of one kind of profile: load(version, id) resolves the profile, or undefined for one it does not bundle.

Naming convention

  • Event names what is keyed or looked up by trigger event: profiles.events, profiles.eventMaps. Event schema names the data an event resolves to: EventSchema. Message structure names only the MSH-9.3 value, the schema's id.
  • …Definition is what a store resolves: FieldDefinition, DatatypeDefinition, TableDefinition, SegmentDefinition, CodeSystemDefinition. …Profile and …Entry are one item inside it: FieldProfile, ComponentProfile, SegmentProfile, TableCodeEntry, UtgCodeEntry. …Module is the shape of a bundled data file.
  • …Element is a node of an event schema; …Match is a node of the groups a runner result carries.
  • A version is a string without a prefix, "2.5.1". A table loads by its number, "0001", while a field names it with the HL7 prefix, "HL70001". A code system ID has the UTG prefix, "v2-0001".

Profile data format

Each profile is loaded on demand, the first time it is requested, and kept for the life of the process.

Segments

interface SegmentDefinition {
  byId: ReadonlyMap<string, { id: string; title: string }>; // "PID" → "Patient Identification"
}

Fields

interface FieldDefinition {
  segmentId: string; // "MSH"
  bySequence: ReadonlyMap<number, FieldProfile>; // 9 → MSH-9
  requiredSequences: ReadonlySet<number>; // the fields whose optionality is "R"
}

interface FieldProfile {
  sequence: number; // 9
  id: string; // "MSH-9"
  name?: string; // "Message Type"
  datatype?: string; // "MSG"; absent on a withdrawn field with no datatype
  optionality?: Optionality; // OPT, from @glion/utils; absent where the standard records none
  repetitions: number | "unbounded"; // RP/#; 1 when the field does not repeat
  maxLength?: number;
  table?: string; // "HL70001" when the field is coded
  item?: string; // the HL7 data element number
}

Optionality (from @glion/utils) is the OPT column as printed: "R" | "RE" | "O" | "C" | "X" | "B" | "W", with named constants such as Optionality.Withdrawn.

Datatypes

interface DatatypeDefinition {
  id: string; // "CX"
  version: string; // "2.5"
  kind: string; // "primitive" or "composite"
  title?: string;
  componentsBySequence: ReadonlyMap<number, ComponentProfile>; // empty for a primitive
  requiredSequences: ReadonlySet<number>; // the components whose optionality is "R"
}

interface ComponentProfile {
  sequence: number; // 7
  name: string; // "Name Type Code"
  optionality?: Optionality; // "O"; absent before v2.5
  datatypeId?: string; // "ID"; absent on a withdrawn component
  table?: string; // "HL70200"
  maxLength?: number;
}

Event schemas

type EventSchema = { id: string; elements: EventSchemaElement[] };

type EventSchemaElement =
  | { type: "segment"; name: string; optional: boolean; repeating: boolean }
  | {
      type: "group";
      id: string;
      name: string;
      optional: boolean;
      repeating: boolean;
      elements: EventSchemaElement[];
    }
  | {
      type: "choice";
      optional: boolean;
      repeating: boolean;
      alternatives: EventSchemaElement[];
    };

optional is the standard's [ ] and repeating its { }. A group's id, such as PATIENT_VISIT, holds only uppercase letters, digits, and _; its name is the group name for display, in title case with abbreviations spelled out, such as Patient Visit or Master File Test Battery Detail. A choice is the standard's < A | B >: exactly one alternative per occurrence, and every alternative matches at least one segment.

JSON Schema

@glion/profiles/hl7v2.schema.json is the JSON Schema (draft-07) of every profile file, with $id https://glion.dev/schemas/hl7v2/v1.json. It has one definition per kind of file:

DefinitionFile
Eventan event schema, such as ADT_A01
Fieldsthe fields of a segment
Datatypea datatype
Tablea table
CodeSysteman HL7 Terminology code system

Every bundled profile file names its definition in $schema, such as https://glion.dev/schemas/hl7v2/v1.json#/definitions/Fields, and conforms to it. The event map and the segment list of each version (event-map.json, segments.json) are lookups, not profiles, and are not in the schema. The root accepts a file of any kind; select a definition to validate one kind. The schema checks the shape; runner also requires that every choice alternative of an event schema matches at least one segment.

import { Ajv } from "ajv";
import schema from "@glion/profiles/hl7v2.schema.json" with { type: "json" };

const ajv = new Ajv({ schemas: [schema] });
const validateEventSchema = ajv.getSchema(`${schema.$id}#/definitions/Event`);

Tables and code systems

interface TableDefinition {
  id: string; // "0001"
  description: string; // "Administrative Sex"
  type: "user" | "hl7";
  codes: ReadonlyMap<string, { name: string; description: string }>; // "F" → "Female"
}

interface CodeSystemDefinition {
  id: string; // "v2-0001"
  url: string;
  oid?: string;
  name: string;
  title: string;
  codes: ReadonlyMap<string, { code: string; display: string; status: string }>;
}