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).
npm install unified vfile @glion/parser @glion/decode-escapes @glion/preset-annotate-profile-recommended @glion/preset-lint-recommendedpnpm add unified vfile @glion/parser @glion/decode-escapes @glion/preset-annotate-profile-recommended @glion/preset-lint-recommendedyarn add unified vfile @glion/parser @glion/decode-escapes @glion/preset-annotate-profile-recommended @glion/preset-lint-recommendedbun add unified vfile @glion/parser @glion/decode-escapes @glion/preset-annotate-profile-recommended @glion/preset-lint-recommendedEach package is a plugin handling one stage of the pipeline:
| Package | Role |
|---|---|
@glion/parser | Turns the wire string into an AST. |
@glion/decode-escapes | Replaces \F\, \S\, \Xdddd\, etc. with their literal characters before later steps see them. |
@glion/preset-annotate-profile-recommended | Attaches profile metadata (segment names, datatypes, code system bindings) using the version in MSH-12. |
@glion/preset-lint-recommended | Runs 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:
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.
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.
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:
| Field | Meaning |
|---|---|
reason | Human-readable description of the problem. |
ruleId | Identifier of the rule that raised it (e.g. no-empty-required-field). |
line | 1-based line number in the source. |
column | 1-based column number in the source. |
fatal | true 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.