Status: Satellite draft for Healthcare RAG hub. Distinct from the FHIR vs HL7 standards-comparison post — this one is about *integrating LLMs with FHIR data*, not about choosing the standard. Body to be drafted; outline locked. Target length: 1,500 words. Schema: Article + FAQPage.
If you're building clinical document AI and your data source is an EHR, you'll touch FHIR. The question is not whether — it's how to make FHIR resources useful as both retrieval input and structured output for a RAG system. This post covers the patterns we ship in production.
Why FHIR + LLM is harder than people expect
Three reasons:
- FHIR resources are not natural-language documents. They're typed, nested JSON. An LLM trained on text doesn't index them well as embeddings without preprocessing.
- Real-world FHIR has profile drift. Epic's Patient is not Cerner's Patient. The optional fields are populated differently.
- The output direction is harder than the input direction. Going *from* FHIR to a summary is easy. Going *from* a summary back *into* a FHIR resource (so it can update the EHR) requires structured generation with strict schema validation.
The integration topology
Diagram (writer to add): EHR FHIR API → SMART on FHIR auth layer → FHIR caching/normalization layer → embedding layer (with FHIR-aware preprocessing) → vector store → LLM → structured-output layer → FHIR PUT/POST back to EHR.
Each arrow is a real engineering problem. We'll walk through them.
Pattern 1: FHIR resources as retrieval source
The naive approach: stringify the FHIR JSON, embed it, retrieve. This works badly because:
- Field names dominate the embedding. "code", "system", "valueQuantity" appear in every Observation.
- Nested references (Patient → Encounter → Condition) are lost in flat embedding.
- Coded values (LOINC, SNOMED, ICD-10) are codes, not natural language.
The pattern that works: render FHIR resources as natural-language summaries before embedding. Writer to render a code sample:
// Pseudocode placeholder — writer to fill in real implementation
function renderObservationForEmbedding(obs: Observation): string {
// "On 2026-03-14, patient's hemoglobin A1C was 7.2 (high). Reported by Dr. Chen at Tampa General."
return `On ${formatDate(obs.effectiveDateTime)}, patient's ${codeToName(obs.code)} ` +
`was ${formatValue(obs.valueQuantity)} ${interpretReferenceRange(obs)}. ` +
`Reported by ${obs.performer?.display} at ${obs.performer?.organization?.display}.`
}The natural-language rendering preserves clinical meaning, normalizes coded values, and embeds well. Keep both: the rendered string for embedding, and a pointer back to the original FHIR resource for retrieval-time hydration.
Pattern 2: FHIR resources as structured output
When the LLM needs to *write* back to the EHR (e.g., creating a DocumentReference for a generated discharge summary), use structured generation with schema enforcement:
- Constrained generation against the FHIR JSON schema (use
response_format: json_schemawith Bedrock/Anthropic, or Tool Use with a strict schema). - Required fields enforced at generation time, not at validation time.
- US Core profile compliance verified post-generation with
fhir-validatorbefore any PUT to the EHR.
Writer to render a code sample for generating a DocumentReference resource with all required fields, including subject, author, content.attachment.url, and context.encounter.
Pattern 3: SMART on FHIR auth — the gotchas
- Token storage: server-side only, never in localStorage. Refresh token rotation enforced.
- Scope discipline:
patient/Observation.readnotpatient/*.read. The audit log is what makes this enforceable. - Launch context: the EHR-launched flow injects
patientandencounterIDs into the token; build the assistant assuming they're authoritative.
Pattern 4: Profile drift across EHRs
Epic FHIR ≠ Cerner FHIR ≠ athenahealth FHIR. The patterns we ship:
- A FHIR adapter layer that normalizes incoming resources to a canonical internal shape before the LLM sees them.
- Per-EHR test fixtures captured from the sandbox (open.epic.com, code.cerner.com).
- Profile-specific prompts when the LLM needs to *generate* output that will be consumed by a specific EHR.
Internal link to: /blog/fhir-vs-hl7 for the underlying standard comparison.
Pattern 5: When NOT to use FHIR
Sometimes the right answer is: don't put FHIR on the LLM's critical path.
- For free-text clinical notes (rich text in
DocumentReference.content), retrieve via FHIR but treat the content as text, not as FHIR. - For high-volume HL7 v2 message processing, the integration engine (Mirth, Rhapsody) is still the right place — write to FHIR after enrichment, not before.
- For workflows where the EHR is read-only to the AI system, skip the SMART on FHIR auth complexity and use a service-account read-only flow with logging.
How this lands in practice
A representative prior-authorization workflow: the system pulls Patient, Coverage, MedicationRequest, and recent Observation resources as patient context. Those resources are rendered as natural-language clinical summaries. A separate retrieval pass fetches payer policy text from a vector index. The LLM generates a draft authorization rationale grounded in both. A pharmacist reviews and approves before anything is submitted. The audit log records FHIR resource references — not the resource values — to keep PHI out of unnecessary log copies.
The same FHIR-as-context pattern applies to any retrieval-grounded legal or document AI workflow where structured records need to feed an LLM without raw JSON verbosity. See the legal deposition RAG case study for a close parallel in a different domain.
FAQ
- Can we just give the LLM the raw FHIR JSON?
- Which FHIR version should we target — R4 or R5?
- How do we handle FHIR resources that span millions of records?
- What's the latency impact of FHIR adapter layers?
- How do we keep retrieved FHIR data in sync with the source EHR?
- Can we cache FHIR data in a HIPAA-aligned way?
Where we fit
CTA — Cal.com architecture review, link to HIPAA-aligned RAG cornerstone, link to healthcare AI consulting.
Evaluating or building a document-analysis system for legal, healthcare, or financial workflows? A Clarity Assessment is a structured way to surface the decisions that will be expensive to change later — before they’re made. Our method starts with the problem, not the model.