HL7v2 to FHIR conversion
Example HL7v2 messages parsing and transformation in Aidbox
18 cells · updated Mar 30, 2023
Documentation on HL7v2 Aidbox integration: https://docs.aidbox.app/modules-1/hl7-v2-integration
Input Data example
HL7v2
The incoming HL7v2 message:
MSH|^~\&||org-1|||20200101120000||ADT^A08|my-unique-message-id|P|2.9|||NE|AL
EVN|A08|20200101120000||Resident Diagnosis Update|1234^MyHealthCare^john.doe@myhealthcare.com^^^|20200101120000
PID|1|pt-1|org-1^^^^FI~pt-1^^^^PI||Doe^John^^^||19700101|M|||1 Lane^^Austin^TX^78000
PV1|1|I|1^1^A^ABC||||345^Adhoc^Test^^^||||||||||||1500|||||||||||||||||||||||||20200101120000
This message is parsed by HL7v2 Aidbox module like this:
EVN:
operator_id:
- id: '1234'
given: john.doe@myhealthcare.com
family:
surname: MyHealthCare
event_occurred:
time: '20200101120000'
event_type_code: A08
event_reason_code: Resident Diagnosis Update
recorded_datetime:
time: '20200101120000'
PID:
name:
- given: John
family:
surname: Doe
gender: M
set_id: '1'
address:
- city: Austin
state: TX
street:
text: 1 Lane
postal_code: '78000'
birth_date:
time: '19990101'
patient_id:
value: pt-1
identifiers:
- value: org-1
system: FI
- value: pt-1
system: PI
HL7 v2 Mapping
Mapping resources describe a transformation of ingested HL7v2 message to FHIR format.
HL7v2 messages can be transformed into multiple FHIR resources which then should be saved into Aidbox, to do this mapping describes a transaction Bundle that will be executed by Aidbox HL7v2 API to get these resources created or updated in Aidbox.
A mapping can be quite large since multiple resources can be created from a single message. To make developing mappings easier a mapping can be split into multiple Mapping resources referring to each other.
Following Mapping resource (Mapping/hl7v2-demo) describes a transaction Bunlde resource and refers to 3 other Mapping resources (Mapping/msh-segment, Mapping/pid-segment, Mapping/pv1-segment) describing different FHIR resources as entries of the Bundle:
PUT /Mapping/hl7v2-demo
content-type: text/yaml
accept: text/yaml
body:
$let:
- {FACILITY_ID: $ msg.MSH.facility.ns}
- {FAC_NAME: $ msg.MSH.facility.uid || "Care Center"}
- {GEN_FAC_ID: $ str(abs(hash(str(FAC_ID) + "myhealthcare")))}
- {PATIENT_ID: $ str(abs(hash(str(msg.PID.account_number.id) + GEN_FAC_ID + "myhealthcare")))}
- meta:
tag:
- {code: myhealthcare, system: 'https://myhealthcare/resource/source'}
- {EVENT_TYPE: $ msg.MSH.type.event}
- {SOURCE_EMR: my-emr}
- payload:
- $if: $ FACILITY_ID && msg.MSH
$then: {$include: msh-segment}
- $if: $ msg.PID
$then: {$include: pid-segment}
- $if: $ msg.PV1.attending_doctor
$then: {$include: pv1-segment}
- mapped: {$as: p, $map: $ payload, $body: $ p.entry}
$body: {type: transaction, entry: $ flatten(mapped), resourceType: Bundle}
body:
$let:
- {FACILITY_ID: $ msg.MSH.facility.ns}
- {FAC_NAME: $ msg.MSH.facility.uid || "Care Center"}
- {GEN_FAC_ID: $ str(abs(hash(str(FAC_ID) + "myhealthcare")))}
- {PATIENT_ID: $ str(abs(hash(str(msg.PID.account_number.id) + GEN_FAC_ID + "myhealthcare")))}
- meta:
tag:
- {code: myhealthcare, system: 'https://myhealthcare/resource/source'}
- {EVENT_TYPE: $ msg.MSH.type.event}
- {SOURCE_EMR: my-emr}
- payload:
- $if: $ FACILITY_ID && msg.MSH
$then: {$include: msh-segment}
- $if: $ msg.PID
$then: {$include: pid-segment}
- $if: $ msg.PV1.attending_doctor
$then: {$include: pv1-segment}
- mapped: {$as: p, $map: $ payload, $body: $ p.entry}
$body: {type: transaction, entry: $ flatten(mapped), resourceType: Bundle}
id: hl7v2-demo
resourceType: Mapping
meta: {lastUpdated: '2023-03-27T14:01:03.876015Z', createdAt: '2023-03-27T13:56:36.739078Z', versionId: '363'}
Create dependent mappings
For each segment create a mapping translating this segment from HL7v2 to FHIR
MSH segment
This segment contains metadata. One of the fields contains facility id. We create an empty Organization resource if it doesn't exist yet.
PUT /Mapping/msh-segment
content-type: text/yaml
accept: text/yaml
body:
type: transaction
entry:
- request:
url: $ "/Organization/" + FACILITY_ID
method: PUT
IfNoneExist: $ FACILITY_ID
resource:
id: $ FACILITY_ID
resourceType: Organization
resourceType: Bundlebody:
type: transaction
entry:
- request: {url: $ "/Organization/" + FACILITY_ID, method: PUT, IfNoneExist: $ FACILITY_ID}
resource: {id: $ FACILITY_ID, resourceType: Organization}
resourceType: Bundle
id: msh-segment
resourceType: Mapping
meta: {lastUpdated: '2023-03-27T14:02:57.561512Z', createdAt: '2023-03-27T13:56:48.382184Z', versionId: '367'}
EVN segment
We want to skip this segment of the HL7v2 message for keeping this example short. If parts of incoming data are not mentioned in a mapping then they will be ignored and not present in resulting FHIR resources
PID segment
This segment describes parts of Patient resource
PUT /Mapping/pid-segment
content-type: text/yaml
accept: text/yaml
body:
$let:
- identifiers:
$map: $ msg.PID.identifiers
$as: i
$body:
$switch: $ i.system
FI:
type:
text: Facility ID
coding:
- code: FI
system: http://terminology.hl7.org/CodeSystem/v2-0203
display: Facility ID
value: $ i.id
system: $ "https://" + SOURCE_EMR + ".com/patient/fi"
MR:
type:
text: Medical record number
coding:
- code: MR
system: http://terminology.hl7.org/CodeSystem/v2-0203
display: Medical record number
value: $ i.id
system: $ "https://" + SOURCE_EMR + ".com/patient/mr"
$body:
type: transaction
resourceType: Bundle
entry:
- request:
url: $ "/Patient/" + PATIENT_ID
method: PUT
resource:
address:
$map: $ msg.PID.address
$as: a
$body:
city: $ a.city
line:
- $ a.street.text
state: $ a.state
postalCode: $ a.postal_code
managingOrganization:
id: $ FACILITY_ID
resourceType: Organization
name:
family: $ msg.PID.name.family.surname
given: $ msg.PID.name.given
birthDate:
$if: $ msg.PID.birth_date.time
$then: $ convertDate(msg.PID.birth_date.time, "YYYYMMdd", "YYYY-MM-dd")
resourceType: Patient
id: $ PATIENT_ID
identifier: $ identifiers
gender:
$switch: $ toLowerCase(msg.PID.gender)
m: male
f: female
$default: $ toLowerCase(msg.PID.gender)
body:
$let:
- identifiers:
$as: i
$map: $ msg.PID.identifiers
$body:
FI:
type:
text: Facility ID
coding:
- {code: FI, system: 'http://terminology.hl7.org/CodeSystem/v2-0203', display: Facility ID}
value: $ i.id
system: $ "https://" + SOURCE_EMR + ".com/patient/fi"
MR:
type:
text: Medical record number
coding:
- {code: MR, system: 'http://terminology.hl7.org/CodeSystem/v2-0203', display: Medical record number}
value: $ i.id
system: $ "https://" + SOURCE_EMR + ".com/patient/mr"
$switch: $ i.system
$body:
type: transaction
entry:
- request: {url: $ "/Patient/" + PATIENT_ID, method: PUT}
resource:
id: $ PATIENT_ID
name: {given: $ msg.PID.name.given, family: $ msg.PID.name.family.surname}
gender: {f: female, m: male, $switch: $ toLowerCase(msg.PID.gender), $default: $ toLowerCase(msg.PID.gender)}
address:
$as: a
$map: $ msg.PID.address
$body:
city: $ a.city
line: [$ a.street.text]
state: $ a.state
postalCode: $ a.postal_code
birthDate: {$if: $ msg.PID.birth_date.time, $then: '$ convertDate(msg.PID.birth_date.time, "YYYYMMdd", "YYYY-MM-dd")'}
identifier: $ identifiers
resourceType: Patient
managingOrganization: {id: $ FACILITY_ID, resourceType: Organization}
resourceType: Bundle
id: pid-segment
resourceType: Mapping
meta: {lastUpdated: '2023-03-27T13:56:51.016953Z', createdAt: '2023-03-27T13:56:51.016953Z', versionId: '358'}
PV1
Using this segment we create an Encounter and a Practitioner resources
PUT /Mapping/pv1-segment
content-type: text/yaml
accept: text/yaml
body:
type: transaction
entry:
$let:
- npi: >-
$ replace(msg.PV1.attending_doctor.0.authority.ns ||
msg.PV1.attending_doctor.0.authority.uid ||
msg.PV1.attending_doctor.0.id, "\D", "")
- practitioner_id: $ msg.PV1.attending_doctor.0.id
- bed: $ msg.PV1.assigned_patient_location.bed
- room: $ msg.PV1.assigned_patient_location.room
- point_of_care: $ msg.PV1.assigned_patient_location.point_of_care
- bed_display:
$if: $ bed
$then: $ "bed " + bed
- room_display:
$if: $ room
$then: $ "room " + room
- point_of_care_display:
$if: $ point_of_care
$then: $ "point of care " + point_of_care
- loc_id:
$if: $ bed || room || point_of_care
$then: >-
$ str(abs(hash(replace(bed + room + point_of_care, "\s",
""))))
$body:
- request:
url: $ "/Practitioner/" + practitioner_id
method: PUT
IfNoneExist: $ "_id=" + practitioner_id
resource:
id: $ practitioner_id
name:
- given:
- $ msg.PV1.attending_doctor.0.given
family: $ msg.PV1.attending_doctor.0.family.surname
- request:
url: $ "/Encounter"
method: POST
resource:
subject:
id: $ PATIENT_ID
resourceType: Patient
status: planned
class:
system: 'http://terminology.hl7.org/CodeSystem/v3-ActCode'
code: IMP
display: inpatient encounter
participant:
- individual:
resourceType: Practitioner
id: $ practitioner_id
resourceType: Bundlebody:
type: transaction
entry:
$let:
- {npi: '$ replace(msg.PV1.attending_doctor.0.authority.ns || msg.PV1.attending_doctor.0.authority.uid || msg.PV1.attending_doctor.0.id, "\D", "")'}
- {practitioner_id: $ msg.PV1.attending_doctor.0.id}
- {bed: $ msg.PV1.assigned_patient_location.bed}
- {room: $ msg.PV1.assigned_patient_location.room}
- {point_of_care: $ msg.PV1.assigned_patient_location.point_of_care}
- bed_display: {$if: $ bed, $then: $ "bed " + bed}
- room_display: {$if: $ room, $then: $ "room " + room}
- point_of_care_display: {$if: $ point_of_care, $then: $ "point of care " + point_of_care}
- loc_id: {$if: $ bed || room || point_of_care, $then: '$ str(abs(hash(replace(bed + room + point_of_care, "\s", ""))))'}
$body:
- request: {url: $ "/Practitioner/" + practitioner_id, method: PUT, IfNoneExist: $ "_id=" + practitioner_id}
resource:
id: $ practitioner_id
name:
- given: [$ msg.PV1.attending_doctor.0.given]
family: $ msg.PV1.attending_doctor.0.family.surname
- request: {url: $ "/Encounter", method: POST}
resource:
class: {code: IMP, system: 'http://terminology.hl7.org/CodeSystem/v3-ActCode', display: inpatient encounter}
status: planned
subject: {id: $ PATIENT_ID, resourceType: Patient}
participant:
- individual: {id: $ practitioner_id, resourceType: Practitioner}
resourceType: Bundle
id: pv1-segment
resourceType: Mapping
meta: {lastUpdated: '2023-03-27T13:56:56.298211Z', createdAt: '2023-03-27T13:56:56.298211Z', versionId: '359'}
Hl7v2Config resource
This resource is used to process incoming messages. The documentation on the Hl7v2Config can be found here
PUT /Hl7v2Config/demo-config
content-type: text/yaml
accept: text/yaml
mapping:
id: hl7v2-demo
resourceType: Mapping
isStrict: falsemapping: {id: hl7v2-demo, resourceType: Mapping}
isStrict: false
id: demo-config
resourceType: Hl7v2Config
meta: {lastUpdated: '2023-03-27T13:57:00.930880Z', createdAt: '2023-03-27T13:57:00.930880Z', versionId: '360'}
Apply mapping
To transform incoming messages and execute resulting bundles use POST /Hl7v2Message specifying an Hl7v2Config and providing HL7v2 message in the body. Documentation
Response contains original message, internal HL7v2 parsed representation, transformation result and transaction Bundle apply response
POST /Hl7v2Message
content-type: text/yaml
accept: text/yaml
config:
id: demo-config
resourceType: Hl7v2Config
src: |
MSH|^~\&||org-1|||20200101120000||ADT^A08|my-unique-message-id|P|2.9|||NE|AL
EVN|A08|20200101120000||Resident Diagnosis Update|1234^MyHealthCare^john.doe@myhealthcare.com^^^|20200101120000
PID|1|pt-1|org-1^^^^FI~pt-1^^^^PI||Doe^John^^^||19990101|M|||1 Lane^^Austin^TX^78000
PV1|1|I|1^1^A^ABC||||345^Adhoc^Test^^^||||||||||||1500|||||||||||||||||||||||||20200101120000
status: receivedmeta: {lastUpdated: '2023-03-27T13:57:04.826308Z', createdAt: '2023-03-27T13:57:04.826308Z', versionId: '362'}
config: {id: demo-config, resourceType: Hl7v2Config}
type: ADT
src: |
MSH|^~\&||org-1|||20200101120000||ADT^A08|my-unique-message-id|P|2.9|||NE|AL
EVN|A08|20200101120000||Resident Diagnosis Update|1234^MyHealthCare^john.doe@myhealthcare.com^^^|20200101120000
PID|1|pt-1|org-1^^^^FI~pt-1^^^^PI||Doe^John^^^||19990101|M|||1 Lane^^Austin^TX^78000
PV1|1|I|1^1^A^ABC||||345^Adhoc^Test^^^||||||||||||1500|||||||||||||||||||||||||20200101120000
outcome:
mapping:
type: transaction
entry:
- request: {url: /Patient/60178987, method: PUT}
resource:
id: '60178987'
gender: male
address:
- city: Austin
line: [1 Lane]
state: TX
postalCode: '78000'
birthDate: '1999-01-01'
identifier:
- type:
text: Facility ID
coding:
- {code: FI, system: 'http://terminology.hl7.org/CodeSystem/v2-0203', display: Facility ID}
system: https://my-emr.com/patient/fi
resourceType: Patient
managingOrganization: {id: org-1, resourceType: Organization}
- request: {url: /Practitioner/345, method: PUT, IfNoneExist: _id=345}
resource:
id: '345'
name:
- given: [Test]
family: Adhoc
- request: {url: /Encounter, method: POST}
resource:
class: {code: IMP, system: 'http://terminology.hl7.org/CodeSystem/v3-ActCode', display: inpatient encounter}
status: planned
subject: {id: '60178987', resourceType: Patient}
participant:
- individual: {id: '345', resourceType: Practitioner}
resourceType: Bundle
response:
body:
id: processing
text: {div: 'Transaction failed at entry[0]. Response status is 422. Response body is {"resourceType":"OperationOutcome","text":{"status":"generated","div":"Invalid resource"},"issue":[{"severity":"fatal","code":"invalid","expression":["Patient.managingOrganization"],"diagnostics":"Referenced resource Organization/org-1 does not exist"}]}.', status: generated}
issue:
- code: invalid
severity: fatal
expression: [Patient.managingOrganization]
diagnostics: Referenced resource Organization/org-1 does not exist
resourceType: OperationOutcome
status: 400
resourceType: Hl7v2Message
event: A08
status: error
id: 832957b4-2267-42d4-9f1e-e717d1c44286
parsed:
EVN:
operator_id:
- id: '1234'
given: john.doe@myhealthcare.com
family: {surname: MyHealthCare}
event_occurred: {time: '20200101120000'}
event_type_code: A08
event_reason_code: Resident Diagnosis Update
recorded_datetime: {time: '20200101120000'}
MSH:
encoding: ^~\&
facility: {ns: org-1}
proc_id: {proc_id: P}
ack_type: NE
type: {code: ADT, event: A08}
app_ack_type: AL
id: my-unique-message-id
datetime: {time: '20200101120000'}
version: {id: '2.9'}
separator: '|'
PID:
name:
- given: John
family: {surname: Doe}
gender: M
set_id: '1'
address:
- city: Austin
state: TX
street: {text: 1 Lane}
postal_code: '78000'
birth_date: {time: '19990101'}
patient_id: {value: pt-1}
identifiers:
- {value: org-1, system: FI}
- {value: pt-1, system: PI}
PV1:
set_id: '1'
visit_number: {value: '1500'}
patient_class: I
admit_datetime: {time: '20200101120000'}
attending_doctor:
- id: '345'
given: Test
family: {surname: Adhoc}
assigned_patient_location:
bed: A
room: '1'
facility: {ns: ABC}
point_of_care: '1'
We got success response from the transaction Bundle apply and now can check if resources are present in Aidbox:
GET /fhir/Patient/60178987
accept: text/yamladdress:
- city: Austin
line: [1 Lane]
state: TX
postalCode: '78000'
meta:
lastUpdated: '2022-03-15T17:42:23.160883Z'
versionId: '3762'
extension:
- {url: 'ex:createdAt', valueInstant: '2022-03-15T17:42:23.160883Z'}
managingOrganization: {reference: Organization/org-1}
birthDate: '1999-01-01'
resourceType: Patient
id: '60178987'
identifier:
- type:
text: Facility ID
coding:
- {code: FI, system: 'http://terminology.hl7.org/CodeSystem/v2-0203', display: Facility ID}
system: https://my-emr.com/patient/fi
gender: male