Skip to main content
Jonathan Andrei
Back to all posts
Feb. 2025 - Mar. 20256 min read

FHIR + CDS Hooks + Gemini: a Risk Score a Clinician Will Actually Read

Risk scores die when they're black boxes. MeldRx integration via FHIR/CDS Hooks pulls patient data; Gemini Flash returns structured JSON with explanations, confidence, and recommended actions; the UI hands clinicians a downloadable PDF report.

FHIRCDS HooksGeminiHealthcare

Risk-prediction widgets that bolt onto an EHR get ignored when they don't explain themselves. The MeldRx Hackathon was an excuse to design the explanation surface first and let the model fit into it.

The Predictive AI Healthcare Assistant dashboard showing a patient summary, risk score, and condition breakdown.
The clinician view after MeldRx hands off the patient context: risk score on the left, drivers and suggested actions on the right.

Why I built it

Before this hackathon I had never built a healthcare app, and I had not heard of CDS Hooks or FHIR. I wanted a forced excuse to learn that side of the industry on a real integration target rather than reading specs in the abstract. The other motivation was simpler: clinicians are under time pressure, and a tool that surfaces patient history faster, with its reasoning visible, is something I would want next to me if I were the one making the call.

This is not a replacement for a medical degree. It is a second pair of eyes that reads the chart faster than I ever could and writes down why it flagged what it flagged.

What it does

Inside the MeldRx EHR view, a button on the patient card opens the assistant. The handoff runs through a SMART-style consent flow so the user explicitly authorizes data access before anything is read. Once inside, the app pulls the patient's Condition and Observation resources over FHIR and sends them to Gemini Flash for analysis.

The output that comes back is opinionated: a risk score with a transparent explanation, recommended actions, preventive measures, and a short summary written for a clinician who has thirty seconds. The model also self-reports an accuracy estimate with the reasoning behind that estimate, and the whole analysis can be exported as a PDF that lives in the chart.

Accuracy score panel with model self-rated confidence and a written explanation.
The accuracy panel. The number is paired with the reasoning the model used to arrive at it.

Structured output is non-negotiable

Gemini Flash is asked for strict JSON with fields the UI relies on: risk_score, confidence, drivers, suggested_actions. Retry logic on malformed responses guarantees the contract. If the model returns prose, I re-ask. The downstream UI never has to defensive-parse.

Active vs resolved is the visualization that earned its space

Most condition lists treat everything as current. A clinician's brain doesn't. The dashboard splits active and resolved conditions visually, surfaces the most common diagnoses across the panel, and exports the consolidated summary as a downloadable PDF, the artifact a clinician actually saves to the chart.

Generated PDF export of the risk analysis, ready to drop into the chart.
The PDF export rendered with @react-pdf/renderer. Same content as the dashboard, formatted for the chart.

How I built it

Next.js with a mix of JavaScript and TypeScript on top, CDS Hooks and FHIR as the integration contract for pulling patient data out of MeldRx, React Redux for state, Recharts for the visualizations, Tailwind plus DaisyUI for styling, and @react-pdf/renderer for the chart-ready export. Gemini Flash sits behind an axios call wrapped in retry logic. Hosted on Vercel.

  • Gemini Flash was picked specifically for latency. Healthcare workflows do not tolerate a 12-second wait on a card click.
  • CDS Hooks is the contract for when the app is allowed to surface, FHIR is the contract for what it reads. Both are non-optional for being taken seriously inside an EHR.
  • Redux is heavier than the app strictly needs, but it makes the consent flow + analysis state easy to reason about across the cards.

What was hard

Prompt engineering the output until it landed in the exact JSON shape the UI expected took the most time. Beyond the prompt, the model occasionally returned malformed JSON during testing. The fix was a backoff retry: wait 500 ms on failure to avoid hammering the model, then try again up to 10 times. Since adding it I have not seen a single failure reach the UI.

The other hard part was just the learning curve. CDS Hooks, FHIR resource shapes, the SMART auth handoff, none of that was on my map before this hackathon. Most of the first day went into reading specs and the MeldRx sandbox docs before any code that mattered got written.

What I learned

  • In a clinical UI, the explanation is the feature. The score is the side effect.
  • Strict JSON contracts plus retry beat trying to clean up prose output every time.
  • FHIR is verbose but predictable. Once you accept the resource model, everything maps cleanly.

What's next

Accuracy. The highest self-reported number I have seen is 85%, and I want that higher by extracting more of the relevant patient signal and pushing further on prompt structure. After that, the bigger idea is turning the assistant into a queuing layer: have the model output a priority that gets posted back to the MeldRx card view so triage isn't strictly first-come-first-served. Patient text notifications about appointment progress fit naturally into the same loop.

Last item on the roadmap is moving off a hosted frontier model. Gemini was the right call for shipping in 48 hours, but a production version of this should run on a small language model or a fine-tuned in-house model, both for data residency and for predictable behavior on the JSON contract.

Related project

Predictive AI Healthcare Assistant (MeldRx Hackathon)

View the project