Nirmitee.io
Mirth ConnectHealthcare IntegrationDeveloper Guide

Mirth Connect JavaScript Transformer: The Complete Developer Reference

July 13, 202611 min readUpdated Oct 19, 2026
Written by
Jitendra Choudhary
Jitendra Choudhary

CTO & Co-Founder

CTO & Co-Founder at Nirmitee.io. Architects healthcare integrations across FHIR, SMART on FHIR, ABDM and NHCX, writing from production experience taking hospital software from sandbox to go-live.

Mirth Connect JavaScript Transformer: The Complete Developer Reference

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.

New to Mirth? Start with our Mirth Connect guide: what it is, how it works, and when it fits.

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.

Teams that want this built rather than researched can work with our Mirth Connect integration team.

Watch: CI/CD for Mirth Connect: Channels in Git, Tests, and Automatic Rollback (Live) (2:14).

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

FeatureRhino (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 assignmentPartialInconsistent — 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:

ShorthandEquivalentUse 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.

Further Reading

  • Automated Testing for Mirth Channels — CI/CD Guide
  • Mirth Channel Design Patterns: 5 Architectures
  • Mirth Connect Performance Tuning to 10,000 msg/hr
  • ADT Processing: A01, A04, A08, A40 Patterns
  • Top 10 Mirth Connect Integration Failures and Fixes

Ready to scale?

Talk to our healthcare engineering team about building, integrating, and shipping faster.

Frequently Asked Questions

What JavaScript version does Mirth Connect's Rhino engine support?

Mirth Connect 4.x uses Mozilla Rhino supporting approximately ES5 with selective ES6 additions. const, let, and arrow functions work. Promise, async/await, and fetch() do not — Rhino has no event loop. The most important Rhino feature is E4X (ECMAScript for XML), a Rhino-specific extension enabling msg['PID']['PID.3'] dot-notation XML access that does not exist in Node.js or browsers. Java class access via Packages.java is also available, enabling direct interop with Java libraries from within transformer code.

Why does msg field comparison always return false even with the right value?

E4X field access returns a Java String object, not a JavaScript primitive string. The === strict equality operator checks both value and type — Java String and JavaScript primitive never match even with identical content. Fix: always call .toString() before any comparison or string operation. Apply this universally to every E4X field access in your transformers. The safeGet() helper pattern wraps .toString() in a try-catch to return an empty string instead of throwing when the field is absent in the message.

What is the difference between channelMap and globalMap?

channelMap is scoped to a single message processing cycle — set in the source transformer, available in all destination transformers for that message, then cleared. globalMap persists for the channel's entire running lifetime and survives message processing cycles. Use channelMap for routing decisions and extracted message values. Use globalMap for static lookup tables (LOINC codes, facility NPI maps) loaded once at channel startup in the Deploy script. In HA clusters, globalMap is per-instance — use a database or Redis for shared state across nodes.

How do I use responseMap to control MLLP acknowledgments?

responseMap controls the HL7 ACK returned to the MLLP source system. Mirth sends AA (Application Accept) by default. Use ResponseFactory.getSuccessResponse('AA') to accept explicitly. Use ResponseFactory.getFailureResponse(errorText) to send AE (Application Error) when transformer validation fails — for example, missing required MRN. Sending AE tells the source system to retry or alert. Do not send AE for business logic filtering (suppressed A08s) — only for genuine message processing failures.

How should I organize code templates for a large Mirth deployment?

Organize by single responsibility: DateUtils for HL7-to-ISO date conversions, HL7Parser for segment iteration helpers, FHIRBuilder for constructing FHIR R4 resources, ValidatorLib for required field checks, AuditLogger for HIPAA compliance event logging. Keep business logic in channel-specific transformers. Code templates should be pure utility functions without side effects — safe to call from any channel without unintended coupling. Version-control code templates in Git with MirthSync and deploy them through CI/CD alongside channel exports.

How do I safely make database queries from a Mirth transformer?

Always use try-finally to guarantee connection closure — unclosed JDBC connections accumulate under load and exhaust the pool. For high-volume channels above 200 messages per minute, preload all lookup data into globalMap in the channel Deploy script rather than querying per message. A per-message DB query at 10,000 messages per hour means 10,000 round-trips per hour; preloading converts that to one query at startup. For data that changes frequently, add a scheduled refresh: a separate channel that reloads globalMap on a timer without stopping the main processing channel.
Share