ADT (Admit, Discharge, Transfer) messages are the patient-movement backbone of hospital integration. Every registration, bed move, demographic update, and discharge generates an ADT event that pharmacy, lab, billing, and clinical decision support all depend on in real time.
A production Mirth Connect ADT channel must process four core event types correctly: A01 (admit), A04 (register outpatient), A08 (update demographics), and A40 (merge patient records). But those four don't tell the whole story — A02 transfers, A03 discharges, A11/A12/A13 cancellations, and A28/A31 person-level updates all flow through the same channel, and each has downstream consequences if mishandled.
This guide covers the channel architecture, PID and PV1 extraction, Z-segment handling, fan-out routing by event type, A08 duplicate suppression, A40 MRN merge, discharge processing, and ADT-to-FHIR mapping. For the broader deployment context, see the production HL7 interface engine guide.
ADT Event Types: What Each One Signals Downstream
Hospital registration systems send 30+ distinct ADT event types. In practice, most production channels need to handle eight of them correctly and gracefully pass through the rest.
| Event | Trigger | Key Downstream Impact |
|---|---|---|
| A01 | Inpatient admit | Activates lab orders, pharmacy reconciliation, bed assignment, inpatient billing |
| A02 | Transfer between units | Updates bed management, routing for location-based lab orders |
| A03 | Discharge | Closes inpatient billing encounter, triggers discharge summary workflows |
| A04 | Outpatient / ED register | Same destinations as A01 but outpatient billing codes, no inpatient workflows |
| A08 | Demographics/insurance update | 60–70% of ADT volume — requires duplicate suppression |
| A11 | Cancel admit | Reverses A01 in all downstream systems |
| A13 | Cancel discharge | Re-opens encounter; reverses discharge billing |
| A40 | MRN merge | Highest-risk event — must apply to every downstream system consistently |
A01 is the most consequential — a missed or misrouted admit leaves lab, pharmacy, and billing unaware of the patient. A40 is the most dangerous — a partially applied merge creates split records that mix two patients' clinical histories. A08 is the most common — at 60–70% of total ADT volume, unfiltered A08 streams are the leading cause of downstream system overload.
Channel Architecture: Single Source, Multiple Queued Destinations
The correct Mirth pattern is one MLLP source channel receiving all ADT events with multiple destinations, each filtered independently. This is not the only pattern — some teams split ADT into separate channels by event group — but single-source fan-out is simpler to monitor, has one error queue to watch, and makes replay straightforward.
// Source transformer — extract all routing variables in one pass
var eventType = msg['MSH']['MSH.9']['MSG.2'].toString(); // A01, A08, A40 etc.
var mrn = msg['PID']['PID.3']['CX.1'].toString();
var lastName = msg['PID']['PID.5']['XPN.1']['FN.1'].toString();
var firstName = msg['PID']['PID.5']['XPN.2'].toString();
var dob = msg['PID']['PID.7'].toString();
var sex = msg['PID']['PID.8'].toString();
var ssn = msg['PID']['PID.19'].toString();
var patClass = msg['PV1']['PV1.2'].toString(); // I=Inpatient, O=Outpatient, E=Emergency
var location = msg['PV1']['PV1.3']['PL.1'].toString();
var admitDate = msg['PV1']['PV1.44'].toString();
var disDate = msg['PV1']['PV1.45'].toString();
var attendMD = msg['PV1']['PV1.7']['XCN.1'].toString();
var facility = msg['MSH']['MSH.4']['HD.1'].toString();
var msgCtrlId = msg['MSH']['MSH.10'].toString();
channelMap.put('event_type', eventType);
channelMap.put('mrn', mrn);
channelMap.put('pat_class', patClass);
channelMap.put('facility', facility);
channelMap.put('msg_ctrl_id', msgCtrlId);Extract everything in the source transformer. Each destination filter then reads channelMap values without touching the message again. This matters under load — re-parsing a message in five destination transformers is five times the parse overhead.
Destination Filter Examples by System
// Lab system — needs admits, transfers, discharges, outpatient, cancellations
// Destination filter:
var event = channelMap.get('event_type');
return ['A01','A02','A03','A04','A08','A11','A12','A13'].indexOf(event) !== -1;
// Pharmacy — needs admit, discharge, transfer, outpatient (not A08 updates)
var event = channelMap.get('event_type');
return ['A01','A02','A03','A04','A11','A13'].indexOf(event) !== -1;
// Billing — needs admit and discharge only
var event = channelMap.get('event_type');
return ['A01','A03','A04','A13'].indexOf(event) !== -1;
// MPI / Patient Index — needs everything including merges
return true;Enable per-destination queuing. A billing system being down at 2am should not block lab ADT delivery. Independent queues with retry and dead-letter handling per destination is the difference between a resilient ADT channel and one that backs up the entire hospital when one downstream system has an outage.
PID Segment: Demographics Extraction
PID carries all patient identity and demographic data. The fields that matter most vary by destination but always extract the full set in the source transformer to avoid re-parsing.
// Insurance from IN1 segment (if present)
var hasIN1 = msg['IN1'].length() > 0;
if (hasIN1) {
var insuranceName = msg['IN1']['IN1.4']['XON.1'].toString();
var policyNum = msg['IN1']['IN1.36'].toString();
var groupNum = msg['IN1']['IN1.8'].toString();
var planEffDate = msg['IN1']['IN1.12'].toString();
var planExpDate = msg['IN1']['IN1.13'].toString();
channelMap.put('insurance_name', insuranceName);
channelMap.put('policy_num', policyNum);
}
// Next of kin from NK1 (can repeat — handle multiple)
var nokList = [];
for each (var nk1 in msg['NK1']) {
nokList.push({
name : nk1['NK1.2']['XPN.2'].toString() + ' ' + nk1['NK1.2']['XPN.1']['FN.1'].toString(),
relation : nk1['NK1.3']['CWE.2'].toString(),
phone : nk1['NK1.5']['XTN.1'].toString()
});
}
channelMap.put('nok', JSON.stringify(nokList));Reading Z-Segments: Epic, Cerner, and Vendor Extensions
Z-segments carry proprietary fields not defined in the HL7 standard. Epic sends ZPI (extended patient info), ZIN (insurance detail), and ZEP (Epic-specific metadata). Cerner sends ZPD and ZFT. Meditech sends its own Z-segment set.
Every Z-segment access in Mirth requires a length guard — without it, the channel crashes on any ADT message from a trading partner that doesn't send that Z-segment:
// Epic ZPI — extended patient info
if (msg['ZPI'].length() > 0) {
var vipFlag = msg['ZPI']['ZPI.6'].toString(); // VIP status
var patientType = msg['ZPI']['ZPI.1'].toString(); // Patient type code
var religion = msg['ZPI']['ZPI.2'].toString();
var motherMRN = msg['ZPI']['ZPI.11'].toString(); // Linked mother record
channelMap.put('vip_flag', vipFlag);
channelMap.put('patient_type', patientType);
}
// Epic ZIN — insurance extensions
if (msg['ZIN'].length() > 0) {
var primaryPayer = msg['ZIN']['ZIN.1']['CWE.2'].toString();
var memberID = msg['ZIN']['ZIN.4'].toString();
var benefitPlan = msg['ZIN']['ZIN.7'].toString();
channelMap.put('member_id', memberID);
}
// Cerner ZPD — extended patient demographics
if (msg['ZPD'].length() > 0) {
var advDirective = msg['ZPD']['ZPD.1'].toString();
var interpreterNeeded = msg['ZPD']['ZPD.5'].toString();
channelMap.put('interpreter_needed', interpreterNeeded);
}Z-segment guards are what make ADT channels portable across vendors. A channel built and tested against Epic will fail the first time it receives a standard HL7 ADT from a scheduling system that doesn't send ZPI. Document which Z-segments each trading partner sends in your integration runbook, and write guards for all of them from day one.
A03 Discharge: Closing the Encounter
A03 is second only to A01 in downstream impact. A missed discharge leaves billing with an open inpatient encounter, lab with stale order context, and pharmacy with active inpatient medication orders that should have been discontinued.
// A03 — discharge processing
if (channelMap.get('event_type') === 'A03') {
var dischargeDate = msg['PV1']['PV1.45'].toString();
var dischargeDisp = msg['PV1']['PV1.36'].toString(); // Discharge disposition code
// Common codes: 01=Home, 02=SNF, 03=Skilled care, 07=AMA, 20=Expired
var dischargeDest = msg['PV1']['PV1.37']['CWE.2'].toString();
var lengthOfStay = msg['PV1']['PV1.45'].toString(); // Days
channelMap.put('discharge_dispo', dischargeDisp);
channelMap.put('discharge_date', dischargeDate);
// Flag expired patients — some destinations handle this differently
if (dischargeDisp === '20' || dischargeDisp === '40' || dischargeDisp === '41') {
channelMap.put('patient_expired', 'true');
logger.info('Patient expired — MRN: ' + channelMap.get('mrn'));
}
}Discharge disposition code 20 (expired) needs special handling in most downstream systems. Pharmacy should discontinue all active orders. Patient portal access may need to be revoked. Population health should update registry records. Flag this in channelMap so destination transformers can apply the right logic per system.
A40 MRN Merge: High-Stakes Processing
A40 is the highest-risk ADT event. It instructs every downstream system to retire a duplicate MRN and move all clinical history to the surviving record. A merge that applies to some destinations but not others produces split records — different systems have different versions of which MRN is active, and clinical data gets attributed to the wrong patient identity.
// A40 — MRN merge with mandatory audit before routing
if (channelMap.get('event_type') === 'A40') {
var survivingMRN = msg['PID']['PID.3']['CX.1'].toString();
var duplicateMRN = msg['MRG']['MRG.1']['CX.1'].toString();
var survivingAN = msg['PID']['PID.18']['CX.1'].toString(); // Account number
var duplicateAN = msg['MRG']['MRG.3']['CX.1'].toString();
// Hard stop — missing MRN is an unrecoverable data integrity error
if (!survivingMRN || !duplicateMRN) {
throw new Error('A40 missing MRN — surviving=' + survivingMRN + ' retiring=' + duplicateMRN);
}
// Immutable audit record — MUST be written before any routing begins
var audit = {
event : 'MRN_MERGE',
surviving : survivingMRN,
retired : duplicateMRN,
survAcct : survivingAN,
retAcct : duplicateAN,
facility : channelMap.get('facility'),
msgCtrlId : channelMap.get('msg_ctrl_id'),
timestamp : new Date().toISOString()
};
channelMap.put('merge_audit', JSON.stringify(audit));
channelMap.put('surviving_mrn', survivingMRN);
channelMap.put('duplicate_mrn', duplicateMRN);
logger.warn('MRN MERGE: retiring ' + duplicateMRN + ' → ' + survivingMRN);
}The audit write must happen in the source transformer before any destination routing begins. If Mirth errors after routing to destination 1 but before destination 2, you need the audit record to identify the incomplete merge and manually complete it. The account number pair (PID-18 and MRG-3) matters too — some downstream systems key records by account number, not MRN, and the merge needs to apply to both.
A08 Duplicate Suppression: Cut Volume by 60%
A08 (Update Patient Information) typically accounts for 60–70% of total ADT volume. Any field change — insurance update, address correction, attending physician reassignment — generates an A08. Many of these are minor changes that don't affect the fields your downstream systems care about, but they still consume processing capacity on every destination.
Fingerprint comparison in Mirth suppresses A08 events where no clinically relevant field changed:
// A08 duplicate suppression
if (channelMap.get('event_type') === 'A08') {
// Fingerprint covers fields downstream systems care about
var fp = [
channelMap.get('mrn'),
msg['PID']['PID.5']['XPN.1']['FN.1'].toString(), // Last name
msg['PID']['PID.5']['XPN.2'].toString(), // First name
msg['PID']['PID.7'].toString(), // DOB
msg['PID']['PID.8'].toString(), // Sex
msg['PV1']['PV1.3']['PL.1'].toString(), // Location
msg['PV1']['PV1.7']['XCN.1'].toString(), // Attending MD
msg['IN1'].length() > 0 ? msg['IN1']['IN1.36'].toString() : '', // Policy number
msg['ZIN'].length() > 0 ? msg['ZIN']['ZIN.4'].toString() : '' // Member ID
].join('|');
var cacheKey = 'adtfp_' + channelMap.get('facility') + '_' + channelMap.get('mrn');
var cached = globalMap.get(cacheKey);
if (fp === cached) {
channelMap.put('suppress', 'true');
logger.debug('A08 suppressed — no relevant change: MRN=' + channelMap.get('mrn'));
} else {
globalMap.put(cacheKey, fp);
channelMap.put('suppress', 'false');
}
}
// Add to destination filter for non-critical systems:
// if (channelMap.get('suppress') === 'true') return false;Key the fingerprint cache by facility + MRN, not just MRN — the same MRN number can exist at different facilities in multi-site deployments. The globalMap cache is in-memory and per-Mirth-instance. For high-availability Mirth clusters, use a database-backed cache or Redis so suppression state is consistent across instances. See the Mirth Connect HA guide for the cluster architecture.
ADT to FHIR R4 Mapping
For downstream systems that consume FHIR rather than HL7 v2, ADT messages map to two primary FHIR resources:
| HL7 ADT Field | FHIR R4 Resource / Element |
|---|---|
| PID-3 (MRN) | Patient.identifier |
| PID-5 (Name) | Patient.name |
| PID-7 (DOB) | Patient.birthDate |
| PID-8 (Sex) | Patient.gender |
| PV1-2 (Patient Class) | Encounter.class (inpatient / outpatient) |
| PV1-3 (Location) | Encounter.location |
| PV1-44 (Admit Date) | Encounter.period.start |
| PV1-45 (Discharge Date) | Encounter.period.end |
| PV1-7 (Attending MD) | Encounter.participant |
| IN1 (Insurance) | Coverage resource |
ADT event type maps to Encounter status: A01 → in-progress, A03 → finished, A11 → cancelled. For A40 merges in FHIR, the retired patient record gets Patient.active = false and a Patient.link pointing to the surviving record with type = 'replaced-by'.
For the full HL7 v2 to FHIR R4 transformation pattern in Mirth Connect, see the HL7 v2 to FHIR APIs guide.
Monitoring Your ADT Channel
Message throughput by event type. ADT volume follows clinical patterns — peaks at shift change (7 am, 3 pm, 7 pm), drops overnight. A sudden drop in A01 volume during business hours means registration is down or the MLLP connection dropped. Alert on throughput by event type, not just total volume, so you can distinguish between a full outage and a specific event type failing.
A40 completion rate. Every A40 received should generate a successful write to every downstream system. Build a post-processing check: query each destination database for the surviving MRN one minute after the A40 processes and verify the record exists. Flag any destination where it doesn't. Unverified merges are the highest-risk undetected error in ADT processing.
A08 suppression rate. Track the percentage of A08 events the fingerprint filter suppresses. 50–65% is typical and healthy. If suppression drops to near zero, the cache may have been cleared or the fingerprint fields may have been changed. If suppression climbs above 80%, the registration system may be generating redundant A08 events that should be investigated.
Dead-letter queue depth per destination. A billing system outage overnight means A01 and A03 events pile up in the billing destination queue. Monitor depth per destination and alert when it exceeds your SLA threshold — typically 100–500 messages depending on volume.
Full monitoring setup is in the Mirth production monitoring guide.



