Brio-IO Communication Server

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 destinations
  • sourceMap, responseMap — context of the source connector and the response
  • globalChannelMap — persistent across the channel
  • globalMap — persistent across the server
  • tmp — 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.