Skip to content

Validators

Validators check messages before they reach destinations. They enforce schemas, required fields, data formats, and business rules — catching problems early so invalid data never leaves your pipeline.

Every validator exports a single validate function:

export function validate(
msg: IntuMessage,
ctx: IntuContext
): ValidationResult;
ParameterTypeDescription
msgIntuMessageThe message to validate (after transformation)
ctxIntuContextPipeline context — map variables, logger, channel config
interface ValidationResult {
valid: boolean;
errors?: string[];
}

When valid is true, the message continues to its destinations. When valid is false, the message is rejected and routed to error handling — the errors array is attached to the error record for debugging.

// Minimal passing validator
export function validate(msg: IntuMessage, ctx: IntuContext): ValidationResult {
return { valid: true };
}
// Rejecting a message with reasons
export function validate(msg: IntuMessage, ctx: IntuContext): ValidationResult {
return {
valid: false,
errors: [
"PID segment missing required MRN (PID.3)",
"PV1.2 patient class is empty",
],
};
}

Validators are configured in channel.yaml under the validator key:

validator:
entrypoint: src/channels/my-channel/validator.ts

Verify that an HL7v2 message contains the segments your downstream system expects:

export function validate(msg: IntuMessage, ctx: IntuContext): ValidationResult {
const raw = msg.content;
const errors: string[] = [];
const requiredSegments = ["MSH", "PID", "PV1", "EVN"];
for (const seg of requiredSegments) {
const pattern = new RegExp(`^${seg}\\|`, "m");
if (!pattern.test(raw)) {
errors.push(`Missing required segment: ${seg}`);
}
}
const mshFields = raw.split("\r")[0]?.split("|") ?? [];
if (!mshFields[8]) {
errors.push("MSH.9 (Message Type) is empty");
}
if (!mshFields[9]) {
errors.push("MSH.10 (Message Control ID) is empty");
}
return { valid: errors.length === 0, errors };
}

Ensure a FHIR Patient resource contains the minimum fields before sending to a FHIR server:

export function validate(msg: IntuMessage, ctx: IntuContext): ValidationResult {
const patient = JSON.parse(msg.content);
const errors: string[] = [];
if (patient.resourceType !== "Patient") {
errors.push(`Expected resourceType "Patient", got "${patient.resourceType}"`);
}
if (!patient.identifier?.length) {
errors.push("At least one identifier is required");
}
if (!patient.name?.length) {
errors.push("At least one name is required");
} else {
const name = patient.name[0];
if (!name.family) errors.push("Patient name.family is required");
if (!name.given?.length) errors.push("Patient name.given is required");
}
if (!patient.birthDate) {
errors.push("birthDate is required");
} else if (!/^\d{4}-\d{2}-\d{2}$/.test(patient.birthDate)) {
errors.push(`birthDate must be YYYY-MM-DD format, got "${patient.birthDate}"`);
}
if (!["male", "female", "other", "unknown"].includes(patient.gender)) {
errors.push(`Invalid gender value: "${patient.gender}"`);
}
return { valid: errors.length === 0, errors };
}

Use a JSON schema library for complex structural validation:

import Ajv from "ajv";
const ajv = new Ajv({ allErrors: true });
const schema = {
type: "object",
required: ["patient_id", "mrn", "last_name", "first_name"],
properties: {
patient_id: { type: "string", minLength: 1 },
mrn: { type: "string", pattern: "^MRN-\\d{8}$" },
last_name: { type: "string", minLength: 1 },
first_name: { type: "string", minLength: 1 },
dob: { type: "string", format: "date" },
gender: { type: "string", enum: ["M", "F", "U"] },
},
additionalProperties: false,
};
const validateSchema = ajv.compile(schema);
export function validate(msg: IntuMessage, ctx: IntuContext): ValidationResult {
const data = JSON.parse(msg.content);
const isValid = validateSchema(data);
if (isValid) {
return { valid: true };
}
const errors = (validateSchema.errors ?? []).map(
(err) => `${err.instancePath || "/"} ${err.message}`
);
return { valid: false, errors };
}

Enforce organization-specific rules that go beyond structural validation:

export function validate(msg: IntuMessage, ctx: IntuContext): ValidationResult {
const data = JSON.parse(msg.content);
const errors: string[] = [];
if (data.admitDate && data.dischargeDate) {
if (new Date(data.dischargeDate) < new Date(data.admitDate)) {
errors.push("Discharge date cannot be before admit date");
}
}
const validDepartments = ctx.globalMap.get("validDepartments") as Set<string> | undefined;
if (validDepartments && !validDepartments.has(data.department)) {
errors.push(`Unknown department: "${data.department}"`);
}
if (data.age !== undefined && (data.age < 0 || data.age > 150)) {
errors.push(`Age out of plausible range: ${data.age}`);
}
const knownMRNs = ctx.globalMap.get("knownMRNs") as Set<string> | undefined;
if (knownMRNs && !knownMRNs.has(data.mrn)) {
ctx.logger.warn(`Unrecognized MRN: ${data.mrn} — allowing but flagging`);
}
return { valid: errors.length === 0, errors };
}

Validate that observation values fall within clinically plausible ranges:

const labRanges: Record<string, { min: number; max: number; unit: string }> = {
"2339-0": { min: 30, max: 500, unit: "mg/dL" }, // Glucose
"2160-0": { min: 0.1, max: 20, unit: "mg/dL" }, // Creatinine
"6690-2": { min: 1000, max: 50000, unit: "10*3/uL" }, // WBC
"718-7": { min: 3, max: 25, unit: "g/dL" }, // Hemoglobin
};
export function validate(msg: IntuMessage, ctx: IntuContext): ValidationResult {
const obs = JSON.parse(msg.content);
const errors: string[] = [];
if (obs.resourceType !== "Observation") {
return { valid: false, errors: ["Expected Observation resource"] };
}
const loincCode = obs.code?.coding?.find(
(c: any) => c.system === "http://loinc.org"
)?.code;
if (!loincCode) {
errors.push("Missing LOINC code");
return { valid: false, errors };
}
const range = labRanges[loincCode];
if (range && obs.valueQuantity?.value !== undefined) {
const val = obs.valueQuantity.value;
if (val < range.min || val > range.max) {
errors.push(
`Value ${val} ${range.unit} out of plausible range [${range.min}${range.max}] for LOINC ${loincCode}`
);
}
}
if (!obs.subject?.reference) {
errors.push("Observation.subject.reference is required");
}
if (!obs.effectiveDateTime) {
errors.push("Observation.effectiveDateTime is required");
}
return { valid: errors.length === 0, errors };
}

Validators and filters both prevent messages from reaching destinations, but they serve different purposes:

ValidatorFilter (return null from transformer)
IntentReject invalid dataSilently drop unwanted data
FeedbackError record with reasonsNo record — message disappears
MetricsCounted as validation errorsCounted as filtered
RoutingError handling / dead-letter queueNothing

Validator — rejecting bad data:

export function validate(msg: IntuMessage, ctx: IntuContext): ValidationResult {
const patient = JSON.parse(msg.content);
if (!patient.identifier?.length) {
return { valid: false, errors: ["Patient must have at least one identifier"] };
}
return { valid: true };
}

Filter — dropping irrelevant messages in a transformer:

export function transform(msg: IntuMessage, ctx: IntuContext): IntuMessage | null {
const hl7 = msg.rawData;
const messageType = hl7.split("|")[8] ?? "";
if (!messageType.startsWith("ADT^A01")) return null;
return msg;
}