How to transform HL7v2 ADT messages into openEHR FLAT compositions and push them to EHRbase — the missing guide that nobody has written until now.
Reading time: 25 minutes
Skill level: Intermediate to advanced (assumes Mirth Connect experience, introduces openEHR concepts)
What you will build: A production-ready Mirth Connect channel that transforms HL7v2 ADT^A01 messages into openEHR FLAT format compositions and POSTs them to an EHRbase Clinical Data Repository via REST API. 42 mapped fields. Complete working code.
Why This Guide Exists
Search for "Mirth Connect openEHR integration" and you will find nothing. Not a blog post. Not a tutorial. Not a code snippet. The openEHR community has tooling guides for programming languages (Java, Python, .NET) and a few references to generic ETL pipelines, but the most widely deployed healthcare integration engine in the world — Mirth Connect, running in thousands of hospitals across 40+ countries — has zero documented guidance for openEHR integration.
This is a problem because the real world does not run on a single standard. Hospitals in Europe, the Middle East, Southeast Asia, and Australasia are adopting openEHR clinical data repositories (EHRbase, Better Platform, Medblocks, Ocean Informatics) while their upstream systems — lab instruments, ADT systems, pharmacy dispensers, radiology modalities — still speak HL7v2. The integration engine sitting between those two worlds is, more often than not, Mirth Connect.
We built this integration from scratch. No plugin. No library. No connector. Just JavaScript in a Mirth transformer, treating the openEHR CDR as a REST API target. This article documents every decision, every code mapping, and every gotcha so you can reproduce it in your own environment.
Who Needs This
- Hospitals migrating from HL7v2-based EHRs to openEHR CDRs. Your existing systems will not stop sending HL7v2 messages the day you deploy EHRbase. You need a translation layer.
- Dual-standard environments. Some departments run on FHIR, some on openEHR. Mirth is the natural place to fan out to both.
- National health IT programs. Countries mandating openEHR (Norway, Germany, Australia, Brazil, parts of India) while hospitals still run HL7v2-native HIS platforms.
- Integration engineers evaluating openEHR. You know Mirth inside out but openEHR is unfamiliar territory. This guide bridges that gap.
The Problem: Mirth Has Zero openEHR Support
Mirth Connect (NextGen Connect) has built-in support for HL7v2, HL7v3 (CDA), FHIR R4, DICOM, X12, delimited text, JSON, XML, and raw binary. It can parse HL7v2 messages into an E4X XML tree, generate FHIR resources with its FHIR Writer destination, and even validate CDA documents against Schematron rules.
For openEHR, it has nothing. Specifically:
| Capability | FHIR in Mirth | openEHR in Mirth |
|---|---|---|
| Native data type parser | Yes (FHIR R4) | No |
| Template/profile awareness | Partial (FHIR profiles) | No |
| Dedicated connector | FHIR Writer destination | None |
| Message validation | JSON Schema, FHIR profiles | None |
| Query language support | FHIR Search params | No AQL support |
| Community channels | Dozens on Mirth Exchange | Zero |
This means every aspect of the openEHR integration must be hand-built:
- Composition structure — You build the FLAT JSON object key by key in JavaScript.
- Data type mapping —
DV_CODED_TEXT,DV_QUANTITY,DV_DATE_TIMEare openEHR-specific types. You encode them manually using path suffixes (|code,|value,|terminology). - Archetype code mapping — HL7v2 codes like
M/Ffor gender must be mapped to openEHR local archetype codes likeat0003/at0004, which vary by archetype. - Template paths — The flat composition keys follow a hierarchical path structure that mirrors the openEHR template. You construct these strings by hand.
- CDR interaction — EHRbase, Better Platform, and other CDRs expose REST APIs with different quirks. You use Mirth's HTTP Sender like you would for any external REST service.
The upside: once you understand the pattern, openEHR compositions from Mirth are just JSON objects delivered via HTTP POST. There is no magic. There is no black box. There is a JavaScript transformer and an HTTP Sender.
openEHR Concepts for HL7/FHIR Engineers
If you have spent your career in HL7v2 and FHIR, openEHR will feel both familiar and alien. Here is a minimum-viable conceptual map.
The Two-Level Modeling Paradigm
HL7v2 and FHIR define the data model and the information structure in the same specification. A FHIR Patient resource is both the schema and the content definition. If you need to capture something the spec does not have a field for, you extend it (FHIR extensions, Z-segments).
openEHR separates these concerns into two layers:
- Reference Model (RM): Defines generic data structures —
COMPOSITION,SECTION,OBSERVATION,EVALUATION,INSTRUCTION,ACTION, and their data types (DV_TEXT,DV_CODED_TEXT,DV_QUANTITY,DV_DATE_TIME, etc.). This is analogous to the FHIR base resource types, but more abstract. - Archetypes and Templates: Archetypes constrain the Reference Model for specific clinical concepts. The archetype
openEHR-EHR-EVALUATION.problem_diagnosis.v1defines how to capture a diagnosis — what fields exist, what coded values are acceptable, what cardinality rules apply. Templates combine archetypes into usable clinical forms.
Why this matters for Mirth integration: You do not map HL7v2 fields to generic openEHR RM fields. You map them to specific paths within a specific template. The template determines the paths, and the paths are what you write into the FLAT composition keys.
Key Terms, Translated
| openEHR Term | Closest HL7v2/FHIR Equivalent | What It Means |
|---|---|---|
| EHR | Patient compartment | A container for all health records belonging to one patient. Created once per patient in the CDR. |
| Composition | FHIR Bundle / CDA document | A single clinical document (admission record, lab report, discharge summary). The atomic unit of data entry. |
| Template | FHIR StructureDefinition | A concrete clinical form definition built from archetypes. Defines what fields exist and what their paths are. |
| Archetype | FHIR resource definition | A reusable clinical concept model (e.g., "blood pressure", "diagnosis"). Referenced by templates. |
| Section | CDA section | Organizational grouping within a composition (e.g., "Admission Details", "Diagnoses"). |
| DV_CODED_TEXT | CodeableConcept | A coded value with terminology binding. Can reference external terminologies (SNOMED, ICD-10) or local archetype codes (at-codes). |
| at-codes | Answer list codes | Local codes defined within an archetype (e.g., at0003 = Male, at0004 = Female in a gender archetype). |
| FLAT format | (No equivalent) | A flattened key-value representation of a composition. openEHR-specific. |
The Composition Formats: CANONICAL, STRUCTURED, and FLAT
openEHR CDRs accept compositions in three formats:
- CANONICAL (XML/JSON): The full Reference Model serialization. Deeply nested, verbose, and faithful to the RM class hierarchy. For a 42-field admission record, the CANONICAL JSON runs to 400+ lines with nested
archetype_node_id,_type, andnameobjects at every level. - STRUCTURED: A JSON format that preserves the nesting but strips some RM overhead. Still complex. Better than CANONICAL for human readability, worse than FLAT.
- FLAT: A completely flattened key-value map where each field is a single key (the template path) and a simple value. This is what we will use.
Architecture Overview
The architecture is straightforward:
- Source system sends an HL7v2 ADT^A01 (patient admission) message over MLLP.
- Mirth TCP Listener receives the message on port 6663 and parses it using the built-in HL7v2 parser.
- JavaScript Transformer extracts fields from PID, PV1, DG1, NK1 segments, applies code mapping tables, formats dates, and builds a FLAT composition JSON object with 42 keys.
- HTTP Sender POSTs the FLAT JSON to the EHRbase REST API endpoint:
POST /ehr/{ehr_id}/composition?templateId=patient_admission.v1&format=FLAT. - EHRbase validates the composition against the uploaded template and stores it.
The critical insight: from Mirth's perspective, EHRbase is just another REST API. There is no openEHR-specific intelligence in the integration engine. All the openEHR knowledge lives in the JavaScript transformer code.
FHIR vs openEHR: Approaching the Same Problem Differently in Mirth
If you have already built Mirth channels that produce FHIR output, this comparison will frame the openEHR approach clearly.
| Aspect | FHIR Approach in Mirth | openEHR Approach in Mirth |
|---|---|---|
| Output format | FHIR Bundle (JSON) | openEHR FLAT Composition (JSON) |
| Data model | Build resource objects (Patient, Encounter, Condition) | Build flat key-value pairs with template paths |
| Code systems | Standard terminologies (SNOMED, ICD-10, LOINC) with URIs | Same external terminologies PLUS local archetype at-codes |
| Validation | FHIR Server validates resources against profiles | CDR validates composition against uploaded template |
| Patient identity | FHIR Patient resource (embedded or referenced) | EHR object in CDR (must exist before posting composition) |
| Multiple resources | Transaction Bundle with N entries | Single FLAT composition (one POST) |
| Lines of code | ~100 lines for a rich Patient+Encounter+Condition Bundle | ~150 lines for a 42-field FLAT composition |
The structural difference is significant. In FHIR, you build multiple discrete resource objects and bundle them. In openEHR, you build one flat map where every clinical data point is a path-value pair within a single composition. The FLAT format actually makes the JavaScript simpler in some ways — you never deal with nested objects, just string keys and primitive values.
The FLAT Format: Why It Is the Only Practical Choice for Mirth
The FLAT format was designed for exactly this use case: building compositions programmatically without having to construct the deep RM object graph. Each key is a path through the template hierarchy. Each value is a simple type (string, number, boolean).
FLAT Path Syntax
A FLAT path looks like this:
patient_admission/admission_details/admission_event/patient_class|codeBreaking it down:
patient_admission— The template ID (root)/admission_details— A SECTION archetype within the template/admission_event— An OBSERVATION, EVALUATION, or other entry archetype/patient_class— A specific ELEMENT within that entry|code— The suffix indicating which attribute of the data type you are setting
FLAT Suffixes for Common Data Types
| Data Type | Suffixes | Example |
|---|---|---|
DV_TEXT | (none) | path: "John Smith" |
DV_CODED_TEXT | |code, |value, |terminology | path|code: "at0003" |
DV_DATE_TIME | (none) or |value | path: "2024-01-15T08:30:00Z" |
DV_QUANTITY | |magnitude, |unit | path|magnitude: 72.5 |
DV_BOOLEAN | (none) | path: true |
DV_IDENTIFIER | |id, |type, |issuer | path|id: "MRN-12345" |
DV_COUNT | |magnitude | path|magnitude: 3 |
Context (ctx) Fields
Every FLAT composition requires metadata in ctx/ prefixed keys:
"ctx/language": "en",
"ctx/territory": "US",
"ctx/composer_name": "Mirth Connect Integration Engine",
"ctx/id_namespace": "HOSPITAL",
"ctx/id_scheme": "HOSPITAL",
"ctx/participation_name:0": "Dr. Robert Smith",
"ctx/participation_function:0": "requester",
"ctx/participation_mode:0": "face-to-face communication",
"ctx/participation_id:0": "DOC001"These are not clinical data — they describe the context of the composition itself (who created it, in what language, with what workflow participation).
Indexed Paths for Repeating Structures
When a template allows multiple instances of a structure (e.g., multiple diagnoses), you use zero-based indexes:
"patient_admission/diagnosis:0/problem_diagnosis|code": "A41.9",
"patient_admission/diagnosis:0/problem_diagnosis|value": "Sepsis",
"patient_admission/diagnosis:1/problem_diagnosis|code": "I10",
"patient_admission/diagnosis:1/problem_diagnosis|value": "Hypertension"Step-by-Step: Building the openEHR Transformer Channel
Step 1: Create the Channel
In Mirth Connect Administrator:
- Channels > New Channel
- Name:
HL7v2 ADT to openEHR Composition - Source Connector:
- Type: TCP Listener
- Mode: MLLP
- Port: 6663
- Inbound data type: HL7v2
- Destination Connector:
- Type: HTTP Sender
- URL:
http://ehrbase-host:8080/ehrbase/rest/openehr/v1/ehr/${ehrId}/composition?templateId=patient_admission.v1&format=FLAT - Method: POST
- Content Type:
application/json
For initial development and testing, use a Channel Writer destination instead. This lets you see the FLAT composition output in the message browser without needing a running EHRbase instance.
Step 2: Define the Mapping Tables
Before writing the transformer, define the code mapping tables. These translate HL7v2 table values into openEHR archetype local codes.
This is the most critical part of the integration. HL7v2 uses single-character table codes (M, F, I, O, E). FHIR uses URI-based code systems. openEHR uses archetype-local at-codes — short codes like at0003 that are defined within each archetype and mean different things in different archetypes.
You must consult your template to find the correct at-codes. There is no universal mapping table. The codes below are for a hypothetical patient_admission.v1 template.
Step 3: Extract HL7v2 Fields
This is standard Mirth territory. We extract from PID (patient demographics), PV1 (visit information), DG1 (diagnosis), and NK1 (next of kin) segments using the familiar msg['SEGMENT']['SEGMENT.N']['SEGMENT.N.M'] syntax.
Step 4: Build the FLAT Composition
This is where the openEHR-specific work happens. We construct a JavaScript object where every key is a template path and every value is a primitive.
Step 5: Configure the HTTP Sender
Set up the HTTP Sender destination to POST the FLAT JSON to your EHRbase instance. The ehrId must be resolved first (see the EHR creation section below).
Deep Dive: Code Mapping from HL7v2 to openEHR Archetypes
Gender Mapping
HL7v2 PID.8 uses Table 0001 (Administrative Sex): M, F, O, A, U, N.
In FHIR, you would map M to male with system http://hl7.org/fhir/administrative-gender.
In openEHR, you map to the archetype's own local codes. For a typical gender element in an administrative demographics archetype:
var GENDER_MAP = {
'M': { code: 'at0003', value: 'Male', terminology: 'local' },
'F': { code: 'at0004', value: 'Female', terminology: 'local' },
'O': { code: 'at0005', value: 'Other', terminology: 'local' },
'U': { code: 'at0006', value: 'Unknown', terminology: 'local' }
};Each DV_CODED_TEXT requires three FLAT keys:
composition["patient_admission/demographics/gender|code"] = "at0003";
composition["patient_admission/demographics/gender|value"] = "Male";
composition["patient_admission/demographics/gender|terminology"] = "local";Patient Class Mapping
HL7v2 PV1.2 uses Table 0004 (Patient Class): I (Inpatient), O (Outpatient), E (Emergency).
var PATIENT_CLASS_MAP = {
'I': { code: 'at0010', value: 'Inpatient', terminology: 'local' },
'O': { code: 'at0011', value: 'Outpatient', terminology: 'local' },
'E': { code: 'at0012', value: 'Emergency', terminology: 'local' }
};Next-of-Kin Relationship Mapping
HL7v2 NK1.3 uses Table 0063 (Relationship): SPO (Spouse), PAR (Parent), CHD (Child), etc.
var CONTACT_TYPE_MAP = {
'SPO': { code: 'at0020', value: 'Spouse', terminology: 'local' },
'PAR': { code: 'at0021', value: 'Parent', terminology: 'local' },
'CHD': { code: 'at0022', value: 'Child', terminology: 'local' },
'SIB': { code: 'at0023', value: 'Sibling', terminology: 'local' },
'EMC': { code: 'at0024', value: 'Emergency Contact', terminology: 'local' }
};Diagnosis: External Terminology Instead of Local Codes
For diagnoses, you typically use external terminologies (ICD-10, SNOMED CT) rather than local at-codes. The FLAT format handles this the same way — three keys per coded value — but with a different terminology identifier:
// ICD-10 coded diagnosis
composition["patient_admission/diagnosis:0/problem_diagnosis|code"] = "A41.9";
composition["patient_admission/diagnosis:0/problem_diagnosis|value"] = "Sepsis, unspecified organism";
composition["patient_admission/diagnosis:0/problem_diagnosis|terminology"] = "ICD-10";
// Or SNOMED CT
composition["patient_admission/diagnosis:0/problem_diagnosis|code"] = "91302008";
composition["patient_admission/diagnosis:0/problem_diagnosis|value"] = "Sepsis (disorder)";
composition["patient_admission/diagnosis:0/problem_diagnosis|terminology"] = "SNOMED-CT";Complete Transformer Code
This is the complete Mirth JavaScript transformer. Copy it into the Source Transformer of your channel.
// =====================================================
// HL7v2 ADT → openEHR FLAT Composition
// Target: EHRbase CDR (or any openEHR REST API)
// Template: patient_admission.v1
// Output: 42-field FLAT composition JSON
// =====================================================
// === CODE MAPPING TABLES ===
// These at-codes are defined in the openEHR template.
// You MUST verify these against your actual template.
var GENDER_MAP = {
'M': { code: 'at0003', value: 'Male', terminology: 'local' },
'F': { code: 'at0004', value: 'Female', terminology: 'local' },
'O': { code: 'at0005', value: 'Other', terminology: 'local' },
'U': { code: 'at0006', value: 'Unknown', terminology: 'local' }
};
var PATIENT_CLASS_MAP = {
'I': { code: 'at0010', value: 'Inpatient', terminology: 'local' },
'O': { code: 'at0011', value: 'Outpatient', terminology: 'local' },
'E': { code: 'at0012', value: 'Emergency', terminology: 'local' }
};
var CONTACT_TYPE_MAP = {
'SPO': { code: 'at0020', value: 'Spouse', terminology: 'local' },
'PAR': { code: 'at0021', value: 'Parent', terminology: 'local' },
'CHD': { code: 'at0022', value: 'Child', terminology: 'local' },
'SIB': { code: 'at0023', value: 'Sibling', terminology: 'local' },
'EMC': { code: 'at0024', value: 'Emergency Contact', terminology: 'local' }
};
// === EXTRACT HL7v2 FIELDS ===
// PID — Patient Identification
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();
// PV1 — Patient Visit
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 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 admitDt = msg['PV1']['PV1.44']['PV1.44.1'].toString();
// DG1 — Diagnosis (optional segment)
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) {
// DG1 segment may not be present in all ADT messages
}
// NK1 — Next of Kin (optional segment)
var nkName = '', nkRelation = '', nkPhone = '';
try {
nkName = msg['NK1']['NK1.2']['NK1.2.1'].toString() + ', '
+ msg['NK1']['NK1.2']['NK1.2.2'].toString();
nkRelation = msg['NK1']['NK1.3']['NK1.3.1'].toString();
nkPhone = msg['NK1']['NK1.4']['NK1.4.1'].toString();
} catch(e) {
// NK1 segment may not be present
}
// MSH — Message Header
var facility = msg['MSH']['MSH.4']['MSH.4.1'].toString();
var msgDateTime = msg['MSH']['MSH.7']['MSH.7.1'].toString();
// === FORMAT DATES ===
function formatHL7Date(hl7Date) {
if (!hl7Date || hl7Date.length < 8) return '';
var y = hl7Date.substring(0, 4);
var m = hl7Date.substring(4, 6);
var d = hl7Date.substring(6, 8);
if (hl7Date.length >= 14) {
var hh = hl7Date.substring(8, 10);
var mm = hl7Date.substring(10, 12);
var ss = hl7Date.substring(12, 14);
return y + '-' + m + '-' + d + 'T' + hh + ':' + mm + ':' + ss + 'Z';
}
return y + '-' + m + '-' + d;
}
var formattedDob = formatHL7Date(dob);
var formattedAdmitDt = formatHL7Date(admitDt) || new Date().toISOString();
var compositionTime = formatHL7Date(msgDateTime) || new Date().toISOString();
// === APPLY CODE MAPPINGS ===
var genderMapped = GENDER_MAP[gender] || GENDER_MAP['U'];
var classMapped = PATIENT_CLASS_MAP[patientClass] || PATIENT_CLASS_MAP['O'];
var contactMapped = CONTACT_TYPE_MAP[nkRelation] || null;
var dxTerminology = 'ICD-10';
if (dxSys === 'SCT' || dxSys === 'SNOMED') {
dxTerminology = 'SNOMED-CT';
}
// === BUILD FLAT COMPOSITION ===
var composition = {};
// --- Context metadata (ctx/) ---
composition["ctx/language"] = "en";
composition["ctx/territory"] = "US";
composition["ctx/composer_name"] = "Mirth Connect Integration Engine";
composition["ctx/id_namespace"] = facility;
composition["ctx/id_scheme"] = facility;
composition["ctx/participation_name:0"] = attFirst + ' ' + attLast;
composition["ctx/participation_function:0"] = "requester";
composition["ctx/participation_mode:0"] = "face-to-face communication";
composition["ctx/participation_id:0"] = attId;
composition["ctx/time"] = compositionTime;
// --- Patient Demographics ---
composition["patient_admission/demographics/patient_name"] = firstName + ' ' + lastName;
composition["patient_admission/demographics/mrn|id"] = mrn;
composition["patient_admission/demographics/mrn|type"] = "MRN";
composition["patient_admission/demographics/mrn|issuer"] = facility;
composition["patient_admission/demographics/date_of_birth"] = formattedDob;
composition["patient_admission/demographics/gender|code"] = genderMapped.code;
composition["patient_admission/demographics/gender|value"] = genderMapped.value;
composition["patient_admission/demographics/gender|terminology"] = genderMapped.terminology;
composition["patient_admission/demographics/address"] = street + ', ' + city + ', ' + state + ' ' + zip;
composition["patient_admission/demographics/phone"] = phone;
// --- Admission Details ---
composition["patient_admission/admission_details/admission_event/patient_class|code"] = classMapped.code;
composition["patient_admission/admission_details/admission_event/patient_class|value"] = classMapped.value;
composition["patient_admission/admission_details/admission_event/patient_class|terminology"] = classMapped.terminology;
composition["patient_admission/admission_details/admission_event/admission_datetime"] = formattedAdmitDt;
composition["patient_admission/admission_details/admission_event/ward"] = ward;
composition["patient_admission/admission_details/admission_event/room"] = room;
composition["patient_admission/admission_details/admission_event/bed"] = bed;
composition["patient_admission/admission_details/admission_event/visit_number"] = visitNum;
composition["patient_admission/admission_details/admission_event/attending_provider"] = attFirst + ' ' + attLast;
composition["patient_admission/admission_details/admission_event/attending_provider_id"] = attId;
composition["patient_admission/admission_details/admission_event/facility"] = facility;
// --- Diagnosis (if DG1 segment present) ---
if (dx) {
composition["patient_admission/diagnosis:0/problem_diagnosis|code"] = dx;
composition["patient_admission/diagnosis:0/problem_diagnosis|value"] = dxDesc;
composition["patient_admission/diagnosis:0/problem_diagnosis|terminology"] = dxTerminology;
composition["patient_admission/diagnosis:0/date_of_onset"] = formattedAdmitDt;
composition["patient_admission/diagnosis:0/severity"] = "Moderate";
composition["patient_admission/diagnosis:0/active_status"] = true;
}
// --- Next of Kin / Emergency Contact (if NK1 segment present) ---
if (nkName && contactMapped) {
composition["patient_admission/contact:0/contact_name"] = nkName;
composition["patient_admission/contact:0/contact_type|code"] = contactMapped.code;
composition["patient_admission/contact:0/contact_type|value"] = contactMapped.value;
composition["patient_admission/contact:0/contact_type|terminology"] = contactMapped.terminology;
composition["patient_admission/contact:0/contact_phone"] = nkPhone;
}
// === STORE OUTPUT ===
var compositionJson = JSON.stringify(composition, null, 2);
channelMap.put('openehrComposition', compositionJson);
channelMap.put('templateId', 'patient_admission.v1');
channelMap.put('patientMrn', mrn);
channelMap.put('fieldCount', Object.keys(composition).length.toString());
logger.info('openEHR FLAT composition built: '
+ Object.keys(composition).length + ' fields | '
+ firstName + ' ' + lastName + ' | MRN: ' + mrn);Input/Output Examples
Input: HL7v2 ADT^A01 Message
MSH|^~\&|HIS|MGH|OPENEHR|EHRBASE|20240115083000||ADT^A01|MSG00001|P|2.5
EVN|A01|20240115083000
PID|1||MRN-67890^^^MGH||JOHNSON^ROBERT^^^MR||19580312|M|||45 OAK STREET^^BOSTON^MA^02108||617-555-0199||M
PV1|1|I|ICU^101^A||||DOC001^SMITH^ROBERT^^^DR|||MED||||1|||DOC001^SMITH^ROBERT^^^DR|IP|VN-2024-001|||||||||||||||||||20240115080000
DG1|1||A41.9^Sepsis, unspecified organism^ICD10|||A
NK1|1|JOHNSON^MARY|SPO|617-555-0198
IN1|1|BCBS|BCBS001|Blue Cross Blue Shield||||GRP-789Output: openEHR FLAT Composition
{
"ctx/language": "en",
"ctx/territory": "US",
"ctx/composer_name": "Mirth Connect Integration Engine",
"ctx/id_namespace": "MGH",
"ctx/id_scheme": "MGH",
"ctx/participation_name:0": "Robert Smith",
"ctx/participation_function:0": "requester",
"ctx/participation_mode:0": "face-to-face communication",
"ctx/participation_id:0": "DOC001",
"ctx/time": "2024-01-15T08:30:00Z",
"patient_admission/demographics/patient_name": "Robert Johnson",
"patient_admission/demographics/mrn|id": "MRN-67890",
"patient_admission/demographics/mrn|type": "MRN",
"patient_admission/demographics/mrn|issuer": "MGH",
"patient_admission/demographics/date_of_birth": "1958-03-12",
"patient_admission/demographics/gender|code": "at0003",
"patient_admission/demographics/gender|value": "Male",
"patient_admission/demographics/gender|terminology": "local",
"patient_admission/demographics/address": "45 Oak Street, Boston, MA 02108",
"patient_admission/demographics/phone": "617-555-0199",
"patient_admission/admission_details/admission_event/patient_class|code": "at0010",
"patient_admission/admission_details/admission_event/patient_class|value": "Inpatient",
"patient_admission/admission_details/admission_event/patient_class|terminology": "local",
"patient_admission/admission_details/admission_event/admission_datetime": "2024-01-15T08:00:00Z",
"patient_admission/admission_details/admission_event/ward": "ICU",
"patient_admission/admission_details/admission_event/room": "101",
"patient_admission/admission_details/admission_event/bed": "A",
"patient_admission/admission_details/admission_event/visit_number": "VN-2024-001",
"patient_admission/admission_details/admission_event/attending_provider": "Robert Smith",
"patient_admission/admission_details/admission_event/attending_provider_id": "DOC001",
"patient_admission/admission_details/admission_event/facility": "MGH",
"patient_admission/diagnosis:0/problem_diagnosis|code": "A41.9",
"patient_admission/diagnosis:0/problem_diagnosis|value": "Sepsis, unspecified organism",
"patient_admission/diagnosis:0/problem_diagnosis|terminology": "ICD-10",
"patient_admission/diagnosis:0/date_of_onset": "2024-01-15T08:00:00Z",
"patient_admission/diagnosis:0/severity": "Moderate",
"patient_admission/diagnosis:0/active_status": true,
"patient_admission/contact:0/contact_name": "Johnson, Mary",
"patient_admission/contact:0/contact_type|code": "at0020",
"patient_admission/contact:0/contact_type|value": "Spouse",
"patient_admission/contact:0/contact_type|terminology": "local",
"patient_admission/contact:0/contact_phone": "617-555-0198"
}42 fields. Zero nesting. One HTTP POST. Compare that to the CANONICAL format for the same data, which would be 400+ lines of nested JSON.
Connecting to EHRbase: REST API Setup
Running EHRbase
The fastest way to get EHRbase running for development:
docker run -d \
--name ehrbase \
-p 8080:8080 \
-e SPRING_DATASOURCE_URL=jdbc:postgresql://postgres:5432/ehrbase \
-e SPRING_DATASOURCE_USERNAME=ehrbase \
-e SPRING_DATASOURCE_PASSWORD=ehrbase \
-e SECURITY_AUTHTYPE=BASIC \
-e SECURITY_AUTHUSER=admin \
-e SECURITY_AUTHPASSWORD=secret \
--link postgres \
ehrbase/ehrbase:2.0.0Or use the official docker-compose.yml from the EHRbase repository.
Upload the Template
Before you can POST compositions, EHRbase must know the template. Upload the Operational Template (OPT) file:
curl -X POST \
'http://localhost:8080/ehrbase/rest/openehr/v1/definition/template/adl1.4' \
-H 'Content-Type: application/XML' \
-H 'Authorization: Basic YWRtaW46c2VjcmV0' \
-d @patient_admission.v1.optYou create templates using tools like Archetype Designer (web-based, by Better) or LinkEHR. Export as OPT 1.4 format for EHRbase compatibility.
Create an EHR for the Patient
Each patient needs an EHR object in the CDR before compositions can be posted. This is typically a one-time operation:
curl -X POST \
'http://localhost:8080/ehrbase/rest/openehr/v1/ehr' \
-H 'Content-Type: application/json' \
-H 'Authorization: Basic YWRtaW46c2VjcmV0' \
-H 'Prefer: return=representation' \
-d '{
"ehr_status": {
"subject": {
"external_ref": {
"id": { "_type": "GENERIC_ID", "value": "MRN-67890", "scheme": "MGH" },
"namespace": "MGH",
"type": "PERSON"
}
},
"is_modifiable": true,
"is_queryable": true
}
}'The response includes the ehr_id (a UUID). Store this mapping: MRN-67890 → ehr_id UUID.
POST the Composition
curl -X POST \
'http://localhost:8080/ehrbase/rest/openehr/v1/ehr/EHRBASE_EHR_UUID/composition?templateId=patient_admission.v1&format=FLAT' \
-H 'Content-Type: application/json' \
-H 'Authorization: Basic YWRtaW46c2VjcmV0' \
-H 'Prefer: return=representation' \
-d @composition.jsonA successful response returns 201 Created with the composition UID in the ETag and Location headers.
Handling EHR Creation and Composition Lifecycle
In production, you need logic to handle the patient-to-EHR mapping. Here are three approaches:
Approach 1: Pre-Create EHRs in Batch
If your patient registry is known, bulk-create EHR objects and maintain a mapping table in Mirth's global channel map or an external database.
// In the transformer, look up ehrId from a mapping table
var ehrId = globalChannelMap.get('ehrMap_' + mrn);
if (!ehrId) {
// EHR not found — queue for creation or reject
logger.error('No EHR found for MRN: ' + mrn);
return; // Skip this message
}
channelMap.put('ehrId', ehrId);Approach 2: Create-on-First-Encounter
Use a pre-processor channel that checks for EHR existence and creates one if needed before the composition channel runs.
Approach 3: Use the Subject-Based Endpoint
Some CDRs (including newer EHRbase versions) allow querying by subject ID. You can use the subject_id and subject_namespace query parameters to find the EHR without maintaining a separate mapping:
GET /ehr?subject_id=MRN-67890&subject_namespace=MGHThe tradeoff: this adds a round-trip HTTP call per message, which matters at volume.
Limitations and Caveats
This approach works and is production-viable. But you need to understand what it cannot do.
What This Integration Cannot Do
- No template validation in Mirth. The CDR validates the composition when you POST it. If a required field is missing or a code is wrong, you will get a 400 error at runtime, not at design time.
- No archetype awareness. Mirth does not know what archetypes or templates are. The FLAT paths are strings you construct manually. Typos in paths are silent until the CDR rejects the composition.
- No AQL (Archetype Query Language) support. AQL is the SQL-equivalent for querying openEHR data. You cannot construct or execute AQL queries from within Mirth.
- No TERMINOLOGY_SERVICE binding. openEHR archetypes can bind elements to external terminologies. Mirth does not enforce these bindings.
- No versioning awareness. openEHR compositions are versioned. If you POST a composition that already exists, you need the preceding_version_uid to create a new version.
- No automatic FLAT path discovery. You must read the template or use the EHRbase example endpoint to discover valid paths.
Known Issues with Mirth HTTP Sender for REST APIs
From our testing with Mirth Connect 4.5.2 on Java 17:
$in URLs causes variable substitution. If your EHRbase URL contains a$character, Mirth will try to substitute it as a variable. Build the full URL in a JavaScript variable and reference it.- Response body handling. The HTTP Sender response transformer can throw NullPointerException on Java 17 when processing certain response bodies. Use try-catch in response transformers.
- Connection pooling. Mirth's HTTP Sender creates new connections per message by default. For high-volume integrations, consider connection pooling configuration.
CDR-Specific Considerations
| CDR | FLAT Format Support | API Quirks |
|---|---|---|
| EHRbase (open source) | Full support via ?format=FLAT | Requires template upload before composition POST. Strict validation. |
| Better Platform | Full support (originated FLAT format) | Proprietary auth. Uses FLAT as default format. |
| Medblocks | Partial (check current version) | JavaScript/Web-native CDR. May prefer STRUCTURED. |
| Ocean Health Systems | Check vendor documentation | Different REST API structure. |
Production Hardening Checklist
Before deploying to production:
- Error handling: Wrap every field extraction in try-catch. HL7v2 messages from real systems have missing segments, empty fields, and non-standard formatting.
- MRN-to-EHR mapping: Implement and test your patient-to-EHR resolution strategy. This is the most common failure point.
- Template version management: Pin your template version. If the template changes, FLAT paths may change, breaking the transformer.
- Retry logic: Configure the HTTP Sender queue for retries on 5xx errors. EHRbase being temporarily unavailable should not lose messages.
- Dead letter queue: Messages that fail validation (400 errors) should route to an error queue for manual review, not retry indefinitely.
- Logging: Log the compositionUid returned by EHRbase for audit trail. Map it back to the HL7v2 message control ID (MSH.10).
- Date timezone handling: HL7v2 dates may or may not include timezone offsets. openEHR is strict about ISO 8601. Decide on a timezone policy.
- Multiple DG1 segments: The example handles one diagnosis. Real ADT messages can have 10+ DG1 segments. Loop through them using indexed FLAT paths.
- Character encoding: HL7v2 uses ASCII/Latin-1 by default. openEHR compositions are UTF-8. Handle accented names and special symbols.
- Load testing: FLAT composition POSTs to EHRbase take 50-200ms. At 100 messages/second, you need connection pooling and possibly multiple HTTP Sender threads.
Need expert help with healthcare interoperability or openEHR integration? Our team builds production-grade Mirth Connect channels, FHIR pipelines, and openEHR data flows for hospitals and health IT vendors. We also offer custom healthcare software development to build clinical data repositories and integration platforms from the ground up. Talk to our team to get started.



