The Mirth Connect JavaScript transformer is where message data is read, transformed, and routed. It runs on Mozilla Rhino — a Java-native JavaScript engine — and gives you access to the parsed HL7 message via the msg E4X object, cross-destination variables via channelMap, a persistent cache via globalMap, and raw message bytes via connectorMessage.
This guide covers every object, every pattern, and the pitfalls that cause most production transformer failures. For channel architecture context, see the channel design patterns guide.
What JavaScript Engine Does Mirth Connect Use?
Mirth runs Mozilla Rhino — not Node.js, not V8. This distinction matters more than most engineers expect when they first touch a Mirth channel.
In Mirth 4.x, Rhino supports ES5 with selective ES6 additions: const, let, and arrow functions work. Promise, async/await, and fetch() do not. Any modern JavaScript pattern that relies on the event loop or native async won't run. If you copy code from a Node.js project into a Mirth transformer, expect it to break.
The most important Rhino-specific feature is E4X (ECMAScript for XML) — a Rhino extension enabling dot-notation XML access. All the msg['PID']['PID.3'] patterns in Mirth transformers are E4X, not standard JavaScript. This syntax does not exist in Node.js or browsers and will not run anywhere except Rhino.
What Rhino Can and Can't Do
| Feature | Rhino (Mirth) | Notes |
|---|---|---|
const / let | ✓ | ES6 basics work |
| Arrow functions | ✓ | () => syntax works |
E4X XML (msg['PID']) | ✓ | Rhino-only, not standard JS |
JSON.parse / JSON.stringify | ✓ | Works for FHIR payloads |
Java class access (Packages.java) | ✓ | Direct Java interop |
Promise / async / await | ✗ | No event loop in Rhino |
fetch() | ✗ | Use HTTPUtil or HTTP Sender connector |
ES6 modules (import/export) | ✗ | Use Code Templates instead |
| Destructuring assignment | Partial | Inconsistent — avoid in production |
The .toString() Rule: The Most Common Mirth Bug
E4X field access returns a Java String object, not a JavaScript primitive. The strict equality operator === checks both value and type — so Java String and JavaScript primitive string never match, even with identical content.
Always call .toString() before any comparison, string operation, or conditional check. This single rule eliminates the most common class of Mirth transformer bugs:
// WRONG — Java String vs JS primitive, always evaluates false
if (msg['PID']['PID.3']['CX.1'] === '12345') { }
// CORRECT — .toString() converts to JavaScript primitive
var mrn = msg['PID']['PID.3']['CX.1'].toString();
if (mrn === '12345') { }
// WRONG — string concatenation with Java String produces unexpected results
var label = 'Patient: ' + msg['PID']['PID.5']['XPN.1']['FN.1'];
// CORRECT
var label = 'Patient: ' + msg['PID']['PID.5']['XPN.1']['FN.1'].toString();
// Safe accessor — returns empty string instead of throwing on absent field
function safeGet(xmlNode) {
try { return xmlNode.toString(); } catch(e) { return ''; }
}The safeGet() function prevents the runtime exception thrown when .toString() is called on an absent E4X node — the single most common cause of the transformer errors listed in the top 10 Mirth failures guide.
Reading HL7 v2 Messages: Key Field Access Patterns
HL7 v2 messages arrive as pipe-delimited text. Mirth's parser converts them to E4X XML automatically. The E4X path mirrors the HL7 segment and field structure:
// Patient demographics — PID segment
var mrn = msg['PID']['PID.3']['CX.1'].toString(); // MRN
var lastName = msg['PID']['PID.5']['XPN.1']['FN.1'].toString();
var firstName = msg['PID']['PID.5']['XPN.2'].toString();
var dob = msg['PID']['PID.7'].toString(); // YYYYMMDD
var sex = msg['PID']['PID.8'].toString(); // M, F, U
var ssn = msg['PID']['PID.19'].toString();
// Visit info — PV1 segment
var patClass = msg['PV1']['PV1.2'].toString(); // I=Inpatient O=Outpatient E=Emergency
var location = msg['PV1']['PV1.3']['PL.1'].toString();
var admitDate = msg['PV1']['PV1.44'].toString();
var disDate = msg['PV1']['PV1.45'].toString();
var attending = msg['PV1']['PV1.7']['XCN.1'].toString();
// Message header — MSH segment
var msgType = msg['MSH']['MSH.9']['MSG.1'].toString(); // ADT, ORU, ORM
var eventType = msg['MSH']['MSH.9']['MSG.2'].toString(); // A01, R01
var facility = msg['MSH']['MSH.4']['HD.1'].toString();
var msgCtrlId = msg['MSH']['MSH.10'].toString();
var sendApp = msg['MSH']['MSH.3']['HD.1'].toString();Iterating Repeating Fields and Segments
Some HL7 fields repeat (PID-3 can carry multiple MRNs from different systems). Some segments repeat (a metabolic panel has 14 OBX segments). The for each E4X loop handles both:
// Iterate repeating PID.3 — multiple MRNs from different assigning authorities
var mrnList = [];
for each (var pid3 in msg['PID']['PID.3']) {
var idValue = pid3['CX.1'].toString();
var oidRoot = pid3['CX.4']['HD.2'].toString(); // OID identifies the assigning authority
mrnList.push({ system: oidRoot, value: idValue });
channelMap.put('mrn_' + oidRoot, idValue);
}
channelMap.put('all_mrns', JSON.stringify(mrnList));
// Iterate OBX segments — lab result components
var results = [];
for each (var obx in msg['OBX']) {
results.push({
loincCode : obx['OBX.3']['CWE.1'].toString(),
value : obx['OBX.5'].toString(),
units : obx['OBX.6']['CWE.1'].toString(),
abnFlag : obx['OBX.8'].toString(),
status : obx['OBX.11'].toString()
});
}
channelMap.put('results', JSON.stringify(results));Context Objects: All Five Explained
Mirth exposes five context objects in every transformer. Knowing when to use each is the foundation of well-structured channel code.
msg — The Parsed Message
msg is the parsed incoming message as an E4X XML object (HL7 v2) or a JSON object (FHIR). For HL7, it's read-write — you can modify segments directly and the modified message becomes the channel's transformed payload. For FHIR, parse and stringify with JSON.parse(msg.toString()).
// Modifying a field directly in msg (HL7 v2)
msg['PID']['PID.5']['XPN.2'] = firstName.toUpperCase();
// Adding a new segment
msg['ZPI']['ZPI.1'] = 'CUSTOM_VALUE';
// FHIR — msg arrives as JSON string
var fhirResource = JSON.parse(msg.toString());
fhirResource.status = 'final';
return JSON.stringify(fhirResource);channelMap — Per-Message Variables
channelMap carries variables from the source transformer through all destination transformers for a single message, then clears. Use it for routing decisions, extracted patient identifiers, and any value that needs to travel from source to destination without re-parsing the message.
// Source transformer — set
channelMap.put('event_type', eventType);
channelMap.put('mrn', mrn);
channelMap.put('is_critical', 'false');
// Destination filter — read (use $c() shorthand)
if ($c('event_type') !== 'A01') return false;
// Destination transformer — read and use
var mrn = channelMap.get('mrn');
var isCritical = channelMap.get('is_critical') === 'true';globalMap — Channel-Lifetime Cache
globalMap persists for the channel's entire running lifetime — set once in the Deploy script, available in every message processing cycle until the channel is stopped. This is the single highest-impact performance pattern in Mirth: load a lookup table once at startup instead of querying the database on every message.
// Deploy script — runs ONCE when channel starts
var db = DatabaseConnectionFactory.createDatabaseConnection(
'org.postgresql.Driver', 'jdbc:postgresql://db:5432/mirth_ref', 'ro', 'pass'
);
try {
var rs = db.executeCachedQuery('SELECT local_code, loinc_code FROM loinc_map WHERE active = true');
var map = {};
while (rs.next()) {
map[rs.getString('local_code')] = rs.getString('loinc_code');
}
globalMap.put('loinc_map', JSON.stringify(map));
logger.info('LOINC map loaded: ' + Object.keys(map).length + ' entries');
} finally { db.close(); }
// Source transformer — fast in-memory lookup
var loincMap = JSON.parse(globalMap.get('loinc_map'));
var loinc = loincMap[localCode] || localCode;globalMap is per-instance. In an HA Mirth cluster with two nodes, each node has its own globalMap. If you update the map on one node, the other doesn't see it until it restarts. For shared state across nodes, use a database table or Redis instead.
connectorMessage — Raw Bytes and Metadata
connectorMessage gives access to the raw unparsed message bytes and connector-level metadata. Use it for audit logs that need to capture the exact bytes received, or for debugging parser issues by comparing raw vs parsed content:
// Get raw message bytes — useful for HIPAA audit trail
var rawMessage = connectorMessage.getRawData();
// Get the message ID assigned by Mirth
var messageId = connectorMessage.getMessageId();
// Get the connector name (which listener received this)
var connectorName = connectorMessage.getConnectorName();
// Write to HIPAA-compliant audit log
logger.info('AUDIT|' + messageId + '|' + channelMap.get('mrn') + '|' + connectorName);responseMap — ACK Control for MLLP
responseMap controls the HL7 acknowledgment sent back to the source system. For MLLP channels, every received message requires an ACK. Mirth sends AA (Application Accept) by default. Use responseMap when you need to send AE (Application Error) or AR (Application Reject) based on transformer logic:
// Send AA — accept
responseMap.put('RESPONSE', ResponseFactory.getSuccessResponse('AA'));
// Send AE — application error (the message was received but has a business logic error)
if (!mrn || mrn.trim() === '') {
var nackMsg = 'Required MRN missing in PID.3';
responseMap.put('RESPONSE', ResponseFactory.getFailureResponse(nackMsg));
channelMap.put('processing_status', 'error');
}
// Customise MSA.3 (error text) in the ACK
responseMap.put('RESPONSE', ResponseFactory.getSuccessResponse('AA', 'Processed OK'));Transformer Shorthand Functions
Mirth provides shorthand wrapper functions for the most common map operations. These are pre-defined in Mirth's JavaScript context — you don't need to define them:
| Shorthand | Equivalent | Use when |
|---|---|---|
$c('key') | channelMap.get('key') | Reading from channelMap in destination |
$c('key', val) | channelMap.put('key', val) | Writing to channelMap |
$g('key') | globalMap.get('key') | Reading globalMap in transformers |
$('key') | channelMap.get('key') (legacy) | Older channel code — prefer $c() |
Code Templates: Shared Function Libraries
Code templates are Mirth's mechanism for sharing JavaScript utility functions across all channels without copy-pasting. Functions defined in a code template are automatically available in every transformer in every channel — no import needed.
// Code Template: DateUtils
function hl7DateToISO(hl7date) {
var d = hl7date.toString().replace(/[^0-9]/g, '');
if (d.length < 8) return null;
var iso = d.substring(0,4) + '-' + d.substring(4,6) + '-' + d.substring(6,8);
if (d.length >= 12)
iso += 'T' + d.substring(8,10) + ':' + d.substring(10,12) + ':00Z';
return iso;
}
// Code Template: ValidatorLib
function requireField(value, fieldName) {
var v = value ? value.toString().trim() : '';
if (!v) throw new Error('Required field missing: ' + fieldName);
return v;
}
// Code Template: FHIRBuilder
function buildObservation(loincCode, value, units, status, patientRef) {
return {
resourceType : 'Observation',
status : status, // final, preliminary, amended
code : { coding: [{ system: 'http://loinc.org', code: loincCode }] },
subject : { reference: patientRef },
valueQuantity: { value: parseFloat(value), unit: units,
system: 'http://unitsofmeasure.org' }
};
}Organize code templates by single responsibility: DateUtils, HL7Parser, FHIRBuilder, ValidatorLib, AuditLogger. Keep business logic in channel-specific transformers. Code templates should be pure utility functions without side effects — safe to call from any channel. For CI/CD versioning of code templates with MirthSync, see the automated testing and CI/CD guide.
Database Lookups: Safe Pattern With Connection Pooling
The finally block is not optional — unclosed JDBC connections accumulate under load and eventually exhaust the connection pool, causing channel failures. Two patterns: per-message lookup (for low-volume channels) and startup preloading (for anything over 200 messages per minute):
// Per-message lookup — use only for low-volume channels
function lookupFacilityCode(localFacility) {
var db = DatabaseConnectionFactory.createDatabaseConnection(
'org.postgresql.Driver', 'jdbc:postgresql://db:5432/mirth_ref', 'ro_user', 'pass'
);
try {
// Use parameterized query to prevent SQL injection
var rs = db.executeCachedQuery(
"SELECT npi FROM facility_map WHERE local_code = '" +
localFacility.replace("'","''") + "'"
);
return rs.next() ? rs.getString('npi') : null;
} finally {
db.close(); // ALWAYS — never rely on garbage collection
}
}
// Startup preloading — for high-volume channels
// In Deploy script:
var db = DatabaseConnectionFactory.createDatabaseConnection(...);
try {
var rs = db.executeCachedQuery('SELECT local_code, npi FROM facility_map');
var map = {};
while (rs.next()) map[rs.getString('local_code')] = rs.getString('npi');
globalMap.put('facility_map', JSON.stringify(map));
} finally { db.close(); }
// In transformer (instant, no DB round-trip):
var facilityMap = JSON.parse(globalMap.get('facility_map'));
var npi = facilityMap[localFacility];For high-volume channels, per-message DB queries at 10,000 messages per hour means 10,000 database round-trips per hour. Preloading converts that to one query at startup. Full caching strategies are in the Mirth performance tuning guide.
Structured Error Handling Pattern
Unhandled exceptions in a Mirth transformer send the message to the error queue and return a NAK to the source system. That's sometimes the right behavior — but catching errors, flagging them in channelMap, and routing them to a dedicated error destination gives you visibility without losing messages:
try {
// Validate required fields first
var mrn = requireField(msg['PID']['PID.3']['CX.1'], 'PID.3 MRN');
var eventType = requireField(msg['MSH']['MSH.9']['MSG.2'], 'MSH.9.2 event type');
// Business logic
channelMap.put('mrn', mrn);
channelMap.put('event_type', eventType);
channelMap.put('processing_status', 'ok');
logger.info('Processing ' + eventType + ' for MRN: ' + mrn);
} catch (e) {
logger.error('Transform failed [' + channelMap.get('msg_ctrl_id') + ']: ' + e.message);
channelMap.put('processing_status', 'error');
channelMap.put('error_message', e.message);
channelMap.put('error_field', e.field || 'unknown');
// Do NOT rethrow — message continues to error-handling destination
}
// In destination filter — skip normal destinations on error
if (channelMap.get('processing_status') === 'error') return false;
// Dedicated error destination filter
return channelMap.get('processing_status') === 'error';The error destination writes to a monitoring queue with the full message and error detail. Operations sees the alert, fixes the data issue (often a missing required field from a trading partner), and replays the message. No messages are lost; every failure is visible. Full error queue setup is in the production monitoring guide.



