Reference

The ER7 format

Background on the format itself, independent of any implementation. This is what the crate reads and writes.

What ER7 is

ER7 — “Encoding Rules 7” — is the original text encoding for HL7 version 2 messages, and still the one nearly every production interface speaks. It is defined in chapter 2 of every v2 release, from 2.1 in 1990 through 2.9. The nickname pipe-hat comes from its two most visible delimiters, | and ^.

An ER7 message is plain text, positional, and small.

Hierarchy

Six levels, each with its own delimiter.

LevelSeparated byExample
messagethe whole text
segmentcarriage returnPID|1||444333222...
field|EVERYWOMAN^EVE^E
repetition~555-1111~555-2222
component^EVERYWOMAN
subcomponent&1.2.840.114398.1.100

A segment is three characters of name followed by its fields: MSH is the message header, PID patient identification, OBX an observation. Names beginning with Z are local extensions, defined by whoever is at the two ends and by nobody else.

Everything is positional. PID-5.1 is a family name because it is fifth and first, not because anything in the message says so. This is why one misplaced | corrupts everything after it, and why a message needs a dictionary — the HL7 standard for that version — before its values mean anything.

Delimiters

Only the segment terminator is fixed: a carriage return, \r, hex 0D. The standard is explicit that implementers cannot change it. In practice many systems store messages in files with \n or \r\n instead, so a tolerant reader accepts all three.

The rest are declared by the message itself, in its first two fields.

MSH|^~\&|
   ^^^^^
   |||||
   ||||+- subcomponent separator  (MSH-2 position 4)
   |||+-- escape character        (MSH-2 position 3)
   ||+--- repetition separator    (MSH-2 position 2)
   |+---- component separator     (MSH-2 position 1)
   +----- field separator         (MSH-1, 4th character of the message)

MSH-1 is a field whose value is the field separator, and MSH-2 is a field whose value is the encoding characters. This is circular by design, and it is why those two fields can never be split or escaped like ordinary ones. It also means a reader learns the delimiters from bytes 4–8 of the message and must not assume |^~\&, however universal that choice is in practice.

HL7 v2.7 added a fifth encoding character, the truncation character (recommended #), marking a value the sender cut short to fit a length limit. Most messages omit it.

The batch envelope segments FHS and BHS declare delimiters the same way, since a batch file may begin with either.

In the crate: the delimiter set is read from every message, never hardcoded, and a set that reuses one character for two roles is rejected — that message could not be read back as the sender meant it.

Empty, and the explicit null

Three states, easily confused, and the difference is clinical.

On the wireMeansA receiver must
the field is absentno informationleave any stored value alone
||present, but no valueleave any stored value alone
|""|the explicit nullclear the stored value

Trailing fields a sender has nothing for may simply be dropped, so a PID ending after field 8 is normal and says nothing about fields 9 onward. The two-character "" is the only way to say “delete what you have”.

Getting this wrong is a patient-safety bug. Treating a null as empty leaves a withdrawn allergy on the record. Treating an empty as a null erases a value that was never sent. The crate keeps all three apart, at every level of the tree.

Escape sequences

A value that needs to contain a delimiter escapes it. A sequence is the escape character, a body, and the escape character again: \F\, \X0D\, \.br\.

Five of them stand for the delimiters themselves; the rest are hexadecimal data, character-set switches, display formatting, or locally agreed extensions.

The full escape-sequence reference →

Batch files

Several messages can share a file, wrapped in an envelope.

FHS   file header
  BHS   batch header
    MSH   message ...
    MSH   message ...
  BTS   batch trailer
FTS   file trailer

The envelope segments describe the file, not any message in it: who sent the batch, how many messages it holds, when it was written. A reader that wants the messages drops them and starts a new message at each MSH.

On the wire

ER7 messages are usually carried by MLLP (Minimal Lower Layer Protocol): each message is wrapped in a start byte (0x0B) and an end sequence (0x1C 0x0D) over a TCP connection, and the receiver answers with an ACK message quoting the original’s MSH-10 control ID.

The framing is a separate concern from the encoding, which is why this crate handles only the latter. Strip the framing bytes before parsing.

Why it persists

The tradeoffs are stark, and they have kept ER7 in place for thirty-five years.

In its favour

Messages are tiny, so an interface engine can move millions a day. The format is trivially streamable. And it is embedded in hundreds of thousands of production interfaces, most of which will never be rewritten.

Against it

It is positional, so it is brittle and unreadable without tooling. There is no schema in the message itself. And the same field number means different things in different versions.

HL7 published an XML encoding in v2.3.1 and FHIR later, but neither displaced ER7 in the installed base.

Sources