---
{
  "title": "Using FHIR as a framework for agentic coding",
  "description": "Why FHIR-native projects are uniquely suited for AI-assisted development — and what we learned building a real Personal Health Record twice with Claude Code.",
  "date": "2026-08-04",
  "author": "Aleksandr Kislitsyn",
  "reading-time": "8 min read",
  "tags": ["AI / Agents", "FHIR Standard", "Aidbox", "Health Samurai Lab"],
  "tldr": "AI coding agents write plausible code fast, but without a strong framework they drift, hallucinate data shapes, and can't verify their own work — a real problem in healthcare. FHIR turns out to be an unusually good set of rails: a fixed data model of 150+ resources, server-side validation on every write, built-in terminology, and generated types give the agent structure to lean on and a tight feedback loop to self-correct. We rebuilt our internal PHR twice with Claude Code — from scratch, then on FHIR — and the FHIR version was smaller, more coherent, and the one worth keeping.",
  "utm-campaign": "ai",
  "utm-content": "fhir-agentic-coding"
}
---

> For the complete documentation index, see [llms.txt](https://www.health-samurai.io/llms.txt).
> Use it to discover all available pages before guessing URLs.

---

LLMs write plausible code, and they write it fast. But anyone who has driven a coding agent through more than a toy project knows the failure mode: give it enough rope and it drifts. Field names shift between sessions, the data model mutates, yesterday's conventions get quietly reinvented. For a landing page, that's an annoyance. For healthcare software, "winging it" is not an option.

We spent some time at Health Samurai's Lab testing a hypothesis: **FHIR is not just a data standard — it's an unusually good framework for AI-assisted development.** This post walks through why, and what happened when we built the same product twice to find out.

## Agentic coding needs rails

Point a capable agent at a generic stack and four problems show up again and again:

- **Agents drift.** Without a fixed model, every session invents slightly different field names, shapes, and conventions. Changes stop composing — each new feature fights the last one.
- **Hallucinated data shapes.** Ask an agent to "add allergies" and it will cheerfully invent a schema. Nothing rejects an invalid structure until it reaches production and someone notices the data is wrong.
- **No self-validation.** A generic stack gives the agent no way to check its own output beyond "does it compile?" — which is not the same question as "is this correct healthcare data?"
- **Generic frameworks don't know healthcare.** React, Rails, and Django know nothing about `Patient`, `Encounter`, or `Observation`. Every project reinvents the same domain modeling from scratch, and the agent reinvents it a little differently each time.

The common thread is the absence of *rails* — a strong, opinionated framework that constrains what the agent can produce and tells it immediately when it's wrong. The better the rails, the less room the agent has to drift, and the faster it can course-correct.

## FHIR as a framework

Here's the insight: FHIR already provides almost all of those rails, and it provides them for healthcare specifically. A fixed data model answers the drift; server-side validation answers the hallucinated shapes and gives the agent a way to check itself; and the whole thing is healthcare-native by construction, so nothing has to be re-modelled per project.

A FHIR-native stack has three parts working together — the FHIR server is the backend, the app is built on FHIR SDKs, and the developer works alongside an AI copilot that already speaks FHIR. Most of that stack is *ready* before you write a line of application code; the only part you actually build is the app that wires the FHIR pieces together.

<div class="my-8 not-prose overflow-x-auto">
  <div class="flex items-stretch gap-2.5 min-w-[600px]">
    <!-- Developer -->
    <div class="flex-1 flex flex-col gap-2.5 p-4 bg-bg-secondary rounded-2xl">
      <div class="flex items-center gap-2 text-text">
        <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><circle cx="12" cy="8" r="3.5"/><path d="M5 20c0-3.5 3-6 7-6s7 2.5 7 6" stroke-linecap="round"/></svg>
        <span class="typo-body16 font-semibold leading-none">Developer</span>
      </div>
      <div class="flex flex-wrap gap-1.5 mt-0.5">
        <span class="px-2.5 py-1 rounded-lg bg-bg-primary border border-border-secondary text-[13px] text-text whitespace-nowrap">AI Copilot</span>
        <span class="inline-flex items-center gap-1 px-2.5 py-1 rounded-lg bg-bg-primary border border-border-secondary text-[13px] text-text whitespace-nowrap">Agent Skills <span class="inline-flex w-3.5 h-3.5 rounded-full bg-fg-success-primary text-white items-center justify-center"><svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3.5" aria-hidden="true"><path d="M4 12l5 5L20 6" stroke-linecap="round" stroke-linejoin="round"/></svg></span></span>
      </div>
    </div>
    <!-- builds -->
    <div class="flex flex-col items-center justify-center text-primary shrink-0">
      <span class="typo-mono text-[9px] uppercase tracking-widest mb-1 whitespace-nowrap">builds</span>
      <svg width="24" height="12" viewBox="0 0 26 14" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path d="M1 7h22" stroke-linecap="round"/><path d="m18 2 5 5-5 5" stroke-linecap="round" stroke-linejoin="round"/></svg>
    </div>
    <!-- PHR App -->
    <div class="flex-1 flex flex-col gap-2.5 p-4 bg-bg-secondary rounded-2xl border border-primary/40">
      <div class="flex items-center gap-2 flex-wrap">
        <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" class="text-text" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z"/></svg>
        <span class="typo-body16 font-semibold leading-none text-text">PHR App</span>
        <span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-primary text-white typo-mono text-[9px] uppercase tracking-wider whitespace-nowrap"><svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M4 20h4L18.5 9.5a2.1 2.1 0 0 0-3-3L5 17v3z" stroke-linejoin="round"/></svg> You build this</span>
      </div>
      <div class="flex flex-col gap-1">
        <p class="typo-mono text-text-light text-[10px] uppercase tracking-wider flex items-center gap-1">FHIR SDK <span class="inline-flex w-3 h-3 rounded-full bg-fg-success-primary text-white items-center justify-center"><svg width="8" height="8" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3.5" aria-hidden="true"><path d="M4 12l5 5L20 6" stroke-linecap="round" stroke-linejoin="round"/></svg></span></p>
        <div class="flex flex-wrap gap-1.5">
          <span class="px-2.5 py-1 rounded-lg bg-bg-primary border border-border-secondary text-[13px] text-text whitespace-nowrap">FHIR Types</span>
          <span class="px-2.5 py-1 rounded-lg bg-bg-primary border border-border-secondary text-[13px] text-text whitespace-nowrap">FHIR Client</span>
        </div>
      </div>
      <div class="flex flex-col gap-1">
        <p class="typo-mono text-text-light text-[10px] uppercase tracking-wider flex items-center gap-1">UI <span class="inline-flex w-3 h-3 rounded-full bg-fg-success-primary text-white items-center justify-center"><svg width="8" height="8" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3.5" aria-hidden="true"><path d="M4 12l5 5L20 6" stroke-linecap="round" stroke-linejoin="round"/></svg></span></p>
        <div class="flex flex-wrap gap-1.5">
          <span class="px-2.5 py-1 rounded-lg bg-bg-primary border border-border-secondary text-[13px] text-text whitespace-nowrap">React Components</span>
        </div>
      </div>
    </div>
    <!-- FHIR REST -->
    <div class="flex flex-col items-center justify-center text-primary shrink-0">
      <span class="typo-mono text-[9px] uppercase tracking-widest mb-1 whitespace-nowrap">FHIR REST</span>
      <svg width="24" height="12" viewBox="0 0 26 14" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path d="M1 7h22" stroke-linecap="round"/><path d="m18 2 5 5-5 5" stroke-linecap="round" stroke-linejoin="round"/></svg>
    </div>
    <!-- FHIR Server -->
    <div class="flex-[1.35] flex flex-col gap-2.5 p-4 bg-bg-secondary rounded-2xl border border-border-success">
      <div class="flex items-center gap-2 flex-wrap">
        <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" class="text-text" aria-hidden="true"><rect x="3" y="4" width="18" height="7" rx="1.5"/><rect x="3" y="13" width="18" height="7" rx="1.5"/><path d="M7 7.5h.01M7 16.5h.01" stroke-linecap="round"/></svg>
        <span class="typo-body16 font-semibold leading-none text-text">FHIR Server</span>
        <span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-fg-success-primary text-white typo-mono text-[9px] uppercase tracking-wider whitespace-nowrap"><svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" aria-hidden="true"><path d="M4 12l5 5L20 6" stroke-linecap="round" stroke-linejoin="round"/></svg> Ready</span>
      </div>
      <div class="flex flex-wrap gap-1.5">
        <span class="px-2.5 py-1 rounded-lg bg-bg-primary border border-border-secondary text-[13px] text-text whitespace-nowrap">Data Model</span>
        <span class="px-2.5 py-1 rounded-lg bg-bg-primary border border-border-secondary text-[13px] text-text whitespace-nowrap">Validation</span>
        <span class="px-2.5 py-1 rounded-lg bg-bg-primary border border-border-secondary text-[13px] text-text whitespace-nowrap">Terminology</span>
        <span class="px-2.5 py-1 rounded-lg bg-bg-primary border border-border-secondary text-[13px] text-text whitespace-nowrap">SDC</span>
        <span class="px-2.5 py-1 rounded-lg bg-bg-primary border border-border-secondary text-[13px] text-text whitespace-nowrap">SQL on FHIR</span>
        <span class="px-2.5 py-1 rounded-lg bg-bg-primary border border-border-secondary text-[13px] text-text whitespace-nowrap">Subscriptions</span>
        <span class="px-2.5 py-1 rounded-lg bg-bg-primary border border-border-secondary text-[13px] text-text whitespace-nowrap">Access Control</span>
      </div>
    </div>
  </div>
</div>

<div class="not-prose flex flex-wrap items-center gap-x-6 gap-y-1.5 mb-8 text-[13px] text-text-light">
  <span class="flex items-center gap-2"><span class="inline-flex w-2.5 h-2.5 rounded-full bg-dot-success"></span><strong class="text-text">Ready</strong> — FHIR spec, server, types, SDKs, AI copilots, agent skills</span>
  <span class="flex items-center gap-2"><span class="inline-flex w-2.5 h-2.5 rounded-full bg-primary"></span><strong class="text-text">You build</strong> — the app itself, wiring FHIR pieces together</span>
</div>

Let's look at what each layer gives the agent.

### A data model you inherit, not design

FHIR R4 defines 150+ healthcare resources — `Patient`, `Condition`, `Observation`, `Encounter`, `MedicationStatement`, `DocumentReference`, and on down the list. Stored natively by a FHIR server like [Aidbox](https://www.health-samurai.io/aidbox), they need no schema design at all.

<figure class="markdown-figure narrow">
  <img src="image-1.png" alt="FHIR R4 resource index — every resource grouped by category" loading="eager" decoding="async" />
  <figcaption>The FHIR R4 resource list: two decades of healthcare domain modeling you inherit for free.</figcaption>
</figure>

This matters more than it first appears. When the agent needs to store allergies, it doesn't invent a table — it reaches for `AllergyIntolerance`, which already has the right fields, cardinalities, and bindings. Custom domain concepts become custom FHIR resources — in our own app, a `PhrUser` — with their own `StructureDefinition`, not a bespoke table with agent-invented columns. You inherit two decades of healthcare domain modeling, and so does the agent.

### Validation as a feedback loop

Every write to a FHIR server is validated against the resource's `StructureDefinition`: cardinality, types, bindings, invariants. Invalid data never enters the database.

For a human developer that's a safety net. For an agent, it's something more valuable — a **tight, precise feedback loop**. Instead of a 500 error three layers deep, the agent gets back something like:

```
OperationOutcome: Patient.gender must be a code from
http://hl7.org/fhir/administrative-gender (male | female | other | unknown)
```

That's exactly the kind of signal an agent self-corrects on. Profile-based validation lets you tighten the rules for your own project — required fields, fixed coding systems — without writing a single line of validation code. The agent writes less, and the server tells it immediately when it's wrong.

### Terminology, forms, and analytics — already solved

A FHIR server ships a lot of hard problems pre-solved, and each one is a problem the agent would otherwise try to hand-roll:

- **Terminology.** `ValueSet`, `CodeSystem`, and operations like `$expand`, `$validate-code`, and `$lookup` are built in. LOINC, SNOMED, RxNorm, ICD — all reachable over standard FHIR endpoints. Your app doesn't *ship* a coding system; it queries one (for example, [Termbox](https://www.health-samurai.io/termbox), a dedicated FHIR terminology server). Terminology stops being a "TODO: find a library" ticket.
- **SQL on FHIR.** Define a `ViewDefinition` that flattens FHIR resources into tabular columns, then query the view with plain SQL. Analytics teams get SQL for reporting; the data stays in FHIR — no transformation pipeline to write. It also lowers the bar for in-app dashboards: an app developer who would struggle to aggregate over nested FHIR resources can write a flat `GROUP BY` against a view, so a chart in the product and a report for the analysts read from the same definition. And a `ViewDefinition` is itself a FHIR resource, which means the agent can write one — declaring columns is a much narrower task than hand-rolling traversal logic over nested arrays.
- **Structured Data Capture (SDC).** FHIR `Questionnaire` resources describe forms; users fill them in and answers are stored as `QuestionnaireResponse` — linked, validated, and searchable like any other FHIR data. SDC implementations bring form builders, renderers, extraction logic, and galleries of ready-made forms.

<figure class="markdown-figure narrow">
  <img src="image-2.png" alt="Aidbox Form Builder — designing and testing a FHIR-native form" loading="lazy" decoding="async" />
  <figcaption>SDC in practice: FHIR-native forms built and tested in Aidbox Form Builder.</figcaption>
</figure>

### Types and skills for the agent

The layers above constrain the *data*. Two more close the loop around the *agent*.

- **Typed SDKs.** Because every resource has a machine-readable `StructureDefinition`, client types can be generated rather than written — for TypeScript, Python, C#, Java and others. The payoff for an agent is bigger than autocomplete: a misspelled field or a wrong enum fails at the type level, before any request is sent, and one generated source of truth is shared by server and client so both sides of a feature cannot drift apart. Publicly available generators make this a build step, not a project.
- **Agent skills.** The newer layer is documentation written for agents instead of humans. Because FHIR is a public standard with public server APIs, this knowledge is reusable across projects — how to shape a search query, how access policies work, when to reach for SQL on FHIR — rather than something each team has to re-teach. Pointing an agent at current documentation also keeps it from leaning on whatever a model absorbed at training time, which for a spec that ships new versions is a real source of confidently wrong code.

Both are things you configure once and the agent then benefits from on every task.

## The experiment: same app, different foundation

Theory is cheap, so we tested it. Our proving ground was Health Samurai's internal **Personal Health Record (PHR)** — a real product for our own team and the families they care for. Its scope is genuinely non-trivial: managing records for yourself and your dependents, entering conditions/medications/allergies/procedures, uploading medical PDFs, chat-based consultations with doctors, a patient summary, and AI assistance grounded in the patient's own FHIR data.

We built it **twice with Claude Code — same scope, different foundation.** The first time we let the agent build from scratch; the second time we rebuilt the whole app with FHIR as the framework.

| | **v1 — from scratch** | **v2 — on FHIR** |
|---|---|---|
| Stack | Plain React + Node + Postgres | Aidbox backend, generated FHIR types, open-source Aidbox UI components, agent skills |
| Data model | Agent-invented, per session | FHIR R4, fixed |
| Validation | Whatever the agent wrote | Server-side, on every write |
| New feature | Re-teach the drifted conventions | Pick a resource, generate types, wire the UI |
| Result | Kept drifting | Smaller, coherent — the one worth keeping |

In **v1**, the agent invented its own data model, its own API shape, its own validation rules. Every new feature meant re-teaching the agent the conventions it had already drifted away from. In **v2**, rebuilding the same app on FHIR produced a smaller, more coherent codebase — the agent leaned on FHIR instead of reinventing around it.

In practice that shows up in ordinary code. Generated types plus a set of [open-source Claude Code skills](https://github.com/HealthSamurai/phr/tree/main/.claude/skills) for working against Aidbox meant the agent wrote a patient create like this, with no schema of its own to design:

```typescript
// The agent writes against generated types — the shape is not up for negotiation
const patient = await aidbox.create<Patient>({
  resourceType: "Patient",
  name: [{ given: [body.givenName], family: body.familyName }],
  birthDate,
  gender: body.gender || undefined,
  active: true,
});
// ...and the server validates it on write.
```

Unremarkable, which is the point: there was no decision to make about field names or storage, and nothing for the agent to drift away from next session.

Three takeaways stood out:

1. **Less code.** The framework handles the boring parts — schema, API, validation — so the agent simply writes fewer of them.
2. **A tighter feedback loop.** The server rejects invalid writes with precise errors, and the agent self-corrects — no mysterious 500 three layers deep.
3. **Features become conversations, not projects.** Adding a clinical concept is one step — pick a FHIR resource, generate types, wire the UI — not five.

The clearest example: we asked for a **patient summary** feature. On a generic stack that means designing a summary schema, deciding how to reference the underlying records, and building an API around it. On FHIR, the agent reached for the [`Composition`](https://build.fhir.org/composition.html) resource — the standard's own model for a structured, sectioned clinical document — and implemented it cleanly: sections referencing the patient's existing `Condition`, `MedicationStatement`, and `AllergyIntolerance` resources, no bespoke schema invented. A feature that would have been a small project became a single conversation, because FHIR had already modeled it.

## How to try it yourself

If you want to give your agent the same rails, the starting recipe is short:

- **Pick a FHIR server** as your backend (for example, [Aidbox](https://www.health-samurai.io/aidbox)).
- **Generate FHIR types** with [`@atomic-ehr/codegen`](https://github.com/atomic-ehr/codegen) so the agent codes against real shapes.
- **Install the [Claude Code skills](https://github.com/HealthSamurai/phr/tree/main/.claude/skills)** so the agent speaks FHIR out of the box.

The PHR source is open too, if you'd like to see a full example: [github.com/HealthSamurai/phr](https://github.com/HealthSamurai/phr).

## What's next: taking the programmer out of the loop?

Here's the question the experiment opened up. If FHIR gives an agent enough structure to build a real app — what if the agent's user isn't a developer at all?

Imagine a platform where **doctors build their own apps**, iterating with an AI assistant in plain language, and those apps plug straight into the healthcare organization's existing infrastructure:

- **Doctors as builders** — no dev team in the middle; describe the workflow, get a working app.
- **Iterate with AI** — change a form, adjust a rule, add a summary. A conversation, not a ticket.
- **Fits the hospital's stack** — talks FHIR to the EHR, respects existing auth, audit, and policies. Not a silo, but a citizen of the infrastructure.

It's the same bet as this whole experiment, one level up: **FHIR as the foundation that lets AI build healthcare software for the people who actually need it.**

---

*Want to talk through applying this on your own stack? Reach out to [Aleksandr Kislitsyn](https://www.linkedin.com/in/aleksandrkislitsyn/), or explore the open-source [PHR](https://github.com/HealthSamurai/phr) repository and its [Claude Code skills](https://github.com/HealthSamurai/phr/tree/main/.claude/skills).*
