A hands-on implementation guide with complete working code, 8 mapping tables, a clinical business rules engine, live terminology validation, and full input-to-output examples.
Why This Matters: The Regulatory Ground Beneath Your Feet
Every US hospital runs on HL7v2. The ADT^A01 message -- admit, discharge, transfer -- is the heartbeat of hospital operations. It fires when a patient walks into the emergency department. It fires when the surgeon admits them. It fires when insurance verification completes. Upstream systems have been producing these messages since the 1990s.
But the regulatory landscape has shifted permanently. The 21st Century Cures Act (2016), the ONC Interoperability Final Rule (2020), and CMS's Patient Access and Provider Directory API requirements now mandate FHIR R4 as the lingua franca of US healthcare data exchange. Hospitals that only speak HL7v2 internally still need to speak FHIR R4 externally -- to payers, to health information exchanges, to patient-facing apps under the information blocking rules.
This creates a concrete engineering problem: how do you take an inbound HL7v2 ADT^A01 message, with its pipe-delimited segments and cryptic single-character codes, and transform it into a compliant FHIR R4 Transaction Bundle -- with proper US Core profiles, dual ICD-10/SNOMED CT coding, and real-time clinical decision support -- all within the message processing pipeline?
This post answers that question with a complete, working Mirth Connect channel. Not pseudocode. Not architecture slides. The actual JavaScript transformer, the mapping tables, the business rules, and the full FHIR Bundle JSON output.
What We Are Building
A single Mirth Connect channel that:
- Receives HL7v2 ADT^A01 messages via MLLP (TCP port 6661)
- Maps HL7v2 coded values to FHIR-compliant ValueSets using 8 lookup tables
- Applies clinical business rules -- sepsis bundles, geriatric screening, ICU protocols, insurance validation, hip fracture fast-tracking
- Validates diagnosis codes against tx.fhir.org (the HL7 public FHIR Terminology Server)
- Produces a FHIR R4 Transaction Bundle with dual ICD-10 + SNOMED CT coding
- Generates between 6 and 9 FHIR resources depending on patient data and triggered rules
The output Bundle conforms to US Core 5.0.1 profiles where applicable and uses proper FHIR terminology bindings throughout.
Infrastructure: Docker Setup
Before the channel, the infrastructure. Mirth Connect 4.5.2 runs on Docker with a PostgreSQL 16 backend. Here is the docker-compose.yml:
services:
mirth-db:
image: postgres:16-alpine
container_name: mirth-db
environment:
POSTGRES_DB: mirthdb
POSTGRES_USER: mirthdb
POSTGRES_PASSWORD: mirthdb
ports:
- "5433:5432"
volumes:
- mirth-db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U mirthdb"]
interval: 5s
timeout: 3s
retries: 10
mirth-connect:
image: nextgenhealthcare/connect:4.5.2
container_name: mirth-connect
depends_on:
mirth-db:
condition: service_healthy
environment:
DATABASE: postgres
DATABASE_URL: jdbc:postgresql://mirth-db:5432/mirthdb
DATABASE_USERNAME: mirthdb
DATABASE_PASSWORD: mirthdb
VMOPTIONS: "-Xmx512m"
ports:
- "8443:8443" # Mirth Admin GUI (HTTPS)
- "8082:8080" # HTTP Listener
- "6661:6661" # MLLP Listener - FHIR Bundle Channel
volumes:
- mirth-appdata:/opt/connect/appdata
restart: unless-stopped
volumes:
mirth-db-data:
mirth-appdata: Start with docker compose up -d. The Mirth admin GUI is at https://localhost:8443. Default credentials: admin / admin.
Channel Configuration
Create a new channel in the Mirth GUI:
- Name:
HL7v2 ADT to FHIR Bundle - Source Connector: TCP Listener
- Transmission Mode: MLLP (Start byte
0x0B, End bytes0x1C 0x0D) - Listen Address:
0.0.0.0 - Port:
6661 - Max Connections: 10
- Keep Connection Open: Yes
- Transmission Mode: MLLP (Start byte
- Source Inbound Data Type: HL7 v2.x
- Destination: Channel Writer (stores the FHIR Bundle as channel output)
The MLLP framing is critical. HL7v2 messages over TCP are wrapped in MLLP (Minimal Lower Layer Protocol) with a vertical tab (0x0B) start byte and file separator + carriage return (0x1C 0x0D) end bytes. Without these, TCP cannot determine where one message ends and the next begins.
The Input: An HL7v2 ADT^A01 Message
Here is the complete HL7v2 message that drives every example in this post. This is a realistic admit message for a 78-year-old patient presenting with sepsis at Massachusetts General Hospital:
MSH|^~\&|ADT_SYS|MGH|FHIR_ENGINE|MAIN|20250525143022||ADT^A01|MSG001|P|2.5.1
EVN|A01|20250525143022
PID|1||MRN-2025-001^^^MGH^MR||CHEN^MARGARET^L||19470312|F|||42 Beacon Street^^Boston^MA^02108||617-555-0142|||M
PV1|1|I|ICU^301^A|E|||DOC001^WILLIAMS^SARAH^^^MD|DOC002^KUMAR^RAJESH^^^MD||MED|||||||V12345
IN1|1|MCARE|MCARE|Medicare|||||||GRP-88901
DG1|1||A41.9^Sepsis, unspecified organism^ICD10||20250525|A
NK1|1|CHEN^DAVID||617-555-0199||EC Let us trace every segment:
| Segment | Purpose | Key Fields |
|---|---|---|
| MSH | Message header | Sending facility MGH, message type ADT^A01, HL7 version 2.5.1 |
| EVN | Event type | Admit event at 2025-05-25 14:30:22 |
| PID | Patient identification | MRN MRN-2025-001, Margaret L. Chen, DOB 1947-03-12, Female, Married |
| PV1 | Patient visit | Inpatient (I), ICU Room 301 Bed A, Emergency admit, Dr. Sarah Williams |
| IN1 | Insurance | Medicare (code MCARE), Group GRP-88901 |
| DG1 | Diagnosis | A41.9 -- Sepsis, unspecified organism (ICD-10-CM) |
| NK1 | Next of kin | David Chen, Emergency Contact |
This single message will trigger three business rules (sepsis bundle, geriatric screening, ICU admission) and generate 9 FHIR resources.
The Eight Mapping Tables
The core of any HL7v2-to-FHIR transformation is the mapping layer. HL7v2 uses terse, implementation-specific codes. FHIR uses structured CodeableConcepts with canonical system URIs. The gap between M (HL7v2 gender) and {system: "http://hl7.org/fhir/administrative-gender", code: "male"} is where mapping tables live.
We define eight lookup tables in the transformer's JavaScript.
1. Facility Map (MSH.4 -> Organization)
Translates the HL7v2 sending facility code from MSH-4 into a full Organization name and NPI (National Provider Identifier).
var FACILITY_MAP = {
'MGH': {name: 'Massachusetts General Hospital', npi: '1234567890'},
'BOSTON_MED': {name: 'Boston Medical Center', npi: '0987654321'},
'CHILDREN': {name: 'Boston Children\'s Hospital', npi: '1112223334'},
'BWH': {name: 'Brigham and Women\'s Hospital', npi: '5556667778'},
'MAIN_HOSP': {name: 'Main Street Hospital', npi: '9998887776'}
}; | HL7v2 MSH.4 | FHIR Organization.name | NPI |
|---|---|---|
MGH | Massachusetts General Hospital | 1234567890 |
BOSTON_MED | Boston Medical Center | 0987654321 |
BWH | Brigham and Women's Hospital | 5556667778 |
The NPI feeds into the Organization resource's identifier array with system http://hl7.org/fhir/sid/us-npi, which is required by US Core for organization identification.
2. Ward/Service Map (PV1.3.1 -> Specialty + SNOMED)
Maps the ward/unit code from PV1-3 to a clinical specialty name and its SNOMED CT code for the Encounter's serviceType.
var WARD_SERVICE_MAP = {
'ICU': {specialty: 'Critical Care Medicine', snomed: '309904001'},
'CCU': {specialty: 'Cardiology', snomed: '309904001'},
'ER': {specialty: 'Emergency Medicine', snomed: '310000008'},
'SURG': {specialty: 'General Surgery', snomed: '394294004'},
'MED': {specialty: 'Internal Medicine', snomed: '394802001'},
'PEDS': {specialty: 'Pediatrics', snomed: '394537008'},
'ONCO': {specialty: 'Oncology', snomed: '394593009'}
}; For our patient in ICU, this resolves to Critical Care Medicine with SNOMED code 309904001. The code goes into both the Encounter's serviceType and the attending Practitioner's qualification.
3. Payer Map (IN1.3 -> Coverage)
Translates the insurance company identifier from IN1-3 into a payer name and plan type, used to construct the FHIR Coverage resource.
var PAYER_MAP = {
'BCBS': {name: 'Blue Cross Blue Shield', type: 'PPO'},
'AETNA': {name: 'Aetna Health Plans', type: 'HMO'},
'UHC': {name: 'UnitedHealthcare', type: 'PPO'},
'MCARE': {name: 'Medicare', type: 'Medicare'},
'MCAID': {name: 'MassHealth (Medicaid)', type: 'Medicaid'},
'SELF': {name: 'Self Pay', type: 'Self'}
}; When the IN1 segment is absent entirely, no Coverage resource is generated and a no-insurance Flag is created instead, triggering a financial counseling referral notification.
4. Critical Diagnosis Map (DG1.3 prefix -> Clinical Alert)
This table maps ICD-10-CM code prefixes (the first three characters) to clinical alerts, priority levels, and specialty teams.
var CRITICAL_DX = {
'I21': {priority: 'STAT', alert: 'STEMI/NSTEMI Protocol', team: 'Cardiology'},
'I46': {priority: 'STAT', alert: 'Cardiac Arrest Protocol', team: 'Code Blue'},
'A41': {priority: 'URGENT', alert: 'Sepsis Bundle (SEP-1)', team: 'Infectious Disease'},
'I63': {priority: 'STAT', alert: 'Stroke Code', team: 'Neurology'},
'K35': {priority: 'URGENT', alert: 'Acute Abdomen', team: 'Surgery'},
'S72': {priority: 'URGENT', alert: 'Hip Fracture Protocol', team: 'Orthopedics'},
'J18': {priority: 'URGENT', alert: 'Pneumonia Bundle (PN-6)', team: 'Pulmonology'},
'E11': {priority: 'ROUTINE', alert: 'Diabetes Care Protocol', team: 'Endocrinology'}
}; For our patient with A41.9, the prefix A41 triggers the sepsis bundle. This is one of CMS's core measures -- the SEP-1 bundle requires lactate measurement within 3 hours, blood cultures before antibiotics, and broad-spectrum antibiotic administration within 1 hour. The transformer encodes this as a FHIR Flag resource with the clinical protocol details.
5. ICD-10 to SNOMED CT Crosswalk
This is the dual-coding table. CMS and payers require ICD-10-CM codes for billing. Clinical decision support systems and FHIR-native applications prefer SNOMED CT. The Condition resource carries both.
var ICD_TO_SNOMED = {
'A41.9': {code: '91302008', display: 'Sepsis (disorder)'},
'I21.9': {code: '22298006', display: 'Myocardial infarction (disorder)'},
'J18.9': {code: '233604007', display: 'Pneumonia (disorder)'},
'K35.80': {code: '74400008', display: 'Appendicitis (disorder)'},
'E11.9': {code: '44054006', display: 'Diabetes mellitus type 2 (disorder)'},
'I10': {code: '38341003', display: 'Hypertensive disorder (disorder)'},
'R07.9': {code: '29857009', display: 'Chest pain (finding)'},
'I63.9': {code: '230690007', display: 'Cerebrovascular accident (disorder)'},
'S52.501A':{code: '263102004', display: 'Fracture of radius (disorder)'},
'S62.001A':{code: '65966004', display: 'Fracture of wrist (disorder)'},
'J06.9': {code: '54150009', display: 'Upper respiratory infection (disorder)'},
'S72.001A':{code: '5913000', display: 'Fracture of neck of femur (disorder)'}
}; For A41.9, the Condition resource will carry two codings:
| System | Code | Display |
|---|---|---|
http://hl7.org/fhir/sid/icd-10-cm | A41.9 | Sepsis, unspecified organism |
http://snomed.info/sct | 91302008 | Sepsis (disorder) |
This dual-coding pattern follows the FHIR CodeableConcept design -- multiple codings from different systems on the same concept, with the text field providing a human-readable fallback.
6-8. Simple Code Maps (Gender, Patient Class, Admit Type, Marital Status)
These are direct one-to-one translations from HL7v2 Table values to FHIR ValueSets.
// Gender: HL7v2 Table 0001 -> FHIR administrative-gender
var gMap = {'M':'male', 'F':'female', 'O':'other'};
// Patient Class: HL7v2 Table 0004 -> FHIR v3-ActCode
var cMap = {
'I': {c:'IMP', d:'inpatient'},
'O': {c:'AMB', d:'ambulatory'},
'E': {c:'EMER', d:'emergency'}
};
// Admit Type: HL7v2 Table 0007 -> Display text
var aMap = {'E':'Emergency', 'R':'Routine', 'U':'Urgent', 'N':'Newborn'};
// Marital Status: HL7v2 Table 0002 -> v3-MaritalStatus
var MARITAL_MAP = {
'S':'Never Married', 'M':'Married', 'D':'Divorced', 'W':'Widowed'
}; | HL7v2 Field | HL7v2 Code | FHIR System | FHIR Code | FHIR Display |
|---|---|---|---|---|
| PID.8 (Gender) | F | administrative-gender | female | female |
| PV1.2 (Patient Class) | I | v3-ActCode | IMP | inpatient |
| PV1.4 (Admit Type) | E | admit-type | E | Emergency |
| PID.16 (Marital) | M | v3-MaritalStatus | M | Married |
The Business Rules Engine
Mapping tables translate codes. Business rules act on clinical context. This is where the transformation becomes a clinical decision support pipeline.
The rules engine evaluates five conditions and generates FHIR Flag resources and notification triggers accordingly.
Rule 1: Critical Diagnosis Alerting
if (dx) {
var prefix = dx.substring(0, 3);
var crit = CRITICAL_DX[prefix];
if (crit) {
alerts.push({
priority: crit.priority,
alert: crit.alert,
team: crit.team,
code: dx
});
}
} Scenario: Margaret Chen presents with A41.9. The prefix A41 matches the CRITICAL_DX table. Result: URGENT priority, Sepsis Bundle (SEP-1) alert, Infectious Disease team assignment.
Rule 2: Geriatric Screening
if (age >= 65 && patientClass == 'I') {
flags.push({
code: 'geriatric-screen',
display: 'Geriatric Screening Required',
reason: 'Inpatient age >= 65 (age: ' + age + ')'
});
notifs.push('Geriatric consult auto-ordered');
} Scenario: Margaret Chen is 78 years old (DOB 1947-03-12) and her patient class is I (inpatient). Both conditions are true. A geriatric-screen Flag is generated and a geriatric consult is auto-ordered. This aligns with the American Geriatrics Society recommendation that all hospitalized adults 65 and older receive a geriatric assessment.
Rule 3: ICU Admission Detection
if (ward.toUpperCase().indexOf('ICU') >= 0) {
flags.push({
code: 'icu-admit',
display: 'ICU Admission',
reason: 'Ward: ' + ward
});
notifs.push('ICU bed management + pharmacy notified');
} Scenario: PV1.3.1 contains ICU. The flag triggers bed management and pharmacy notifications. ICU admissions carry distinct formulary requirements (sedatives, vasopressors, insulin drips) that pharmacy needs to prepare proactively.
Rule 4: Insurance Validation
if (!pay) {
flags.push({
code: 'no-insurance',
display: 'Insurance Missing',
reason: 'No IN1 segment'
});
notifs.push('Financial counseling referral');
} Scenario: If the ADT message arrives without an IN1 segment, the patient has no insurance on file. Under EMTALA, the hospital must treat the patient regardless -- but financial counseling should engage early. Margaret Chen has Medicare, so this rule does not fire.
Rule 5: Sepsis-Specific SEP-1 Timer
if (dx && dx.substring(0, 3) == 'A41') {
flags.push({
code: 'sep-1',
display: 'SEP-1 Sepsis Bundle Required',
reason: 'DX: ' + dx
});
notifs.push('SEP-1 timer: Lactate 3h, cultures before ABX, broad-spectrum ABX 1h');
} Scenario: A41.9 fires this rule. The SEP-1 bundle is a CMS quality measure (National Quality Forum #0500) with strict time windows: serum lactate ordered within 3 hours, blood cultures drawn before antibiotics, and broad-spectrum antibiotics administered within 1 hour of presentation. The Flag captures this for downstream clinical workflow systems.
Rule 6: Elderly Hip Fracture Fast-Track
if (age >= 65 && dx && dx.substring(0, 3) == 'S72') {
flags.push({
code: 'hip-fx-elderly',
display: 'Elderly Hip Fracture Fast-Track',
reason: 'S72.x + age >= 65'
});
alerts.push({
priority: 'STAT',
alert: 'Target OR within 24h',
team: 'Ortho+Geriatrics',
code: dx
});
} Scenario: If a 78-year-old patient presented with S72.001A (fracture of neck of femur) instead of sepsis, this rule would fire alongside the geriatric screening rule. Hip fracture in the elderly carries 30-day mortality of 5-10%; evidence shows surgery within 24 hours improves outcomes significantly. This rule does not fire for Margaret Chen because her diagnosis is A41.9, not S72.x.
Live Terminology Validation: Calling tx.fhir.org
Before building the FHIR Bundle, the transformer validates the incoming ICD-10-CM code against the HL7 public FHIR Terminology Server at tx.fhir.org. This is optional but demonstrates a production pattern: confirming that the diagnosis code exists in the code system before embedding it in the output.
The Java 17 HTTP Problem
Mirth Connect 4.5.2 runs on Java 17 (OpenJDK Temurin 17.0.13). The Rhino JavaScript engine resolves java.net.URL to an internal sun.net.www.protocol.http.HttpURLConnection class that is blocked by Java 17's module system. This is a known Mirth issue (GitHub #6254).
The workaround: copy Apache HttpClient JARs (httpclient-4.5.13.jar and httpcore-4.4.13.jar) to /opt/connect/custom-lib/, set server.includecustomlib=true in Mirth's configuration, and use Packages.org.apache.http.* in the Rhino transformer.
The Validation Code
var txTestResult = 'NOT_TESTED';
try {
var HC = Packages.org.apache.http.impl.client.HttpClients;
var cl = HC.createDefault();
var hg = new Packages.org.apache.http.client.methods.HttpGet(
'https://tx.fhir.org/r4/CodeSystem/$lookup' +
'?system=http%3A%2F%2Fhl7.org%2Ffhir%2Fsid%2Ficd-10-cm' +
'&code=A41.9'
);
hg.addHeader('Accept', 'application/fhir+json');
var resp = cl.execute(hg);
var body = Packages.org.apache.http.util.EntityUtils
.toString(resp.getEntity(), 'UTF-8');
resp.close();
cl.close();
var parsed = JSON.parse(body);
var params = parsed.parameter || [];
for (var pi = 0; pi < params.length; pi++) {
if (params[pi].name == 'display') {
txTestResult = 'VALIDATED: ' + params[pi].valueString;
}
}
} catch(txe) {
txTestResult = 'ERROR: ' + txe;
}
channelMap.put('txTestResult', txTestResult); The $lookup operation against tx.fhir.org returns a FHIR Parameters resource. We extract the display parameter to confirm the code resolves to "Sepsis, unspecified organism". Response time is typically 0.8-1.1 seconds per call, which is acceptable during admission processing but would need caching in a high-throughput environment.
FHIR Bundle Assembly: Resource by Resource
With all mappings applied and business rules evaluated, the transformer constructs a FHIR R4 Transaction Bundle. Each resource gets a PUT request entry, making the Bundle idempotent -- safe to reprocess if the same ADT message arrives twice.
Resource 1: Patient
Source segments: PID (all fields), partial PV1 (for MRN context).
var patient = {
resourceType: 'Patient',
id: mrn,
meta: {
profile: [
'http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient'
]
},
identifier: [{
use: 'usual',
type: {
coding: [{
system: 'http://terminology.hl7.org/CodeSystem/v2-0203',
code: 'MR'
}]
},
system: 'urn:oid:2.16.840.1.113883.1.' + fac.npi,
value: mrn
}],
active: true,
name: [{use: 'official', family: lastName, given: [firstName]}],
gender: fGender,
birthDate: fDob,
address: [{
use: 'home',
line: [street],
city: city,
state: state,
postalCode: zip,
country: 'US'
}],
telecom: [{system: 'phone', value: phone, use: 'home'}]
};
// Conditional marital status
if (MARITAL_MAP[marital]) {
patient.maritalStatus = {
coding: [{
system: 'http://terminology.hl7.org/CodeSystem/v3-MaritalStatus',
code: marital,
display: MARITAL_MAP[marital]
}]
};
} Key mapping decisions:
- MRN system OID: Built from
urn:oid:2.16.840.1.113883.1.plus the facility NPI. In production, this should be the actual OID assigned to the facility's MRN namespace. - US Core profile: The
meta.profiledeclares conformance tous-core-patient, which mandatesname,gender, andidentifieras MustSupport elements. - Date format: HL7v2 uses
YYYYMMDD(e.g.,19470312). FHIR usesYYYY-MM-DD(e.g.,1947-03-12). Simple substring extraction handles this.
Resource 2: Organization
Source: MSH.4 + FACILITY_MAP lookup.
var org = {
resourceType: 'Organization',
id: 'org-' + facility,
identifier: [{
system: 'http://hl7.org/fhir/sid/us-npi',
value: fac.npi
}],
name: fac.name
}; Resource 3: Practitioner
Source: PV1.7 (attending physician) + WARD_SERVICE_MAP lookup for specialty.
var pract = {
resourceType: 'Practitioner',
id: 'pract-' + attId,
identifier: [{
system: 'http://hospital.org/practitioners',
value: attId
}],
name: [{family: attLast, given: [attFirst]}],
qualification: [{
code: {
coding: [{
system: 'http://snomed.info/sct',
code: svc.snomed,
display: svc.specialty
}]
}
}]
}; The Practitioner's qualification is driven by the ward-to-specialty mapping, not by data in the HL7v2 message. This is a practical decision -- the PV1.7 component gives us an ID and name but not the physician's specialty. The ward assignment is a reasonable proxy.
Resource 4: Encounter
Source: PV1 (patient class, ward, room, bed, admit type, visit number), plus references to Patient, Practitioner, and Organization.
var enc = {
resourceType: 'Encounter',
id: 'enc-' + (visitNum || mrn),
status: 'in-progress',
'class': {
system: 'http://terminology.hl7.org/CodeSystem/v3-ActCode',
code: encClass.c,
display: encClass.d
},
type: [{
coding: [{
system: 'http://terminology.hl7.org/CodeSystem/admit-type',
code: admitType,
display: fAdmit
}]
}],
serviceType: {
coding: [{
system: 'http://snomed.info/sct',
code: svc.snomed,
display: svc.specialty
}]
},
subject: {
reference: 'Patient/' + mrn,
display: firstName + ' ' + lastName
},
participant: [{
individual: {
reference: 'Practitioner/pract-' + attId,
display: attFirst + ' ' + attLast
}
}],
location: [{
location: {
display: ward + ' Room ' + room + ' Bed ' + bed
},
status: 'active'
}],
serviceProvider: {
reference: 'Organization/org-' + facility,
display: fac.name
}
}; Note the JavaScript reserved word issue: class is a reserved word in JavaScript, so we access it as 'class' (quoted property) rather than .class. The FHIR Encounter's class element uses the v3-ActCode ValueSet where IMP = inpatient, AMB = ambulatory, and EMER = emergency.
Resource 5: Condition (Conditional -- requires DG1)
Source: DG1.3 + ICD_TO_SNOMED crosswalk for dual coding.
if (dx) {
var cond = {
resourceType: 'Condition',
id: 'cond-' + mrn + '-' + dx.replace(/\./g, '-'),
clinicalStatus: {
coding: [{
system: 'http://terminology.hl7.org/CodeSystem/condition-clinical',
code: 'active'
}]
},
code: {
coding: [
{system: fDxSys, code: dx, display: dxDesc}
].concat(
ICD_TO_SNOMED[dx]
? [{
system: 'http://snomed.info/sct',
code: ICD_TO_SNOMED[dx].code,
display: ICD_TO_SNOMED[dx].display
}]
: []
),
text: dxDesc
},
subject: {reference: 'Patient/' + mrn},
encounter: {reference: 'Encounter/enc-' + (visitNum || mrn)}
};
bundle.entry.push({
resource: cond,
request: {method: 'PUT', url: 'Condition/cond-' + mrn + '-' + dx.replace(/\./g, '-')}
});
} The dual-coding implementation uses Array.concat() to conditionally append the SNOMED coding only when a crosswalk entry exists. If the ICD-10 code is not in our ICD_TO_SNOMED table, the Condition still gets the ICD-10 coding alone -- it degrades gracefully.
Resource 6: Coverage (Conditional -- requires IN1)
Source: IN1.3, IN1.4, IN1.8 + PAYER_MAP lookup.
if (pay) {
var cov = {
resourceType: 'Coverage',
id: 'cov-' + mrn,
status: 'active',
beneficiary: {reference: 'Patient/' + mrn},
payor: [{display: pay.name}],
'class': [
{
type: {coding: [{code: 'group'}]},
value: grpNum,
name: pay.name
},
{
type: {coding: [{code: 'plan'}]},
value: pay.type
}
]
};
bundle.entry.push({
resource: cov,
request: {method: 'PUT', url: 'Coverage/cov-' + mrn}
});
} Resources 7-9: Flags (Conditional -- per business rule)
Each triggered business rule generates a Flag resource.
for (var i = 0; i < flags.length; i++) {
var fl = {
resourceType: 'Flag',
id: 'flag-' + mrn + '-' + flags[i].code,
status: 'active',
code: {
coding: [{
system: 'http://hospital.org/flags',
code: flags[i].code,
display: flags[i].display
}],
text: flags[i].display + ' (' + flags[i].reason + ')'
},
subject: {reference: 'Patient/' + mrn}
};
bundle.entry.push({
resource: fl,
request: {method: 'PUT', url: 'Flag/flag-' + mrn + '-' + flags[i].code}
});
} For Margaret Chen's case, three Flags are generated: geriatric-screen, icu-admit, and sep-1.
The Complete Transformer Code
Here is the full, unabridged JavaScript transformer. This is the exact code running inside the Mirth Connect channel.
// =====================================================
// HL7v2 ADT -> FHIR R4 Bundle + Business Rules
// =====================================================
// === MAPPING TABLES ===
var FACILITY_MAP = {
'MGH': {name: 'Massachusetts General Hospital', npi: '1234567890'},
'BOSTON_MED': {name: 'Boston Medical Center', npi: '0987654321'},
'CHILDREN': {name: 'Boston Children\'s Hospital', npi: '1112223334'},
'BWH': {name: 'Brigham and Women\'s Hospital', npi: '5556667778'},
'MAIN_HOSP': {name: 'Main Street Hospital', npi: '9998887776'}
};
var WARD_SERVICE_MAP = {
'ICU': {specialty: 'Critical Care Medicine', snomed: '309904001'},
'CCU': {specialty: 'Cardiology', snomed: '309904001'},
'ER': {specialty: 'Emergency Medicine', snomed: '310000008'},
'SURG': {specialty: 'General Surgery', snomed: '394294004'},
'MED': {specialty: 'Internal Medicine', snomed: '394802001'},
'PEDS': {specialty: 'Pediatrics', snomed: '394537008'},
'ONCO': {specialty: 'Oncology', snomed: '394593009'}
};
var PAYER_MAP = {
'BCBS': {name: 'Blue Cross Blue Shield', type: 'PPO'},
'AETNA': {name: 'Aetna Health Plans', type: 'HMO'},
'UHC': {name: 'UnitedHealthcare', type: 'PPO'},
'MCARE': {name: 'Medicare', type: 'Medicare'},
'MCAID': {name: 'MassHealth (Medicaid)', type: 'Medicaid'},
'SELF': {name: 'Self Pay', type: 'Self'}
};
var CRITICAL_DX = {
'I21': {priority: 'STAT', alert: 'STEMI/NSTEMI Protocol', team: 'Cardiology'},
'I46': {priority: 'STAT', alert: 'Cardiac Arrest Protocol', team: 'Code Blue'},
'A41': {priority: 'URGENT', alert: 'Sepsis Bundle (SEP-1)', team: 'Infectious Disease'},
'I63': {priority: 'STAT', alert: 'Stroke Code', team: 'Neurology'},
'K35': {priority: 'URGENT', alert: 'Acute Abdomen', team: 'Surgery'},
'S72': {priority: 'URGENT', alert: 'Hip Fracture Protocol', team: 'Orthopedics'},
'J18': {priority: 'URGENT', alert: 'Pneumonia Bundle (PN-6)', team: 'Pulmonology'},
'E11': {priority: 'ROUTINE', alert: 'Diabetes Care Protocol', team: 'Endocrinology'}
};
var ICD_TO_SNOMED = {
'A41.9': {code: '91302008', display: 'Sepsis (disorder)'},
'I21.9': {code: '22298006', display: 'Myocardial infarction (disorder)'},
'J18.9': {code: '233604007', display: 'Pneumonia (disorder)'},
'K35.80':{code: '74400008', display: 'Appendicitis (disorder)'},
'E11.9': {code: '44054006', display: 'Diabetes mellitus type 2 (disorder)'},
'I10': {code: '38341003', display: 'Hypertensive disorder (disorder)'},
'R07.9': {code: '29857009', display: 'Chest pain (finding)'},
'I63.9': {code: '230690007', display: 'Cerebrovascular accident (disorder)'},
'S52.501A':{code:'263102004',display: 'Fracture of radius (disorder)'},
'S62.001A':{code:'65966004', display: 'Fracture of wrist (disorder)'},
'J06.9': {code: '54150009', display: 'Upper respiratory infection (disorder)'},
'S72.001A':{code:'5913000', display: 'Fracture of neck of femur (disorder)'}
};
// === LIVE TERMINOLOGY VALIDATION ===
var txTestResult = 'NOT_TESTED';
try {
var HC = Packages.org.apache.http.impl.client.HttpClients;
txTestResult = 'CLASS_FOUND: ' + HC;
var cl = HC.createDefault();
txTestResult = 'CLIENT_CREATED';
var hg = new Packages.org.apache.http.client.methods.HttpGet(
'https://tx.fhir.org/r4/CodeSystem/$lookup?system=' +
'http%3A%2F%2Fhl7.org%2Ffhir%2Fsid%2Ficd-10-cm&code=A41.9'
);
hg.addHeader('Accept', 'application/fhir+json');
txTestResult = 'GET_CREATED';
var resp = cl.execute(hg);
txTestResult = 'EXECUTED: ' + resp.getStatusLine().getStatusCode();
var body = Packages.org.apache.http.util.EntityUtils
.toString(resp.getEntity(), 'UTF-8');
txTestResult = 'BODY_LEN: ' + body.length;
resp.close();
cl.close();
var parsed = JSON.parse(body);
var params = parsed.parameter || [];
for (var pi = 0; pi < params.length; pi++) {
if (params[pi].name == 'display')
txTestResult = 'VALIDATED: ' + params[pi].valueString;
}
} catch(txe) {
txTestResult = 'ERROR: ' + txe;
}
channelMap.put('txTestResult', txTestResult);
var MARITAL_MAP = {
'S':'Never Married', 'M':'Married', 'D':'Divorced', 'W':'Widowed'
};
// === EXTRACT FIELDS FROM HL7v2 MESSAGE ===
var facility = msg['MSH']['MSH.4']['MSH.4.1'].toString();
var mrn = msg['PID']['PID.3']['PID.3.1'].toString();
var lastName = msg['PID']['PID.5']['PID.5.1'].toString();
var firstName = msg['PID']['PID.5']['PID.5.2'].toString();
var dob = msg['PID']['PID.7']['PID.7.1'].toString();
var gender = msg['PID']['PID.8']['PID.8.1'].toString();
var street = msg['PID']['PID.11']['PID.11.1'].toString();
var city = msg['PID']['PID.11']['PID.11.3'].toString();
var state = msg['PID']['PID.11']['PID.11.4'].toString();
var zip = msg['PID']['PID.11']['PID.11.5'].toString();
var phone = msg['PID']['PID.13']['PID.13.1'].toString();
var marital = msg['PID']['PID.16']['PID.16.1'].toString();
var patientClass = msg['PV1']['PV1.2']['PV1.2.1'].toString();
var ward = msg['PV1']['PV1.3']['PV1.3.1'].toString();
var room = msg['PV1']['PV1.3']['PV1.3.2'].toString();
var bed = msg['PV1']['PV1.3']['PV1.3.3'].toString();
var admitType = msg['PV1']['PV1.4']['PV1.4.1'].toString();
var attId = msg['PV1']['PV1.7']['PV1.7.1'].toString();
var attLast = msg['PV1']['PV1.7']['PV1.7.2'].toString();
var attFirst = msg['PV1']['PV1.7']['PV1.7.3'].toString();
var visitNum = msg['PV1']['PV1.19']['PV1.19.1'].toString();
var dx = '', dxDesc = '', dxSys = '';
try {
dx = msg['DG1']['DG1.3']['DG1.3.1'].toString();
dxDesc = msg['DG1']['DG1.3']['DG1.3.2'].toString();
dxSys = msg['DG1']['DG1.3']['DG1.3.3'].toString();
} catch(e) {}
var insId = '', insName = '', grpNum = '';
try {
insId = msg['IN1']['IN1.3']['IN1.3.1'].toString();
insName = msg['IN1']['IN1.4']['IN1.4.1'].toString();
grpNum = msg['IN1']['IN1.8']['IN1.8.1'].toString();
} catch(e) {}
// === APPLY MAPPINGS ===
var gMap = {'M':'male', 'F':'female', 'O':'other'};
var fGender = gMap[gender] || 'unknown';
var cMap = {
'I': {c:'IMP', d:'inpatient'},
'O': {c:'AMB', d:'ambulatory'},
'E': {c:'EMER', d:'emergency'}
};
var encClass = cMap[patientClass] || {c:'AMB', d:'ambulatory'};
var aMap = {'E':'Emergency', 'R':'Routine', 'U':'Urgent', 'N':'Newborn'};
var fAdmit = aMap[admitType] || admitType;
var fac = FACILITY_MAP[facility] || {name: facility, npi: 'UNKNOWN'};
var svc = WARD_SERVICE_MAP[ward.toUpperCase()]
|| {specialty: 'General', snomed: '394802001'};
var pay = insId
? (PAYER_MAP[insId.toUpperCase()] || {name: insName||insId, type: 'Unknown'})
: null;
var fDob = dob.length >= 8
? dob.substring(0,4)+'-'+dob.substring(4,6)+'-'+dob.substring(6,8)
: '';
var age = dob.length >= 8
? new Date().getFullYear() - parseInt(dob.substring(0,4))
: 0;
var fDxSys = dxSys == 'ICD10'
? 'http://hl7.org/fhir/sid/icd-10-cm'
: 'http://snomed.info/sct';
// === BUSINESS RULES ===
var alerts = [];
var flags = [];
var notifs = [];
if (dx) {
var prefix = dx.substring(0, 3);
var crit = CRITICAL_DX[prefix];
if (crit) {
alerts.push({
priority: crit.priority,
alert: crit.alert,
team: crit.team,
code: dx
});
}
}
if (age >= 65 && patientClass == 'I') {
flags.push({
code: 'geriatric-screen',
display: 'Geriatric Screening Required',
reason: 'Inpatient age >= 65 (age: ' + age + ')'
});
notifs.push('Geriatric consult auto-ordered');
}
if (age < 18) {
flags.push({
code: 'pediatric',
display: 'Pediatric Patient',
reason: 'Age < 18'
});
}
if (ward.toUpperCase().indexOf('ICU') >= 0) {
flags.push({
code: 'icu-admit',
display: 'ICU Admission',
reason: 'Ward: ' + ward
});
notifs.push('ICU bed management + pharmacy notified');
}
if (!pay) {
flags.push({
code: 'no-insurance',
display: 'Insurance Missing',
reason: 'No IN1 segment'
});
notifs.push('Financial counseling referral');
}
if (dx && dx.substring(0, 3) == 'A41') {
flags.push({
code: 'sep-1',
display: 'SEP-1 Sepsis Bundle Required',
reason: 'DX: ' + dx
});
notifs.push(
'SEP-1 timer: Lactate 3h, cultures before ABX, broad-spectrum ABX 1h'
);
}
if (age >= 65 && dx && dx.substring(0, 3) == 'S72') {
flags.push({
code: 'hip-fx-elderly',
display: 'Elderly Hip Fracture Fast-Track',
reason: 'S72.x + age >= 65'
});
alerts.push({
priority: 'STAT',
alert: 'Target OR within 24h',
team: 'Ortho+Geriatrics',
code: dx
});
}
// === BUILD FHIR BUNDLE ===
var patient = {
resourceType: 'Patient',
id: mrn,
meta: {
profile: [
'http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient'
]
},
identifier: [{
use: 'usual',
type: {
coding: [{
system: 'http://terminology.hl7.org/CodeSystem/v2-0203',
code: 'MR'
}]
},
system: 'urn:oid:2.16.840.1.113883.1.13.' + fac.npi,
value: mrn
}],
active: true,
name: [{use: 'official', family: lastName, given: [firstName]}],
gender: fGender,
birthDate: fDob,
address: [{
use: 'home',
line: [street],
city: city,
state: state,
postalCode: zip,
country: 'US'
}],
telecom: [{system: 'phone', value: phone, use: 'home'}]
};
if (MARITAL_MAP[marital]) {
patient.maritalStatus = {
coding: [{
system: 'http://terminology.hl7.org/CodeSystem/v3-MaritalStatus',
code: marital,
display: MARITAL_MAP[marital]
}]
};
}
var org = {
resourceType: 'Organization',
id: 'org-' + facility,
identifier: [{
system: 'http://hl7.org/fhir/sid/us-npi',
value: fac.npi
}],
name: fac.name
};
var pract = {
resourceType: 'Practitioner',
id: 'pract-' + attId,
identifier: [{
system: 'http://hospital.org/practitioners',
value: attId
}],
name: [{family: attLast, given: [attFirst]}],
qualification: [{
code: {
coding: [{
system: 'http://snomed.info/sct',
code: svc.snomed,
display: svc.specialty
}]
}
}]
};
var enc = {
resourceType: 'Encounter',
id: 'enc-' + (visitNum || mrn),
status: 'in-progress',
'class': {
system: 'http://terminology.hl7.org/CodeSystem/v3-ActCode',
code: encClass.c,
display: encClass.d
},
type: [{
coding: [{
system: 'http://terminology.hl7.org/CodeSystem/admit-type',
code: admitType,
display: fAdmit
}]
}],
serviceType: {
coding: [{
system: 'http://snomed.info/sct',
code: svc.snomed,
display: svc.specialty
}]
},
subject: {
reference: 'Patient/' + mrn,
display: firstName + ' ' + lastName
},
participant: [{
individual: {
reference: 'Practitioner/pract-' + attId,
display: attFirst + ' ' + attLast
}
}],
location: [{
location: {
display: ward + ' Room ' + room + ' Bed ' + bed
},
status: 'active'
}],
serviceProvider: {
reference: 'Organization/org-' + facility,
display: fac.name
}
};
var bundle = {
resourceType: 'Bundle',
type: 'transaction',
timestamp: new Date().toISOString(),
entry: [
{resource: patient, request: {method:'PUT', url:'Patient/'+mrn}},
{resource: org, request: {method:'PUT', url:'Organization/org-'+facility}},
{resource: pract, request: {method:'PUT', url:'Practitioner/pract-'+attId}},
{resource: enc, request: {method:'PUT', url:'Encounter/enc-'+(visitNum||mrn)}}
]
};
if (dx) {
var condId = 'cond-' + mrn + '-' + dx.replace(/\./g, '-');
var cond = {
resourceType: 'Condition',
id: condId,
clinicalStatus: {
coding: [{
system: 'http://terminology.hl7.org/CodeSystem/condition-clinical',
code: 'active'
}]
},
code: {
coding: [{system: fDxSys, code: dx, display: dxDesc}].concat(
ICD_TO_SNOMED[dx]
? [{
system: 'http://snomed.info/sct',
code: ICD_TO_SNOMED[dx].code,
display: ICD_TO_SNOMED[dx].display
}]
: []
),
text: dxDesc
},
subject: {reference: 'Patient/' + mrn},
encounter: {reference: 'Encounter/enc-' + (visitNum || mrn)}
};
bundle.entry.push({
resource: cond,
request: {method: 'PUT', url: 'Condition/' + condId}
});
}
if (pay) {
var cov = {
resourceType: 'Coverage',
id: 'cov-' + mrn,
status: 'active',
beneficiary: {reference: 'Patient/' + mrn},
payor: [{display: pay.name}],
'class': [
{
type: {coding: [{code: 'group'}]},
value: grpNum,
name: pay.name
},
{
type: {coding: [{code: 'plan'}]},
value: pay.type
}
]
};
bundle.entry.push({
resource: cov,
request: {method: 'PUT', url: 'Coverage/cov-' + mrn}
});
}
for (var i = 0; i < flags.length; i++) {
var fl = {
resourceType: 'Flag',
id: 'flag-' + mrn + '-' + flags[i].code,
status: 'active',
code: {
coding: [{
system: 'http://hospital.org/flags',
code: flags[i].code,
display: flags[i].display
}],
text: flags[i].display + ' (' + flags[i].reason + ')'
},
subject: {reference: 'Patient/' + mrn}
};
bundle.entry.push({
resource: fl,
request: {method: 'PUT', url: 'Flag/flag-' + mrn + '-' + flags[i].code}
});
}
// === OUTPUT ===
var rules = {
alerts: alerts,
flags: flags,
notifications: notifs,
mappings: {
facility: facility + ' -> ' + fac.name,
ward: ward + ' -> ' + svc.specialty,
gender: gender + ' -> ' + fGender,
class: patientClass + ' -> ' + encClass.d,
admit: admitType + ' -> ' + fAdmit,
insurance: (insId||'NONE') + ' -> ' + (pay ? pay.name : 'Not provided'),
snomedMapping: (dx && ICD_TO_SNOMED[dx])
? dx + ' -> SNOMED ' + ICD_TO_SNOMED[dx].code
+ ' (' + ICD_TO_SNOMED[dx].display + ')'
: 'No SNOMED mapping',
marital: marital + ' -> ' + (MARITAL_MAP[marital] || 'N/A')
}
};
channelMap.put('fhirBundle', JSON.stringify(bundle, null, 2));
channelMap.put('businessRules', JSON.stringify(rules, null, 2));
channelMap.put('resourceCount', bundle.entry.length.toString());
channelMap.put('alertCount', alerts.length.toString());
channelMap.put('flagCount', flags.length.toString());
logger.info(
'Bundle: ' + bundle.entry.length + ' resources, '
+ alerts.length + ' alerts, ' + flags.length + ' flags | '
+ firstName + ' ' + lastName
); The Complete Output: FHIR R4 Transaction Bundle
Here is the full FHIR Bundle JSON produced from the sample HL7v2 message above. Nine resources: Patient, Organization, Practitioner, Encounter, Condition (with dual coding), Coverage, and three Flags.
{
"resourceType": "Bundle",
"type": "transaction",
"timestamp": "2025-05-25T14:30:22.000Z",
"entry": [
{
"resource": {
"resourceType": "Patient",
"id": "MRN-2025-001",
"meta": {
"profile": [
"http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient"
]
},
"identifier": [
{
"use": "usual",
"type": {
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/v2-0203",
"code": "MR"
}
]
},
"system": "urn:oid:2.16.840.1.113883.1.13.1234567890",
"value": "MRN-2025-001"
}
],
"active": true,
"name": [
{
"use": "official",
"family": "CHEN",
"given": ["MARGARET"]
}
],
"gender": "female",
"birthDate": "1947-03-12",
"address": [
{
"use": "home",
"line": ["42 Beacon Street"],
"city": "Boston",
"state": "MA",
"postalCode": "02108",
"country": "US"
}
],
"telecom": [
{
"system": "phone",
"value": "617-555-0142",
"use": "home"
}
],
"maritalStatus": {
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/v3-MaritalStatus",
"code": "M",
"display": "Married"
}
]
}
},
"request": {
"method": "PUT",
"url": "Patient/MRN-2025-001"
}
},
{
"resource": {
"resourceType": "Organization",
"id": "org-MGH",
"identifier": [
{
"system": "http://hl7.org/fhir/sid/us-npi",
"value": "1234567890"
}
],
"name": "Massachusetts General Hospital"
},
"request": {
"method": "PUT",
"url": "Organization/org-MGH"
}
},
{
"resource": {
"resourceType": "Practitioner",
"id": "pract-DOC001",
"identifier": [
{
"system": "http://hospital.org/practitioners",
"value": "DOC001"
}
],
"name": [
{
"family": "WILLIAMS",
"given": ["SARAH"]
}
],
"qualification": [
{
"code": {
"coding": [
{
"system": "http://snomed.info/sct",
"code": "309904001",
"display": "Critical Care Medicine"
}
]
}
}
]
},
"request": {
"method": "PUT",
"url": "Practitioner/pract-DOC001"
}
},
{
"resource": {
"resourceType": "Encounter",
"id": "enc-V12345",
"status": "in-progress",
"class": {
"system": "http://terminology.hl7.org/CodeSystem/v3-ActCode",
"code": "IMP",
"display": "inpatient"
},
"type": [
{
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/admit-type",
"code": "E",
"display": "Emergency"
}
]
}
],
"serviceType": {
"coding": [
{
"system": "http://snomed.info/sct",
"code": "309904001",
"display": "Critical Care Medicine"
}
]
},
"subject": {
"reference": "Patient/MRN-2025-001",
"display": "MARGARET CHEN"
},
"participant": [
{
"individual": {
"reference": "Practitioner/pract-DOC001",
"display": "SARAH WILLIAMS"
}
}
],
"location": [
{
"location": {
"display": "ICU Room 301 Bed A"
},
"status": "active"
}
],
"serviceProvider": {
"reference": "Organization/org-MGH",
"display": "Massachusetts General Hospital"
}
},
"request": {
"method": "PUT",
"url": "Encounter/enc-V12345"
}
},
{
"resource": {
"resourceType": "Condition",
"id": "cond-MRN-2025-001-A41-9",
"clinicalStatus": {
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/condition-clinical",
"code": "active"
}
]
},
"code": {
"coding": [
{
"system": "http://hl7.org/fhir/sid/icd-10-cm",
"code": "A41.9",
"display": "Sepsis, unspecified organism"
},
{
"system": "http://snomed.info/sct",
"code": "91302008",
"display": "Sepsis (disorder)"
}
],
"text": "Sepsis, unspecified organism"
},
"subject": {
"reference": "Patient/MRN-2025-001"
},
"encounter": {
"reference": "Encounter/enc-V12345"
}
},
"request": {
"method": "PUT",
"url": "Condition/cond-MRN-2025-001-A41-9"
}
},
{
"resource": {
"resourceType": "Coverage",
"id": "cov-MRN-2025-001",
"status": "active",
"beneficiary": {
"reference": "Patient/MRN-2025-001"
},
"payor": [
{
"display": "Medicare"
}
],
"class": [
{
"type": {
"coding": [
{
"code": "group"
}
]
},
"value": "GRP-88901",
"name": "Medicare"
},
{
"type": {
"coding": [
{
"code": "plan"
}
]
},
"value": "Medicare"
}
]
},
"request": {
"method": "PUT",
"url": "Coverage/cov-MRN-2025-001"
}
},
{
"resource": {
"resourceType": "Flag",
"id": "flag-MRN-2025-001-geriatric-screen",
"status": "active",
"code": {
"coding": [
{
"system": "http://hospital.org/flags",
"code": "geriatric-screen",
"display": "Geriatric Screening Required"
}
],
"text": "Geriatric Screening Required (Inpatient age >= 65 (age: 78))"
},
"subject": {
"reference": "Patient/MRN-2025-001"
}
},
"request": {
"method": "PUT",
"url": "Flag/flag-MRN-2025-001-geriatric-screen"
}
},
{
"resource": {
"resourceType": "Flag",
"id": "flag-MRN-2025-001-icu-admit",
"status": "active",
"code": {
"coding": [
{
"system": "http://hospital.org/flags",
"code": "icu-admit",
"display": "ICU Admission"
}
],
"text": "ICU Admission (Ward: ICU)"
},
"subject": {
"reference": "Patient/MRN-2025-001"
}
},
"request": {
"method": "PUT",
"url": "Flag/flag-MRN-2025-001-icu-admit"
}
},
{
"resource": {
"resourceType": "Flag",
"id": "flag-MRN-2025-001-sep-1",
"status": "active",
"code": {
"coding": [
{
"system": "http://hospital.org/flags",
"code": "sep-1",
"display": "SEP-1 Sepsis Bundle Required"
}
],
"text": "SEP-1 Sepsis Bundle Required (DX: A41.9)"
},
"subject": {
"reference": "Patient/MRN-2025-001"
}
},
"request": {
"method": "PUT",
"url": "Flag/flag-MRN-2025-001-sep-1"
}
}
]
} Business Rules Summary Output
Alongside the FHIR Bundle, the transformer produces a structured rules summary:
{
"alerts": [
{
"priority": "URGENT",
"alert": "Sepsis Bundle (SEP-1)",
"team": "Infectious Disease",
"code": "A41.9"
}
],
"flags": [
{"code": "geriatric-screen", "display": "Geriatric Screening Required", "reason": "Inpatient age >= 65 (age: 78)"},
{"code": "icu-admit", "display": "ICU Admission", "reason": "Ward: ICU"},
{"code": "sep-1", "display": "SEP-1 Sepsis Bundle Required", "reason": "DX: A41.9"}
],
"notifications": [
"Geriatric consult auto-ordered",
"ICU bed management + pharmacy notified",
"SEP-1 timer: Lactate 3h, cultures before ABX, broad-spectrum ABX 1h"
],
"mappings": {
"facility": "MGH -> Massachusetts General Hospital",
"ward": "ICU -> Critical Care Medicine",
"gender": "F -> female",
"class": "I -> inpatient",
"admit": "E -> Emergency",
"insurance": "MCARE -> Medicare",
"snomedMapping": "A41.9 -> SNOMED 91302008 (Sepsis (disorder))",
"marital": "M -> Married"
}
} Segment-to-Resource Mapping Reference
This table provides a complete tracing of every HL7v2 field to its FHIR destination, for the eight segments this channel handles.
| HL7v2 Segment.Field | HL7v2 Description | FHIR Resource.Element | Mapping Logic |
|---|---|---|---|
| MSH.4.1 | Sending Facility | Organization.name, Organization.identifier | FACILITY_MAP lookup |
| PID.3.1 | Patient ID (MRN) | Patient.identifier.value, Patient.id | Direct copy |
| PID.5.1 / PID.5.2 | Patient Name | Patient.name.family / .given | Direct copy |
| PID.7.1 | Date of Birth | Patient.birthDate | YYYYMMDD -> YYYY-MM-DD |
| PID.8.1 | Sex | Patient.gender | gMap: M->male, F->female |
| PID.11.1/.3/.4/.5 | Address | Patient.address | Direct copy to line/city/state/postalCode |
| PID.13.1 | Phone | Patient.telecom | Direct copy, system='phone' |
| PID.16.1 | Marital Status | Patient.maritalStatus | MARITAL_MAP + v3-MaritalStatus system |
| PV1.2.1 | Patient Class | Encounter.class | cMap: I->IMP, O->AMB, E->EMER |
| PV1.3.1/.2/.3 | Assigned Location | Encounter.location.display | Concatenated: "{ward} Room {room} Bed {bed}" |
| PV1.4.1 | Admission Type | Encounter.type | aMap: E->Emergency, R->Routine |
| PV1.7.1/.2/.3 | Attending Doctor | Practitioner.identifier, .name | Direct copy |
| PV1.19.1 | Visit Number | Encounter.id | Prefixed with "enc-" |
| IN1.3.1 | Insurance Company ID | Coverage.class (via PAYER_MAP) | PAYER_MAP lookup for name + type |
| IN1.4.1 | Insurance Company Name | Coverage.payor.display | Fallback if PAYER_MAP miss |
| IN1.8.1 | Group Number | Coverage.class[group].value | Direct copy |
| DG1.3.1 | Diagnosis Code | Condition.code.coding[0].code | Direct + ICD_TO_SNOMED dual code |
| DG1.3.2 | Diagnosis Description | Condition.code.text | Direct copy |
| DG1.3.3 | Coding System | Condition.code.coding[0].system | ICD10 -> hl7.org/fhir/sid/icd-10-cm |
Production Considerations
This channel works. It transforms real HL7v2 messages into valid FHIR R4 Bundles. But moving from a working demo to production deployment introduces considerations that are worth addressing explicitly.
Terminology Server Latency and Caching
The live call to tx.fhir.org adds 0.8-1.1 seconds per message. In an ADT feed processing hundreds of messages per hour, this is unacceptable. Production options:
- Local terminology server: Deploy a local instance of HAPI FHIR or Ontoserver loaded with ICD-10-CM and SNOMED CT. Reduces validation latency to single-digit milliseconds.
- Lookup table caching: For the ICD-10 to SNOMED crosswalk, the local
ICD_TO_SNOMEDtable already provides sub-millisecond lookups. Expand it with the full National Library of Medicine ICD-10-CM to SNOMED CT map file (approximately 70,000 entries). - Batch validation: Queue codes and validate in batch after message processing, flagging mismatches asynchronously.
Error Handling and Dead-Letter Queues
The current code uses try/catch blocks to handle missing segments (DG1, IN1) gracefully. Production requires more:
- Dead-letter channel: Route messages that fail transformation to a dedicated error channel for manual review.
- Segment validation: Check for required field presence before mapping. An MRN that is blank, a DOB that is malformed, or a facility code that is not in the mapping table should be handled with specific error responses.
- ACK/NAK: Return HL7v2 ACK messages with appropriate status codes. A successful transformation should return
AA(Application Accept). A failed transformation should returnAE(Application Error) with details in MSA.3.
FHIR Server Destination
This demo channel writes the FHIR Bundle to channelMap for inspection. In production, the Bundle would be POSTed to a FHIR server:
- Destination: HTTP Sender pointing at your FHIR server's base URL (e.g.,
https://fhir.hospital.org/fhir) - Method: POST
- Content-Type:
application/fhir+json - Authentication: OAuth 2.0 client credentials flow, or mutual TLS depending on your infrastructure
Note the Mirth-specific gotcha: FHIR $lookup URLs contain $ characters that conflict with Mirth's variable interpolation syntax. Build these URLs in JavaScript and pass them via channelMap variables to avoid silent corruption.
Idempotency
The Bundle uses PUT requests with deterministic resource IDs (e.g., Patient/MRN-2025-001, Encounter/enc-V12345). This makes the entire Bundle idempotent -- processing the same ADT message twice produces the same server state. This is essential for HL7v2 feeds, which may retransmit messages after network failures.
Mapping Table Management
In this demo, mapping tables are inline JavaScript objects. In production:
- Code template libraries: Mirth's Code Template feature allows shared JavaScript libraries across channels. Store mapping tables in a Code Template so they can be updated once and used by multiple channels.
- External database: For tables that change frequently (payer codes, facility expansions), query a reference database rather than maintaining static JavaScript objects.
- Version control: Export channels as XML and commit to Git. The mapping tables embedded in the transformer script are versioned alongside the channel configuration.
Multi-DG1 and Multi-IN1 Support
The current transformer reads only the first DG1 and IN1 segment. Real ADT messages can carry multiple diagnoses and multiple insurance segments. Extending this requires iterating over repeating segments:
// Example: handle multiple DG1 segments
var dg1List = msg['DG1'];
for (var d = 0; d < dg1List.length(); d++) {
var dxCode = dg1List[d]['DG1.3']['DG1.3.1'].toString();
// ... create a Condition resource for each
} Testing the Channel
Send the sample HL7v2 message using any MLLP client. Here is a quick test using netcat (or Mirth's built-in message sender):
# Using HAPI TestPanel or any MLLP sender
# Target: localhost:6661
# Or with the Mirth Connect GUI:
# 1. Go to the channel's Messages tab
# 2. Click "Send Message"
# 3. Paste the HL7v2 message
# 4. Click "Process Message" After processing, check the channel's channelMap variables:
- fhirBundle: The complete JSON Bundle (examine in Mirth's message viewer)
- businessRules: The alerts, flags, notifications, and mapping trace
- resourceCount: Number of resources in the Bundle (expect 9 for this message)
- alertCount: Number of clinical alerts triggered (expect 1)
- flagCount: Number of Flag resources generated (expect 3)
- txTestResult: Terminology server validation result (expect
VALIDATED: Sepsis, unspecified organism)
Need help building EHR integration pipelines like this? Explore our Healthcare Interoperability Solutions for end-to-end HL7v2, FHIR, and Mirth Connect implementation. We also offer specialized Healthcare Software Product Development services for production-grade healthcare data pipelines. Talk to our team to get started.
References
- HL7 FHIR R4 Specification: Bundle
- US Core Implementation Guide 5.0.1
- HL7 V2 Table 0001 (Administrative Sex)
- HL7 V2 Table 0004 (Patient Class)
- HL7 V2 Table 0007 (Admission Type)
- FHIR ValueSet: v3-ActCode (Encounter Class)
- FHIR ValueSet: v3-MaritalStatus
- CMS SEP-1 Early Management Bundle (NQF #0500)
- NLM ICD-10-CM to SNOMED CT Map
- 21st Century Cures Act -- ONC Final Rule
- Mirth Connect User Guide: JavaScript Transformer
- tx.fhir.org FHIR Terminology Server
This post is part of our Mirth Connect integration series. The complete channel XML, Docker configuration, and transformer JavaScript are available in our demo repository. Next in the series: building a smart ADT router that distributes messages to specialty-specific downstream systems based on ward, diagnosis, and patient demographics.


