Transformer handbook
The heart of it: reading and writing field paths, creating segments, repeating fields, escapes — in Groovy, GraalJS and Rhino, with examples that run exactly as shown.
Last updated: Examples verified against brio-server fa237ec7ad
An integration server spends most of its time rewriting messages. In Brio-IO that happens in the transformer. This chapter is the reference for it: every example is verified against the running server, in all three engines.
If you do not know the underlying model yet — canonical XML, underscores, positional components — read Concepts first.
The rule for field paths
- Simple field → directly:
msg.MSH.MSH_10 - Composite field → one level per component:
msg.PID.PID_5.PID_5_1 - Subcomponent → one level deeper:
msg.PID.PID_5.PID_5_1_1
Accessing a path that does not exist does not throw — it returns an empty string. That is deliberate and matches what Mirth practitioners expect: a message without the expected field should not abort processing.
msg is the root element, so you start below the root — no ADT_A01
prefix.
Reading
// Groovy — .text() yields the field value
def familyName = msg.PID.PID_5.PID_5_1.text() // "Schmidt"
def givenName = msg.PID.PID_5.PID_5_2.text() // "Hans"
// GraalJS (js-modern) — a text field reads directly as a string
let familyName = msg.PID.PID_5.PID_5_1; // "Schmidt"
let patientId = msg.PID.PID_3.PID_3_1; // "12345"
// Rhino (js-legacy) — E4X brackets with dot notation, as in Mirth
var familyName = msg['PID']['PID.5']['PID.5.1'].toString();
That Rhino sees the dot variant of the same message is handled by the pipeline processor. You do not have to care.
Writing and returning
A transformer mutates the parsed msg object and returns it. The engine
serialises back to canonical XML at the step boundary — you never serialise by
hand.
// Groovy: explicit return, set the value on the node via [0].value
def familyName = msg.PID.PID_5.PID_5_1.text()
msg.PID.PID_5.PID_5_1[0].value = familyName.toUpperCase()
return msg
// GraalJS: direct assignment, last expression is the result
msg.PID.PID_5.PID_5_1 = msg.PID.PID_5.PID_5_1.toUpperCase();
msg;
// Rhino: set the field, msg as the last expression
msg['PID']['PID.5']['PID.5.1'] = 'Mueller';
msg;
The difference between Groovy and GraalJS is only language idiom: Groovy needs
an explicit return and sets the value on the node ([0].value); in GraalJS
the last expression and direct assignment are enough.
Missing paths are created
If you write to a path that does not exist in the message, Brio-IO creates the missing levels. You do not have to check whether a field exists before setting it.
Filters: return a boolean
A filter must return a boolean. true lets the message through, anything
else drops it — visible as FILTERED in the message browser.
// Groovy: only messages with a patient ID
return msg.PID.PID_3.PID_3_1.text() != ""
// GraalJS
msg.PID.PID_3.PID_3_1 !== "";
An example that runs in the demo exactly like this — let only ADT^A08 through:
msg.MSH.MSH_9.MSH_9_2.text() == 'A08'
Repeating fields
A repeated element reads as an indexable list:
// GraalJS — two repetitions of PID-3
msg.PID.PID_3[0] // "MRN-1"
msg.PID.PID_3[1] // "MRN-2"
msg.PID.PID_3.length // 2
The same index pattern applies in Groovy (msg.PID.PID_3[0].text()). For more
complex navigation, full XPath access is available via xml.xpath.
Creating segments
A new segment — say a Z segment for site-specific extras — is created with
createSegment. The result is a node you fill directly.
// Groovy: append and fill ZBR
def z = createSegment('ZBR', msg)
z.appendNode('ZBR_1', 'script-created')
msg
// GraalJS: append and fill ZBR
var z = createSegment('ZBR', msg);
z.ZBR_1 = 'script-created';
msg;
Both produce <ZBR><ZBR_1>script-created</ZBR_1></ZBR> at the end of the
message.
Insert at a specific position instead of appending:
// index 1 = right after the first segment (MSH)
createSegment('ZZZ', msg, 1);
msg;
Insert directly after a given segment:
// GraalJS
createSegmentAfter('ZZZ', msg.MSH);
msg;
// Groovy — address the node explicitly here
createSegmentAfter('ZZZ', msg.MSH[0])
msg
GraalJS additionally offers the short form that creates segment and content in one step:
msg.ZBR = 'script-created';
msg;
Escapes
HL7 escape sequences stay verbatim in the canonical XML — \S\ remains
\S\. To resolve or produce them in a script, use the two helpers available in
all three engines:
escapeHl7('a^b&c') // "a\S\b\T\c"
unescapeHl7('a\\S\\b\\T\\c') // "a^b&c"
This is how you safely write a value into a field when the value itself contains HL7 delimiters — without tearing apart the message structure.
Parsing a second message
Sometimes a script needs a second message — one sitting in a map, say. That is what the serialiser facade is for:
// GraalJS
var s = SerializerFactory.getSerializer('HL7V2');
var x = s.toXML(someOtherEr7String);
// x is canonical XML now, and the same paths run against it
The message context: maps
Passing values between filter, transformer and destinations:
def patientId = msg.PID.PID_3.PID_3_1.text()
channelMap.put("patientId", patientId)
return msg
channelMap— lives for this one message through to the destinationssourceMap,responseMap— context of the source connector and the responseglobalChannelMap— persistent across the channelglobalMap— persistent across the servertmp— scratchpad, only for this one execution
xml.get / xml.set: the transition API
For existing scripts and non-XML payloads (JSON, raw — where msg stays
a string) there is the path helper xml. It is identical across engines and
accepts msg both as a string and as a parsed object:
// path from the root, including the root element
def familyName = xml.get(msg, "ADT_A01/PID/PID_5/PID_5_1")
Unlike property access, the helper path starts at the root element. It
accepts both slash and dot as separators. Besides get there is set for
writing and xpath for full XPath access:
let familyName = xml.xpath(msg, "//*[local-name()='PID_5_1']/text()");
For new scripts, property access is the recommended way — it is more readable and faster, because it works directly on the parsed object instead of re-serialising the message on every field access.
What is different in Rhino
Rhino (js-legacy) exists for imported Mirth scripts and is the only engine
that gets the dot notation — the world those scripts expect. There msg is
directly an E4X XML object; the new XML(msg) known from Mirth is no longer
needed.
Rhino is deliberately deprecated: every deploy of a channel with at least
one Rhino script produces a deprecation warning — in the log and in the API
response — naming the affected scripts. The script editor also shows a warning
as soon as you pick js-legacy.
The migration path is described in Mirth migration.
Common pitfalls
Data-type paths from an interim version. msg.PID.PID_5.XPN_1.FN_1 silently
returns an empty string today. The correct path is msg.PID.PID_5.PID_5_1. On
deployment Brio-IO warns by name if a script still contains such paths — take
that warning seriously.
Forgotten return in Groovy. Without return msg the transformer returns
the value of the last expression, which is rarely the message.
Multi-level vivification by property in Groovy/GraalJS. A missing
intermediate node cannot be created by property assignment alone in those two
engines — use createSegment or xml.set. Only Rhino/E4X can do it natively.
router.routeMessage() does not exist. The way from channel to channel is a
destination on the channel.
This page is currently available in German and English. A Spanish version will follow.