FDE PulseFDE jobs open 434New in the last 7 days 27
VI

The newspaper of the Forward Deployed Engineer

Guides

HL7 v2, FHIR and Epic: integrating AI with hospital data through two pipelines

In a hospital, data reaches you by two routes: HL7 v2 pushes events to you, while FHIR lets you ask for state. If you pick the wrong route early on, your agent will either act too late or read the wrong data.

In brief

  • HL7 v2 is an event stream. Each message is a set of segments, its type is set by the trigger in MSH, and at Epic every v2 interface goes through Bridges.
  • FHIR is how you ask about state. It consists of resources and APIs, and it targets REST with create, read, update, search, history and transaction.
  • An AI agent that talks to Epic service-to-service should use backend OAuth 2.0, and its parser must cope with missing optional segments.
ShareLinkedInFacebookX
GraphicThe two hospital data pipelines
HL7 v2FHIR
NatureEvent-driven messages made of segment linesResources and APIs that let two applications interact
How data arrivesPushed to you when an event occurs, e.g. ADT^A04You ask for it via create, read, search, history and so on
Identifying the data typeTrigger in the MSH segmentResource type and its matching endpoint
Route at EpicEvery v2 interface goes through BridgesFHIR API; background clients use backend OAuth 2.0
Common trapOptional segments may be missingResources follow the 80/20 rule and miss rare needs

Use HL7 v2 to know when to act, and FHIR to know what data to act on.

Graphic: FDE Times

In your first week at a hospital you get a task that sounds simple: “When a patient arrives, the agent summarises their record for the doctor before the doctor walks into the room.” You ask hospital IT where the data lives. One person says “take the HL7 feed”. Another says “call Epic’s FHIR API”. They are describing two completely different things.

Every AI project in healthcare reaches this fork early. Hospital data travels through two pipelines built on opposite ideas. The first pushes events to you as soon as something happens. The second lets you ask for the state of a patient at any time.

A good FDE does not need to know the specifications by heart. What matters is knowing which pipeline each step of the use case should draw from, and where that pipeline tends to break.

HL7 v2 is a stream of events, not a database

Epic itself calls HL7 v2 one of the most widely implemented healthcare data standards. An HL7 v2 message is a set of lines called segments. The message type depends on the event that triggered it. For example, registering a patient produces an ADT^A04 message.

The message type is identified by the trigger in the MSH segment, which is the first line of the message. ADT messages carry administrative information and the events of a visit. So to learn that a patient has just arrived, you do not ask anyone. You listen for ADT messages, read the trigger in MSH and react.

At Epic, every inbound and outbound HL7 v2 interface goes through Bridges, the core messaging infrastructure Epic uses as its interface engine. This includes acknowledgements for outgoing messages. In practice, “getting an HL7 feed” means the hospital’s interface team configures a flow on Bridges that pushes events to your system.

FHIR is for asking: how is this patient right now?

FHIR is built around two parts. Resources are information models that define data elements, constraints and the relationships between them. APIs are the interfaces two applications use to talk to each other. The specification is aimed at RESTful interfaces, although REST is not mandatory.

FHIR’s set of REST interactions covers create, read, update, search, history and transaction, and each maps onto a familiar HTTP method. To create a resource, for instance, you send an HTTP POST to the endpoint for that resource type, and the client does not need to supply an id. A backend developer will find this far more familiar than HL7 v2.

In the US, FHIR also carries legal weight. Rules from the ONC, the US regulator for health information technology, tie the standardised API criterion §170.315(g)(10) to FHIR. Certified software developers must therefore provide their customers with a FHIR-based API.

This is US law and does not automatically apply elsewhere. With a US client, though, it makes sense to ask about the FHIR API in your first meeting with the IT team.

Outside the US, developers are most likely to meet Epic, HL7 v2 and FHIR while working for US healthtech clients or in remote FDE roles, rather than at hospitals in their own country. That alone makes the skill worth learning if you are aiming at that market.

Worked example: an agent that summarises the record when a patient arrives

Go back to the original request and split it into two questions: when the agent runs, and what it reads. The first is a question about events. The second is about state.

Step one is to listen for the event. The interface team configures Bridges to push ADT messages to your listener. Here is an illustrative message, shortened and included only to show its shape:

MSH|^~\&|<sending app>|<sending facility>|<receiving app>|<receiving facility>|<timestamp>||ADT^A04^ADT_A01|...
PID|...|...|<patient number>|...

