---
{
  "title": "Usar FHIR como marco de trabajo para la programación agéntica",
  "description": "Por qué los proyectos nativos de FHIR son especialmente adecuados para el desarrollo asistido por IA — y qué aprendimos al construir dos veces un Historial Clínico Personal real con Claude Code.",
  "date": "2026-08-04",
  "author": "Aleksandr Kislitsyn",
  "reading-time": "8 min read",
  "tags": ["AI / Agents", "FHIR Standard", "Aidbox", "Health Samurai Lab"],
  "tldr": "Los agentes de programación con IA escriben código plausible con rapidez, pero sin un marco sólido se desvían, alucinan estructuras de datos y no pueden verificar su propio trabajo — un problema real en el sector sanitario. FHIR resulta ser un conjunto de «raíles» excepcionalmente bueno: un modelo de datos fijo de más de 150 recursos, validación en el servidor en cada escritura, terminología integrada y tipos generados que ofrecen al agente una estructura en la que apoyarse y un ciclo de retroalimentación ajustado para autocorregirse. Reconstruimos nuestro PHR interno dos veces con Claude Code — desde cero y luego sobre FHIR — y la versión FHIR fue más pequeña, más coherente y la que mereció la pena conservar.",
  "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.

---

Los LLM escriben código plausible, y lo hacen rápido. Pero cualquiera que haya guiado un agente de programación en un proyecto más allá de lo trivial conoce el fallo típico: darle demasiada libertad y se desvía. Los nombres de los campos cambian entre sesiones, el modelo de datos muta, las convenciones de ayer se reinventan silenciosamente. En una página de aterrizaje eso es una molestia. En software sanitario, «improvisar» no es una opción.

Dedicamos tiempo en el laboratorio de Health Samurai a contrastar una hipótesis: **FHIR no es solo un estándar de datos — es un marco de trabajo excepcionalmente bueno para el desarrollo asistido por IA.** Esta entrada explica el porqué y qué ocurrió cuando construimos el mismo producto dos veces para comprobarlo.

## La programación agéntica necesita raíles

Oriente un agente capaz hacia una pila genérica y aparecen una y otra vez cuatro problemas:

- **Los agentes se desvían.** Sin un modelo fijo, cada sesión inventa nombres de campos, estructuras y convenciones ligeramente distintos. Los cambios dejan de componerse — cada nueva funcionalidad choca con la anterior.
- **Estructuras de datos alucinadas.** Pida al agente que «añada alergias» y creará alegremente un esquema propio. Nada rechaza una estructura no válida hasta que llega a producción y alguien detecta que los datos son incorrectos.
- **Sin autovalidación.** Una pila genérica no le ofrece al agente ninguna forma de comprobar su propia salida más allá de «¿compila?» — que no es lo mismo que «¿son datos sanitarios correctos?»
- **Los marcos genéricos no conocen el sector sanitario.** React, Rails y Django no saben nada sobre `Patient`, `Encounter` u `Observation`. Cada proyecto reinventa el mismo modelado de dominio desde cero, y el agente lo reinventa de forma ligeramente diferente cada vez.

El hilo común es la ausencia de *raíles* — un marco firme y con criterio propio que restrinja lo que el agente puede producir y le indique de inmediato cuándo se equivoca. Cuanto mejores son los raíles, menos margen tiene el agente para desviarse y más rápido puede corregir el rumbo.

## FHIR como marco de trabajo

La clave está aquí: FHIR ya proporciona casi todos esos raíles, y los proporciona específicamente para el sector sanitario. Un modelo de datos fijo resuelve la deriva; la validación en el servidor resuelve las estructuras alucinadas y ofrece al agente un mecanismo para autocomprobarse; y todo el sistema es nativo del dominio sanitario por construcción, de modo que nada tiene que remodelarse en cada proyecto.

Una pila nativa de FHIR tiene tres capas que trabajan juntas — el servidor FHIR actúa como backend, la aplicación se construye sobre los SDK de FHIR, y el desarrollador trabaja junto a un copiloto de IA que ya habla FHIR. La mayor parte de esa pila está *lista* antes de escribir una sola línea de código de aplicación; la única parte que realmente hay que construir es la app que conecta las piezas FHIR entre sí.

<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">Desarrollador</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">construye</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">Aplicación PHR</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> Usted construye esto</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">Componentes React</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">Servidor FHIR</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> Listo</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">Modelo de datos</span>
        <span class="px-2.5 py-1 rounded-lg bg-bg-primary border border-border-secondary text-[13px] text-text whitespace-nowrap">Validación</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">Suscripciones</span>
        <span class="px-2.5 py-1 rounded-lg bg-bg-primary border border-border-secondary text-[13px] text-text whitespace-nowrap">Control de acceso</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">Listo</strong> — especificación FHIR, servidor, tipos, SDK, copilotos de IA, habilidades de agente</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">Usted construye</strong> — la propia aplicación, conectando las piezas FHIR entre sí</span>
</div>

Veamos qué aporta cada capa al agente.

### Un modelo de datos que se hereda, no se diseña

FHIR R4 define más de 150 recursos sanitarios — `Patient`, `Condition`, `Observation`, `Encounter`, `MedicationStatement`, `DocumentReference`, y así sucesivamente. Almacenados de forma nativa por un servidor FHIR como [Aidbox](https://www.health-samurai.io/aidbox), no requieren ningún diseño de esquema.

<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>La lista de recursos de FHIR R4: dos décadas de modelado del dominio sanitario que se heredan gratuitamente.</figcaption>
</figure>

Esto importa más de lo que parece a primera vista. Cuando el agente necesita almacenar alergias, no inventa una tabla — recurre a `AllergyIntolerance`, que ya tiene los campos correctos, las cardinalidades y los enlaces. Los conceptos de dominio personalizados se convierten en recursos FHIR personalizados — en nuestra propia aplicación, un `PhrUser` — con su propio `StructureDefinition`, no en una tabla ad hoc con columnas inventadas por el agente. Se heredan dos décadas de modelado del dominio sanitario, y el agente también.

### La validación como ciclo de retroalimentación

Cada escritura en un servidor FHIR se valida contra el `StructureDefinition` del recurso: cardinalidad, tipos, enlaces, invariantes. Los datos no válidos nunca entran en la base de datos.

Para un desarrollador humano eso es una red de seguridad. Para un agente, es algo más valioso — un **ciclo de retroalimentación ajustado y preciso**. En lugar de un error 500 enterrado a tres capas de profundidad, el agente recibe algo como:

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

Esa es exactamente la clase de señal sobre la que un agente se autocorrige. La validación basada en perfiles permite ajustar las reglas para el propio proyecto — campos obligatorios, sistemas de codificación fijos — sin escribir ni una sola línea de código de validación. El agente escribe menos, y el servidor le indica de inmediato cuándo se equivoca.

### Terminología, formularios y analítica — ya resueltos

Un servidor FHIR trae muchos problemas difíciles ya resueltos, y cada uno de ellos es un problema que el agente intentaría construir manualmente:

- **Terminology.** `ValueSet`, `CodeSystem` y operaciones como `$expand`, `$validate-code` y `$lookup` están integradas. LOINC, SNOMED, RxNorm, ICD — todos accesibles a través de los endpoints estándar de FHIR. Su aplicación no *distribuye* un sistema de codificación; lo consulta (por ejemplo, [Termbox](https://www.health-samurai.io/termbox), un servidor de terminología FHIR dedicado). La terminología deja de ser una tarea pendiente «TODO: buscar una librería».
- **SQL on FHIR.** Defina un `ViewDefinition` que aplane los recursos FHIR en columnas tabulares y consulte la vista con SQL estándar. Los equipos de analítica obtienen SQL para informes; los datos permanecen en FHIR — sin necesidad de escribir un pipeline de transformación. También reduce el umbral para los paneles dentro de la aplicación: un desarrollador de aplicaciones que tendría dificultades para agregar recursos FHIR anidados puede escribir un `GROUP BY` plano contra una vista, de modo que un gráfico en el producto y un informe para los analistas leen desde la misma definición. Y un `ViewDefinition` es en sí mismo un recurso FHIR, lo que significa que el agente puede escribir uno — declarar columnas es una tarea mucho más acotada que construir manualmente lógica de recorrido sobre arrays anidados.
- **Structured Data Capture (SDC).** Los recursos `Questionnaire` de FHIR describen formularios; los usuarios los rellenan y las respuestas se almacenan como `QuestionnaireResponse` — vinculadas, validadas y buscables como cualquier otro dato FHIR. Las implementaciones de SDC incorporan constructores de formularios, renderizadores, lógica de extracción y galerías de formularios ya preparados.

<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 en la práctica: formularios nativos de FHIR construidos y probados en Aidbox Form Builder.</figcaption>
</figure>

### Tipos y habilidades para el agente

Las capas anteriores restringen los *datos*. Dos más cierran el ciclo en torno al *agente*.

- **SDK con tipos.** Dado que cada recurso tiene un `StructureDefinition` legible por máquina, los tipos de cliente pueden generarse en lugar de escribirse — para TypeScript, Python, C#, Java y otros. El beneficio para un agente va más allá del autocompletado: un campo mal escrito o una enumeración incorrecta falla a nivel de tipos, antes de que se envíe ninguna petición, y una única fuente de verdad generada es compartida por el servidor y el cliente, de modo que ambos lados de una funcionalidad no pueden desincronizarse. Los generadores disponibles públicamente convierten esto en un paso de compilación, no en un proyecto.
- **Habilidades de agente.** La capa más reciente es documentación escrita para agentes en lugar de para humanos. Dado que FHIR es un estándar público con API de servidor públicas, este conocimiento es reutilizable entre proyectos — cómo estructurar una consulta de búsqueda, cómo funcionan las políticas de acceso, cuándo recurrir a SQL on FHIR — en lugar de ser algo que cada equipo tiene que volver a enseñar. Apuntar a un agente hacia documentación actualizada también evita que se apoye en lo que un modelo absorbió en el momento de su entrenamiento, que para una especificación que publica nuevas versiones es una fuente real de código equivocado con total seguridad.

Ambas son cosas que se configuran una vez y de las que el agente se beneficia en cada tarea.

## El experimento: la misma aplicación, fundamentos distintos

La teoría es barata, así que lo pusimos a prueba. Nuestro campo de experimentación fue el **Historial Clínico Personal (PHR)** interno de Health Samurai — un producto real para nuestro propio equipo y las familias a las que atienden. Su alcance es genuinamente no trivial: gestionar registros de uno mismo y sus dependientes, introducir condiciones, medicamentos, alergias y procedimientos, cargar PDF médicos, consultas con médicos por chat, un resumen del paciente y asistencia de IA basada en los propios datos FHIR del paciente.

Lo construimos **dos veces con Claude Code — mismo alcance, fundamento diferente.** La primera vez dejamos que el agente construyera desde cero; la segunda vez reconstruimos toda la aplicación con FHIR como marco de trabajo.

| | **v1 — desde cero** | **v2 — sobre FHIR** |
|---|---|---|
| Pila | React + Node + Postgres estándar | Backend Aidbox, tipos FHIR generados, componentes de UI de Aidbox de código abierto, habilidades de agente |
| Modelo de datos | Inventado por el agente, por sesión | FHIR R4, fijo |
| Validación | Lo que el agente escribió | En el servidor, en cada escritura |
| Nueva funcionalidad | Volver a enseñar las convenciones que se habían desviado | Elegir un recurso, generar tipos, conectar la UI |
| Resultado | Seguía derivando | Más pequeña, coherente — la que mereció la pena conservar |

En la **v1**, el agente inventó su propio modelo de datos, su propia forma de API y sus propias reglas de validación. Cada nueva funcionalidad implicaba volver a enseñar al agente las convenciones de las que ya se había alejado. En la **v2**, reconstruir la misma aplicación sobre FHIR produjo una base de código más pequeña y más coherente — el agente se apoyó en FHIR en lugar de reinventarlo a su alrededor.

En la práctica eso se refleja en código ordinario. Los tipos generados más un conjunto de [habilidades de Claude Code de código abierto](https://github.com/HealthSamurai/phr/tree/main/.claude/skills) para trabajar contra Aidbox hicieron que el agente escribiera la creación de un paciente así, sin necesidad de diseñar ningún esquema propio:

```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.
```

Nada llamativo, que es precisamente el punto: no había ninguna decisión que tomar sobre nombres de campos o almacenamiento, y nada de lo que el agente pudiera alejarse en la siguiente sesión.

Tres conclusiones destacaron:

1. **Menos código.** El marco gestiona las partes aburridas — esquema, API, validación — de modo que el agente simplemente escribe menos de ellas.
2. **Un ciclo de retroalimentación más ajustado.** El servidor rechaza las escrituras no válidas con errores precisos, y el agente se autocorrige — sin misteriosos errores 500 enterrados a tres capas de profundidad.
3. **Las funcionalidades se convierten en conversaciones, no en proyectos.** Añadir un concepto clínico es un solo paso — elegir un recurso FHIR, generar tipos, conectar la UI — no cinco.

El ejemplo más claro: pedimos una funcionalidad de **resumen del paciente**. En una pila genérica eso significa diseñar un esquema de resumen, decidir cómo referenciar los registros subyacentes y construir una API a su alrededor. Sobre FHIR, el agente recurrió al recurso [`Composition`](https://build.fhir.org/composition.html) — el propio modelo del estándar para un documento clínico estructurado y con secciones — y lo implementó limpiamente: secciones que referencian los recursos existentes `Condition`, `MedicationStatement` y `AllergyIntolerance` del paciente, sin inventar ningún esquema ad hoc. Una funcionalidad que habría sido un pequeño proyecto se convirtió en una sola conversación, porque FHIR ya la había modelado.

## Cómo probarlo usted mismo

Si desea dar a su agente los mismos raíles, la receta de partida es breve:

- **Elija un servidor FHIR** como backend (por ejemplo, [Aidbox](https://www.health-samurai.io/aidbox)).
- **Genere tipos FHIR** con [`@atomic-ehr/codegen`](https://github.com/atomic-ehr/codegen) para que el agente programe contra estructuras reales.
- **Instale las [habilidades de Claude Code](https://github.com/HealthSamurai/phr/tree/main/.claude/skills)** para que el agente hable FHIR desde el primer momento.

El código fuente del PHR también es abierto, si desea ver un ejemplo completo: [github.com/HealthSamurai/phr](https://github.com/HealthSamurai/phr).

## ¿Qué sigue: sacar al programador del circuito?

Aquí está la pregunta que abrió el experimento. Si FHIR proporciona a un agente suficiente estructura para construir una aplicación real — ¿qué ocurriría si el usuario del agente no fuera un desarrollador en absoluto?

Imagine una plataforma donde **los médicos construyen sus propias aplicaciones**, iterando con un asistente de IA en lenguaje natural, y esas aplicaciones se conectan directamente a la infraestructura existente de la organización sanitaria:

- **Los médicos como constructores** — sin un equipo de desarrollo intermedio; describa el flujo de trabajo, obtenga una aplicación funcional.
- **Itere con IA** — cambie un formulario, ajuste una regla, añada un resumen. Una conversación, no un ticket.
- **Encaja en la pila del hospital** — habla FHIR con el HCE, respeta la autenticación, auditoría y políticas existentes. No un silo, sino un ciudadano más de la infraestructura.

Es la misma apuesta que todo este experimento, un nivel más arriba: **FHIR como la base que permite a la IA construir software sanitario para las personas que realmente lo necesitan.**

---

*¿Desea comentar cómo aplicar esto en su propia pila? Contacte con [Aleksandr Kislitsyn](https://www.linkedin.com/in/aleksandrkislitsyn/), o explore el repositorio de código abierto [PHR](https://github.com/HealthSamurai/phr) y sus [habilidades de Claude Code](https://github.com/HealthSamurai/phr/tree/main/.claude/skills).*