Skip to content

Processing HL7v2 messages

Run a message through Glion's processing pipeline — parse, decode escapes, annotate against a profile, and lint — and read back the annotated tree plus diagnostics.

Glion's HL7v2 processing runs a message through a unified pipeline: parse the wire string into an AST, decode escape sequences, annotate the tree with profile metadata, and lint. The output is the annotated tree plus a list of diagnostics — each one carrying a rule identifier, a human-readable reason, and the line and column where the problem sits.

unified is the same engine that powers remark and rehype. Glion's pipeline is assembled from small plugins, each doing one part of the work, so a route can swap any step — use a different parser, skip annotation, add a custom lint rule — without affecting the rest.

Manual installation

Install the parser, escape decoder, profile annotator, and recommended lint preset alongside the unified runtime and vfile (which carries diagnostics).

Terminal
npm install unified vfile @glion/parser @glion/decode-escapes @glion/preset-annotate-profile-recommended @glion/preset-lint-recommended

Each package is a plugin handling one stage of the pipeline:

PackageRole
@glion/parserTurns the wire string into an AST.
@glion/decode-escapesReplaces \F\, \S\, \Xdddd\, etc. with their literal characters before later steps see them.
@glion/preset-annotate-profile-recommendedAttaches profile metadata (segment names, datatypes, code system bindings) using the version in MSH-12.
@glion/preset-lint-recommendedRuns the recommended lint rules and pushes a diagnostic for every violation.

Build the pipeline

Create process.ts and compose the four pieces into a single reusable pipeline:

process.ts
import { unified } from "unified";
import { VFile } from "vfile";
import { hl7v2Parser } from "@glion/parser";
import { hl7v2DecodeEscapes } from "@glion/decode-escapes";
import hl7v2PresetAnnotateProfileRecommended from "@glion/preset-annotate-profile-recommended";
import hl7v2PresetLintRecommended from "@glion/preset-lint-recommended";

const pipeline = unified()
  .use(hl7v2Parser)
  .use(hl7v2DecodeEscapes)
  .use(hl7v2PresetAnnotateProfileRecommended)
  .use(hl7v2PresetLintRecommended)
  .freeze();

Order matters. Decoding runs first so subsequent steps see real values, not escape sequences. Profile annotation runs before the lint preset so rules can read the metadata the annotation step attaches.

.freeze() locks in the plugin list. The pipeline is reusable and safe to share between routes — build it once at startup, run it against every incoming message.

Run a message

Glion does not ship a compiler, so the pipeline stops at the processed tree rather than at a serialized string. The two-call shape is the right one: parse, then run.

process.ts
export async function process(message: string) {
  const file = new VFile(message);
  const tree = pipeline.parse(file);
  await pipeline.run(tree, file);
  return { tree, diagnostics: file.messages };
}

After run resolves, tree is the annotated AST and file.messages holds every diagnostic raised along the pipeline. Both are useful: the diagnostics tell you what is wrong; the tree tells you what the message contained.

Reading diagnostics

Each entry in file.messages is a VFileMessage carrying a rule identifier, a human-readable reason, the line and column in the source, and a severity flag.

process.ts
const message = [
  "MSH|^~\\&|HIS|HOSPITAL|EMR|HOSPITAL|20260101120000||ADT^A01|MSG00001|P|2.5",
  "PID|1||PATID1234^^^HOSPITAL^MR||DOE^JOHN",
].join("\r");

const { diagnostics } = await process(message);

for (const d of diagnostics) {
  const severity = d.fatal ? "error" : "warning";
  console.log(`${severity}: ${d.reason} at ${d.line}:${d.column} (${d.ruleId})`);
}

Each diagnostic exposes the same fields, regardless of which plugin raised it:

FieldMeaning
reasonHuman-readable description of the problem.
ruleIdIdentifier of the rule that raised it (e.g. no-empty-required-field).
line1-based line number in the source.
column1-based column number in the source.
fataltrue when downstream work should stop. Lint rules raise warnings (false) by default.

Plugins set d.fatal to true for problems that should block downstream work — a malformed segment, a missing required field. Lint rules raise warnings by default; reconfigure individual rules to upgrade them to errors when the project needs strict mode.

Adjusting rules

The recommended preset is a starting point, not a contract. To turn off a rule, change its severity, or pass it different options, re-use it after the preset with the new configuration. unified runs each plugin once with its final options, so a later .use(rule, options) overrides whatever the preset set.

Common patterns

Per-route pipelines. A single Node process that handles two HL7v2 versions or two partner profiles builds one frozen base pipeline and forks per route by calling .use() on the frozen instance. Each route gets the rules it needs without leaking into the others.

Processing in tests. pipeline.parse(file) returns the AST without running rules — useful when a test wants to inspect the parser output. pipeline.run(tree, file) runs the rules against an existing tree. Use the two together to fixture a tree once and run it through different rule sets.

Custom rules. A custom lint rule is a unified plugin: a function that returns a transformer, walks the tree with unist-util-visit, and pushes diagnostics with file.message(reason, position, ruleId). The rule slots into the same .use() chain as the recommended preset.

See also

  • Receiving HL7v2 messages — wire the pipeline into the server's middleware chain so every incoming message is processed before its handler runs.
  • Message conformance — what each rule checks, the structural vs profile split, diagnostics, severity, and presets.
  • Abstract Syntax Tree — the node types rules walk.
  • unist-util-visit — the standard tree walker for custom rules.
  • vfile — the file model that carries diagnostics.