Filters and transformers are JavaScript (or TypeScript, compiled before execution) that
run inside a sandbox for each message. This reference documents the surface actually
exposed to your scripts, sourced from packages/engine/src/sandbox/sandbox-executor.ts,
sandbox-context.ts, and bridge-functions.ts.
Execution model. Scripts run under
VmSandboxExecutor, anode:vm-based sandbox. Your code is wrapped in a'use strict'IIFE and executed with a timeout. The value youreturnbecomes the script result (a filter returns a boolean; a transformer typically mutatesmsg/tmpand the maps).Design doc
docs/design/04predates this and still referencesisolated-vm. That is stale — the engine usesnode:vm(VmSandboxExecutor). Treat this page as current.
| Global | Type | Description |
|---|---|---|
msg |
object/string | The inbound message. HL7v2 is auto-parsed to an HL7 message object (see below); otherwise the parsed/raw value. Mutations persist to later stages. |
tmp |
object/string | The outbound message being built. Defaults to msg. |
rawData |
string | The original raw content string. |
All maps are plain objects. Reads and writes on the read/write maps are captured and carried to later pipeline stages.
| Global | Access | Scope |
|---|---|---|
sourceMap |
read-only | Set by the source connector for this message. |
channelMap |
read/write | Shared across all connectors for this message. |
connectorMap |
read/write | Per-destination scope. |
responseMap |
read/write | Response values (available in the postprocessor). |
globalChannelMap |
read-only in script | Per-channel, persists across messages within a deployment. |
globalMap |
read/write | Global across all channels; flushed to the database. |
configMap |
read-only (frozen) | Configuration values loaded at deploy time. Writing throws (strict mode). |
| Function | Behavior |
|---|---|
$(key) |
Looks up key across maps in order: responseMap, connectorMap, channelMap, globalChannelMap, globalMap, configMap, sourceMap. Returns the first defined value. |
$r(key) / $r(key, value) |
Get / set a responseMap entry. |
$g(key) / $g(key, value) |
Get / set a globalMap entry. |
$gc(key) |
Get a configMap entry. |
logger writes structured log entries attached to the message; they appear in the
message browser and server logs.
logger.info('message');
logger.warn('message');
logger.error('message');
logger.debug('message');Parses an HL7v2 string into an HL7 message object. If passed an already-parsed HL7
object (e.g. msg on an HL7 channel), it is returned as-is.
The returned object exposes:
| Member | Description |
|---|---|
get(path) |
Read a field by path, e.g. msg.get('PID.3.1'). get('MSH.9') auto-resolves to the first subcomponent — use get('MSH.9.2') for the trigger event. |
set(path, value) |
Set a field by path. |
delete(path) |
Remove a field. |
toString() |
Serialize back to an HL7 string. |
messageType |
Message type (read-only). |
messageControlId |
MSH-10 control ID (read-only). |
getSegmentCount(name) |
Number of segments with the given name. |
getSegmentString(name, index?) |
Raw string of the Nth segment (default first). |
Builds an HL7 ACK for the original message. ackCode is the acknowledgment code
(e.g. AA, AE, AR); textMessage is optional.
const ack = createACK(rawData, 'AA');
$r('ack', ack); // expose it to the responseThese globals exist only when the host enables them for the channel (they are absent
otherwise, so guard with typeof). They are async — await them. Outbound HTTP is
subject to SSRF protection: requests to private/loopback address ranges are blocked.
| Function | Signature | Description |
|---|---|---|
httpFetch |
httpFetch(url, { method, headers, body, timeout }?) → { status, statusText, headers, body } |
Outbound HTTP request. |
dbQuery |
dbQuery(dataSourceName, sql, params?) → rows |
Parameterized query against a named Data Source. Never string-interpolate values into sql. |
routeMessage |
routeMessage(channelName, rawData) → { success, response? } |
Route a message to another channel. |
getResource |
getResource(name) → string | null |
Load a configured resource by name. |
getCollection |
getCollection(name) → { store, find } |
Read/write a durable keyed record store. See Collections. |
Wiring status: all bridges above (
getResource,httpFetch,routeMessage,getCollection,dbQuery) are wired into the production engine.
dbQuery(dataSourceName, sql, params?) runs a parameterized query against a Data Source —
a database connection profile an admin defines under Data Sources in the admin UI (host,
port, database, user, password). Credentials live server-side, encrypted at rest; scripts never
see them and can only reach configured sources.
const [order] = await dbQuery('reporting-db',
'SELECT report FROM reports WHERE accession = $1 ORDER BY created_at DESC LIMIT 1',
[accession]);- Parameterized only — pass values as
params($1, $2, …); never build SQL by string concatenation. - Read-only by default — a data source rejects writes unless an admin marks it read-write.
- Bounded by a per-source statement timeout and a max-rows cap (a larger result set fails loud).
- PostgreSQL only in v1.
A collection is a durable, queryable, TTL-pruned keyed record store, shared across
channels. Define one under Collections in the admin UI (a name, the field names records
are indexed by, and a default TTL). Then getCollection(name) returns a handle:
await store(fields, payload, options?)— append a record.fieldskeys must be among the collection's indexed fields;payloadis your value (HL7/JSON/text — you parse it).optionsmay override the TTL with{ expireAt }(ISO) or{ ttlSeconds }; otherwise the collection default applies. Records are append-only (no upsert).await find(match, options?)— query.matchis equality on indexed fields;options.filteradds more field predicates (a scalar is equality, an array isIN, multiple fields are AND'd);options.latestreturns only the single newest match;options.limit/options.orderpage and order by creation time (default newest-first). Returns an array of{ id, fields, payload, expireAt, createdAt }.
Reads/writes hit the database directly (no caching). Collections are not a secret store — any channel script can read any collection by name.
Order/report matching — an orders channel stashes each order; a reports channel later pulls the newest matching order to build the outbound report:
// Orders channel — store each inbound order
await getCollection('orders').store(
{ accessionNumber: msg.get('OBR.3.1'), institutionName: channelMap.institution, orderControl: msg.get('ORC.1') },
msg.toString(),
);
// Reports channel — grab the newest NW/SC/XO order for this accession
const [order] = await getCollection('orders').find(
{ accessionNumber: msg.get('OBR.3.1'), institutionName: channelMap.institution },
{ filter: { orderControl: ['XO', 'NW', 'SC'] }, latest: true },
);
if (order) {
channelMap.orderPayload = order.payload;
}FUNCTION-type code templates whose contexts match the current script stage are prepended
to your script before compilation, so their functions are callable directly by name.
CODE_BLOCK templates are editor snippets and are not injected.
Filter — accept only ADT messages:
return msg.get('MSH.9.1') === 'ADT';Transformer — enrich the channel map and rewrite a field:
channelMap.mrn = msg.get('PID.3.1');
msg.set('PID.8', 'U'); // normalize sex to Unknown
logger.info('Normalized message ' + msg.messageControlId);Async transformer (requires httpFetch enabled):
const res = await httpFetch('https://api.example.org/lookup?mrn=' + channelMap.mrn, {
method: 'GET',
timeout: 5000,
});
if (res.status === 200) {
channelMap.enrichment = res.body;
}