Your listener splits the message into segments by line and reads the message type field (MSH-9) from its correct position. It handles only ADT^A04 and ignores every other message type. Match on the prefix, because the real value is often longer, as in ADT^A04^ADT_A01. The listener then takes the patient number from the identification segment and puts a job on the queue.

def handle(raw: str):
    segments = {}
    for line in raw.strip().splitlines():
        name = line[:3]
        segments.setdefault(name, []).append(line.split("|"))
    msh = segments.get("MSH")
    if not msh:
        log_missing("MSH", raw)
        return
    fields = msh[0]
    # MSH-1 is the "|" character itself, so after split, MSH-9 sits at index 8
    msg_type = fields[8] if len(fields) > 8 else ""
    if not msg_type.startswith("ADT^A04"):
        return  # not an event we need to handle
    pid = segments.get("PID")
    if not pid:
        log_missing("PID", raw)  # missing segment: log it, don't crash
        return
    enqueue_summary(pid[0])

Step two is to ask for state. A worker picks up the job and calls Epic’s FHIR API with search and read to pull the resources the agent needs. The request sequence below is illustrative and only shows the shape. Take the actual parameter names and identifier systems from Epic’s documentation and from the hospital’s IT team:

# 1. search: find the Patient by the patient number taken from PID
GET [base]/Patient?identifier=<identifier system>|<patient number>
Authorization: Bearer <access token>

# 2. read: fetch the exact Patient resource by the id the server returned
GET [base]/Patient/<patient id>

# 3. search: that patient's encounters and conditions
GET [base]/Encounter?patient=<patient id>
GET [base]/Condition?patient=<patient id>

The order is what matters. The number in PID is the hospital’s identifier, not a resource id. You search first to turn it into a patient id, and only then read and search the related resources. Search results usually come back as a list. The worker collects the Encounter and Condition resources and filters out what it needs before passing anything to the summarisation model.

This is a service-to-service flow, and no doctor logs in at any point. Epic recommends backend OAuth 2.0 for exactly this kind of querying client, so your service authenticates itself and obtains its own token before sending the requests above.

Step three, if the hospital allows it, is to write the result back. You POST a new resource to the matching endpoint and let the server generate the id. On a first project, though, it is usually wiser to stay read-only and show the summary in a separate interface.

Steps to follow at a client

First, write the use case out as a timeline and label each step as either “need to know when” or “need to know what”. Steps of the first kind go through HL7 v2. Steps of the second kind go through FHIR. Take this diagram to the interface team in your first week.

Next, request two things at once, because each follows its own process: an ADT flow on Bridges to the test environment, and backend OAuth access for FHIR. Then ask for a few real, de-identified messages from the test environment to check your parser. Do not rely only on the examples in the documentation.

Finally, write the data checks before you write the agent’s prompt. Record which segments each source sends and which it leaves out. That table will be the most useful document you have at handover.

The mistakes that derail projects

The most common mistake is to assume every message contains every segment. HL7 v2 has required segments and optional ones, so data from different sources is uneven. A parser written against one tidy sample message will break on its first day in production.

The second mistake is to expect FHIR to contain everything. FHIR resources are designed on an 80/20 rule and cover the common needs. If your use case depends on a rare detail, check early whether the standard resource includes it, rather than finding out during a demo.

The third mistake is using the wrong kind of authentication. A background agent that borrows a user’s login session will be stopped at the security review. The last mistake is using FHIR to poll instead of listening for events. Calling search over and over to work out whether a patient has arrived wastes resources and adds delay, when ADT messages are already there to tell you.

Turning this skill into an edge when applying

When you read job descriptions for FDE roles in healthtech, look for the keywords HL7 v2, FHIR, Epic, interface engine and OAuth. They tell you the role will involve both pipelines.

On your CV, do not just write “knows FHIR”. Describe a complete flow, such as “ADT^A04 listener triggers a worker that calls FHIR via backend OAuth, with per-source statistics on missing segments”.

A small personal project is enough to show this: an HL7 v2 parser that copes with missing segments, and a FHIR client that can search, read and then POST. Be ready to explain which pipeline you chose for each step and why. It is also the first question you will have to answer once you are working inside a hospital.

7 sources
Read next on the roadmap · Stage 2: Broad engineeringHands-on: letting an agent work a no-API internal web app with Playwright MCPFive steps to have an agent use a preloaded login session to read and fill forms on an admin page with no API, while every write still needs a human's approval.