Transformers
Transformers are the core logic of every channel. Written in TypeScript, they receive an incoming message, manipulate it however you need, and return the result to continue through the pipeline. Because transformers are plain TypeScript functions, you have the full npm ecosystem at your disposal — parse HL7v2 with node-hl7-client, build FHIR resources, convert CSV, reshape JSON, or call any third-party API.
Function Signature
Section titled “Function Signature”Every transformer exports a single transform function:
export function transform( msg: IntuMessage, ctx: IntuContext): IntuMessage | IntuMessage[] | null;| Parameter | Type | Description |
|---|---|---|
msg | IntuMessage | The inbound message with rawData, content, headers, and metadata |
ctx | IntuContext | Pipeline context — map variables, logger, channel config |
Return Types
Section titled “Return Types”The return type controls how the pipeline handles the transformer output:
| Return | Behavior | Use Case |
|---|---|---|
IntuMessage | One message continues through the pipeline | 1:1 transformation |
IntuMessage[] | Each element continues independently | Fan-out / batch splitting |
null | Message is silently dropped | Filtering / conditional routing |
// 1:1 — transform a single messageexport function transform(msg: IntuMessage, ctx: IntuContext): IntuMessage { msg.content = JSON.stringify({ wrapped: JSON.parse(msg.rawData) }); return msg;}// 1:many — fan out a FHIR Bundle into individual resourcesexport function transform(msg: IntuMessage, ctx: IntuContext): IntuMessage[] { const bundle = JSON.parse(msg.rawData); return bundle.entry.map((entry: any) => ({ ...msg, content: JSON.stringify(entry.resource), }));}// Filter — drop messages that don't match criteriaexport function transform(msg: IntuMessage, ctx: IntuContext): IntuMessage | null { const hl7 = msg.rawData; if (!hl7.startsWith("MSH|")) return null; return msg;}Auto-Compilation and Hot Reload
Section titled “Auto-Compilation and Hot Reload”TypeScript is compiled automatically when the engine starts. During development (npm run dev), the engine watches for file changes and recompiles on save — no manual build step required.
Type Safety with intu.d.ts
Section titled “Type Safety with intu.d.ts”Every scaffolded project includes src/types/intu.d.ts, which declares the IntuMessage and IntuContext interfaces. Your editor provides autocomplete, type checking, and inline documentation out of the box.
interface IntuMessage { rawData: string; content: string; headers: Record<string, string>; sourceType: string; channelId: string; messageId: string; timestamp: string;}
interface IntuContext { logger: IntuLogger; globalMap: Map<string, any>; channelMap: Map<string, any>; responseMap: Map<string, any>; connectorMap: Map<string, any>; channelId: string; channelName: string;}Using the Full npm Ecosystem
Section titled “Using the Full npm Ecosystem”Transformers run in a standard Node.js environment. Install any npm package and import it directly:
npm install node-hl7-client fhir fast-csv fast-xml-parserimport { Hl7Message } from "node-hl7-client";
export function transform(msg: IntuMessage, ctx: IntuContext): IntuMessage { const hl7 = new Hl7Message(msg.rawData); const patientName = hl7.get("PID.5"); const mrn = hl7.get("PID.3.1");
msg.content = JSON.stringify({ resourceType: "Patient", identifier: [{ system: "http://hospital.example/mrn", value: mrn }], name: [{ family: patientName.split("^")[0], given: [patientName.split("^")[1]] }], });
return msg;}Examples
Section titled “Examples”HL7v2 ADT to FHIR Patient
Section titled “HL7v2 ADT to FHIR Patient”import { Hl7Message } from "node-hl7-client";
export function transform(msg: IntuMessage, ctx: IntuContext): IntuMessage { const hl7 = new Hl7Message(msg.rawData);
const pid = { family: hl7.get("PID.5.1"), given: hl7.get("PID.5.2"), dob: hl7.get("PID.7"), gender: hl7.get("PID.8"), mrn: hl7.get("PID.3.1"), ssn: hl7.get("PID.19"), address: { line: hl7.get("PID.11.1"), city: hl7.get("PID.11.3"), state: hl7.get("PID.11.4"), postalCode: hl7.get("PID.11.5"), }, };
const patient = { resourceType: "Patient", identifier: [ { system: "http://hospital.example/mrn", value: pid.mrn }, { system: "http://hl7.org/fhir/sid/us-ssn", value: pid.ssn }, ], name: [{ family: pid.family, given: [pid.given] }], birthDate: `${pid.dob.slice(0, 4)}-${pid.dob.slice(4, 6)}-${pid.dob.slice(6, 8)}`, gender: pid.gender === "M" ? "male" : pid.gender === "F" ? "female" : "unknown", address: [ { line: [pid.address.line], city: pid.address.city, state: pid.address.state, postalCode: pid.address.postalCode, }, ], };
msg.content = JSON.stringify(patient); return msg;}CSV Lab Results to FHIR Observations
Section titled “CSV Lab Results to FHIR Observations”import { parse } from "fast-csv";
export async function transform( msg: IntuMessage, ctx: IntuContext): Promise<IntuMessage[]> { const rows: any[] = [];
await new Promise<void>((resolve, reject) => { const stream = parse({ headers: true }).on("data", (row) => rows.push(row)).on("end", resolve).on("error", reject); stream.write(msg.rawData); stream.end(); });
return rows.map((row) => ({ ...msg, content: JSON.stringify({ resourceType: "Observation", status: "final", code: { coding: [{ system: "http://loinc.org", code: row.loinc_code, display: row.test_name }], }, subject: { reference: `Patient/${row.patient_id}` }, valueQuantity: { value: parseFloat(row.value), unit: row.unit, system: "http://unitsofmeasure.org", code: row.unit, }, effectiveDateTime: row.collected_date, }), }));}JSON Payload Reshape
Section titled “JSON Payload Reshape”export function transform(msg: IntuMessage, ctx: IntuContext): IntuMessage { const input = JSON.parse(msg.rawData);
msg.content = JSON.stringify({ id: input.patient_id, fullName: `${input.first_name} ${input.last_name}`, dob: input.date_of_birth, facility: input.sending_facility ?? "UNKNOWN", receivedAt: msg.timestamp, });
return msg;}XML CDA to JSON
Section titled “XML CDA to JSON”import { XMLParser } from "fast-xml-parser";
export function transform(msg: IntuMessage, ctx: IntuContext): IntuMessage { const parser = new XMLParser({ ignoreAttributes: false }); const doc = parser.parse(msg.rawData);
const patient = doc.ClinicalDocument?.recordTarget?.patientRole?.patient;
msg.content = JSON.stringify({ resourceType: "Patient", name: [ { family: patient?.name?.family, given: [patient?.name?.given], }, ], gender: patient?.administrativeGenderCode?.["@_code"] === "M" ? "male" : "female", birthDate: patient?.birthTime?.["@_value"], });
return msg;}Map Variables
Section titled “Map Variables”Map variables let you share state across pipeline stages. They are available on ctx in every transformer:
| Map | Scope | Lifetime | Use Case |
|---|---|---|---|
globalMap | All channels | Engine lifetime | Lookup tables, shared config |
channelMap | Single channel | Channel lifetime | Sequence counters, caches |
responseMap | Single message | Current message | Destination response data |
connectorMap | Single connector step | Current step | Inter-step data passing |
export function transform(msg: IntuMessage, ctx: IntuContext): IntuMessage { let seq = (ctx.channelMap.get("sequence") ?? 0) + 1; ctx.channelMap.set("sequence", seq);
const data = JSON.parse(msg.rawData); data.sequenceNumber = seq;
const facilityName = ctx.globalMap.get("facilityLookup")?.get(data.facilityCode) ?? "Unknown"; data.facilityName = facilityName;
msg.content = JSON.stringify(data); return msg;}Destination-Specific Transformers
Section titled “Destination-Specific Transformers”When a channel has multiple destinations, you can customize the output for each one. Place a transformer file alongside the destination reference in channel.yaml:
destinations: - ref: fhir-server transformer: entrypoint: src/channels/my-channel/fhir-transformer.ts - ref: data-warehouse transformer: entrypoint: src/channels/my-channel/warehouse-transformer.tsEach destination transformer receives the output of the main channel transformer and can reshape it for that specific target:
// fhir-transformer.ts — wrap in a FHIR Bundle for the FHIR serverexport function transform(msg: IntuMessage, ctx: IntuContext): IntuMessage { const resource = JSON.parse(msg.content); msg.content = JSON.stringify({ resourceType: "Bundle", type: "transaction", entry: [{ resource, request: { method: "PUT", url: `${resource.resourceType}/${resource.id}` } }], }); return msg;}// warehouse-transformer.ts — flatten to a row for the data warehouseexport function transform(msg: IntuMessage, ctx: IntuContext): IntuMessage { const patient = JSON.parse(msg.content); msg.content = JSON.stringify({ mrn: patient.identifier?.[0]?.value, last_name: patient.name?.[0]?.family, first_name: patient.name?.[0]?.given?.[0], dob: patient.birthDate, gender: patient.gender, loaded_at: new Date().toISOString(), }); return msg;}Response Transformers
Section titled “Response Transformers”Response transformers process the reply from a destination after delivery. They are useful for extracting IDs, logging acknowledgements, or feeding data back through responseMap.
destinations: - ref: fhir-server response_transformer: entrypoint: src/channels/my-channel/response-handler.tsexport function transform(msg: IntuMessage, ctx: IntuContext): IntuMessage { const response = JSON.parse(msg.rawData);
ctx.responseMap.set("fhirId", response.id); ctx.responseMap.set("fhirStatus", response.meta?.versionId); ctx.logger.info(`FHIR resource created: ${response.resourceType}/${response.id}`);
return msg;}Preprocessors and Postprocessors
Section titled “Preprocessors and Postprocessors”Preprocessors run before the main transformer. Postprocessors run after all destinations have been written. Both use the same function signature.
transformer: entrypoint: src/channels/my-channel/transformer.ts preprocessor: src/channels/my-channel/preprocessor.ts postprocessor: src/channels/my-channel/postprocessor.ts// preprocessor.ts — strip BOM and normalize line endings before transformationexport function transform(msg: IntuMessage, ctx: IntuContext): IntuMessage { let raw = msg.rawData; if (raw.charCodeAt(0) === 0xfeff) raw = raw.slice(1); raw = raw.replace(/\r\n/g, "\r"); msg.rawData = raw; msg.content = raw; return msg;}// postprocessor.ts — record metrics after all destinations completeexport function transform(msg: IntuMessage, ctx: IntuContext): IntuMessage { const fhirId = ctx.responseMap.get("fhirId"); ctx.logger.info(`Pipeline complete — FHIR ID: ${fhirId}, message: ${msg.messageId}`); ctx.channelMap.set("lastProcessedAt", new Date().toISOString()); return msg;}Error Handling
Section titled “Error Handling”Throwing an error inside a transformer stops the pipeline for that message. The message is routed to error handling (dead-letter queue, retry, or alert) based on your channel configuration.
export function transform(msg: IntuMessage, ctx: IntuContext): IntuMessage { const data = JSON.parse(msg.rawData);
if (!data.patient_id) { throw new Error("Missing required field: patient_id"); }
if (!data.mrn?.match(/^MRN-\d{8}$/)) { throw new Error(`Invalid MRN format: ${data.mrn}`); }
msg.content = JSON.stringify(data); return msg;}Code Template Libraries
Section titled “Code Template Libraries”Shared logic can be extracted into code template libraries and reused across channels. Define templates in a code_templates directory and reference them:
my-project/├── src/│ ├── code_templates/│ │ ├── hl7-helpers.ts│ │ └── fhir-builders.ts│ └── channels/│ ├── channel-a/│ │ └── transformer.ts ← imports from code_templates│ └── channel-b/│ └── transformer.ts ← imports from code_templatesexport function buildPatient(mrn: string, family: string, given: string) { return { resourceType: "Patient", identifier: [{ system: "http://hospital.example/mrn", value: mrn }], name: [{ family, given: [given] }], };}
export function buildObservation( patientRef: string, loincCode: string, value: number, unit: string) { return { resourceType: "Observation", status: "final", code: { coding: [{ system: "http://loinc.org", code: loincCode }] }, subject: { reference: patientRef }, valueQuantity: { value, unit, system: "http://unitsofmeasure.org", code: unit }, };}import { buildPatient } from "../../code_templates/fhir-builders";import { Hl7Message } from "node-hl7-client";
export function transform(msg: IntuMessage, ctx: IntuContext): IntuMessage { const hl7 = new Hl7Message(msg.rawData); const patient = buildPatient( hl7.get("PID.3.1"), hl7.get("PID.5.1"), hl7.get("PID.5.2") ); msg.content = JSON.stringify(patient); return msg;}