El sobre que contiene todo lo demás
Casi cualquier payload FHIR que abras será un Bundle. Consultas las observaciones de un paciente y recibes un Bundle. Escribes varios recursos relacionados en una sola operación atómica y envías un Bundle. Recibes un documento clínico, un mensaje o el historial de versiones de un recurso, y llega como Bundle. Y sin embargo es una de las partes menos explicadas de la especificación, porque es infraestructura y no contenido clínico: no aporta significado sanitario propio.
Precisamente por eso conviene leerlo con calma. Un Bundle es un contenedor con reglas, y esas reglas cambian según un único campo: Bundle.type. Una estructura obligatoria en una transacción está prohibida en un searchset. Un campo que en un Bundle significa «aquí vive este recurso», en otro significa «este es un identificador provisional que me he inventado». Interpretar mal esas reglas es una fuente habitual de integraciones que funcionan en pruebas y se rompen contra un servidor real.
Esta guía repasa qué es un Bundle, el conjunto completo de tipos —con énfasis en los cuatro que verás casi siempre—, la anatomía de una entrada y cómo reconocer de un vistazo qué clase de Bundle tienes delante. Cualquier ejemplo puede abrirse en el Explorador de Bundles FHIR, que analiza el contenido íntegramente en tu navegador.
Bundle: un recurso cuyo trabajo es contener recursos
En FHIR R4, Bundle es un recurso como cualquier otro: tiene resourceType igual a "Bundle", se puede almacenar y recuperar en un endpoint /Bundle y tiene sus propios elementos. Lo peculiar es que su carga útil son otros recursos. La estructura de primer nivel, tal como la define la página de Bundle en R4, es breve:
identifier(0..1) — identificador persistente del propio Bundle, que se mantiene cuando el Bundle se copia entre servidores.type(1..1) — el único elemento obligatorio. Declara qué clase de Bundle es y, por tanto, qué reglas aplican.timestamp(0..1) — cuándo se ensamblaron los recursos en este Bundle.total(0..1) — número total de coincidencias, restringido a Bundles de búsqueda e historial.link(0..*) — enlaces de navegación, cada uno conrelationyurl. Así funciona la paginación.entry(0..*) — las entradas, cada una envolviendo un recurso más metadatos sobre él.signature(0..1) — firma digital del Bundle.
Fíjate en lo que no está. No hay paciente, ni fecha de asistencia, ni autor. Un Bundle no tiene semántica clínica: es embalaje. El significado vive en los recursos que contiene y en el type que indica cómo interpretar ese embalaje.
El conjunto completo de tipos
R4 define nueve códigos para Bundle.type: document, message, transaction, transaction-response, batch, batch-response, history, searchset y collection. Cuatro de ellos —searchset, transaction, collection y document— cubren la inmensa mayoría de lo que maneja un ingeniero de integración, y el resto se entienden mejor en relación con esos. transaction-response y batch-response son lo que devuelve el servidor tras procesar una transacción o un batch. history es el historial de versiones de un recurso o de un servidor. message es el sobre de mensajería basada en eventos, con un MessageHeader como primera entrada.
searchset: el Bundle que se lee
Cuando lanzas una búsqueda FHIR —GET [base]/Observation?patient=Patient/123— el servidor responde con un Bundle de tipo searchset. Es el primero que conoce casi todo el mundo y, desde el punto de vista del cliente, es de solo lectura: lo construyó el servidor y tú lo consumes.
Aquí importan dos elementos que no aparecen en ningún otro sitio. Bundle.total lleva el número total de recursos que cumplen la búsqueda, que puede ser mucho mayor que el número de entradas de esta página concreta. Y Bundle.entry.search lleva metadatos por entrada: mode y score. El campo mode es el relevante: distingue una entrada que realmente coincidió con tu consulta de otra que el servidor incorporó como contexto porque pediste _include o _revinclude. Si consumes todas las entradas de un searchset como si fueran coincidencias, un Practitioner incluido por _include se convertirá silenciosamente en un falso positivo.
La paginación se expresa con Bundle.link. Un searchset paginado lleva enlaces con relaciones como self, next, previous, first y last. La forma correcta de recorrer un resultado grande es seguir el enlace next hasta que deje de aparecer, en lugar de construir tu propia aritmética de desplazamientos: el cursor del servidor puede no ser un offset simple, y calcularlo por tu cuenta puede saltarse o duplicar filas sin avisar.
transaction: el Bundle que se escribe
Un Bundle de tipo transaction es lo contrario: lo construyes tú y lo envías por POST a la URL base del servidor, y cada entrada describe una operación a ejecutar. La garantía que lo define, según las reglas de la API REST de R4, es la atomicidad: el servidor debe aceptar todas las acciones y devolver 200 OK junto con un Bundle transaction-response, o rechazarlo todo y devolver una respuesta 4xx o 5xx con un único OperationOutcome. No existe el éxito parcial. Un Bundle batch es estructuralmente idéntico pero renuncia a esa garantía: cada entrada se procesa de forma independiente, el estado HTTP global es 200 OK aunque hayan fallado entradas concretas, y hay que inspeccionar la respuesta de cada una para saber qué ocurrió.
Como las entradas son instrucciones y no solo datos, cada una lleva Bundle.entry.request, con estos subelementos: method, url, ifNoneMatch, ifModifiedSince, ifMatch e ifNoneExist. El method es el verbo HTTP: POST para crear, PUT para actualizar, DELETE para borrar, GET para leer. Los campos condicionales se corresponden con las operaciones condicionales de FHIR: ifNoneExist, por ejemplo, expresa «crea esto solo si no existe ya un recurso que cumpla esta búsqueda», que es la forma de evitar duplicar un Patient que quizá ya enviaste.
La especificación también fija el orden en que un servidor procesa una transacción, y ese orden es independiente del orden de las entradas: primero las interacciones DELETE, después POST, después PUT o PATCH, después GET o HEAD, y por último la resolución de referencias condicionales. Por eso la norma dice que el resultado no debe depender del orden de los recursos en la transacción, y que un mismo recurso solo puede aparecer una vez por identidad.
Por qué una transaction resulta rara la primera vez
Sorprende encontrar un Bundle de transacción donde todos los fullUrl son UUID y todas las referencias apuntan a esos UUID. Es deliberado. Cuando creas varios recursos enlazados de una vez —un Patient, un Encounter de ese paciente y una Observation de ese encuentro— ninguno tiene todavía un id asignado por el servidor. No puedes escribir "reference": "Patient/123" porque 123 no existe. Así que inventas un identificador provisional, lo usas como fullUrl de la entrada y lo referencias desde las demás. Cuando el servidor procesa los POST y asigna ids reales, reescribe las referencias coincidentes dentro del mismo Bundle para que los enlaces sobrevivan. La mecánica de esa reescritura, y qué ocurre cuando una referencia apunta a la nada, se detalla en Cómo funcionan las referencias en FHIR.

collection: el Bundle que solo agrupa
Un Bundle collection es el más sencillo de todos: un conjunto de recursos sin ninguna semántica de procesamiento. Sin elementos request, sin metadatos de búsqueda, sin atomicidad, sin regla sobre la primera entrada. Significa «aquí hay un montón de recursos que van juntos por algún motivo que no estoy codificando en el propio Bundle».
Eso lo convierte en el formato natural para fixtures de prueba, conjuntos de datos de ejemplo, extracciones para análisis y cualquier payload ensamblado a mano que quieras pasar a un compañero. También es el tipo al que deberías recurrir cuando te tiente usar transaction para algo que no es una transacción: etiquetar un fichero estático como transacción invita a que un sistema aguas abajo lo envíe por POST y empiece a escribir registros.
document: el Bundle que es un documento clínico
Un Bundle document es un documento clínico congelado y susceptible de atestación, la respuesta de FHIR a CDA. Es el tipo con reglas estructurales más estrictas, y la página de Bundle de R4 las expresa como invariantes: un Bundle documento debe tener un identifier con system y value, debe tener fecha, y su primera entrada debe ser un recurso Composition. Ese Composition es la columna vertebral: declara el tipo de documento, el sujeto, el autor, el atestador y un conjunto de secciones cuyas entradas apuntan al resto del Bundle.
El tipo message tiene una regla paralela: la primera entrada de un Bundle message debe ser un MessageHeader. En ambos casos, esa regla de «la primera entrada es especial» es lo que hace interpretable el Bundle como un todo y no como una bolsa de piezas sueltas.
Anatomía de una entrada
Todas las entradas de todos los Bundles comparten la misma forma, aunque qué partes están permitidas depende del tipo:
fullUrl(0..1) — URL absoluta del recurso de esta entrada.resource(0..1) — el recurso en sí.search(0..1) —modeyscore. Solo permitido en Bundles de búsqueda.request(0..1) —method,urly los campos condicionales. Obligatorio en batch, transaction e history; prohibido en el resto.response(0..1) —status,location,etag,lastModified,outcome. Obligatorio en batch-response, transaction-response e history; prohibido en el resto.link(0..*) — enlaces a nivel de entrada, con la misma estructura que los del Bundle.
Esas reglas de «obligatorio aquí, prohibido allá» son invariantes formales del recurso Bundle, no recomendaciones de estilo. Un validador señalará un elemento search en un Bundle collection, y señalará una entrada de transacción sin request.
fullUrl es el campo peor entendido de FHIR
fullUrl responde a la pregunta «¿cuál es la identidad de este recurso?», y la respuesta depende del tipo de Bundle. En un searchset devuelto por un servidor es la dirección real y resoluble del recurso: https://ejemplo.org/fhir/Patient/123. En una transacción que estás a punto de enviar suele ser un identificador provisional urn:uuid: que solo existe mientras dure ese Bundle, porque el recurso aún no tiene identidad en el servidor. En un collection puede ser cualquiera de las dos cosas, o no estar.
Los invariantes de R4 exigen que los valores de fullUrl sean únicos dentro de un Bundle, salvo que dos entradas compartan URL y se distingan por meta.versionId, que es exactamente el caso de un Bundle de historial, donde el mismo recurso aparece en varias versiones. Si generas Bundles reutilizando el mismo fullUrl para recursos distintos, has creado algo que un consumidor conforme no puede interpretar.
Reconocer un Bundle de un vistazo
Al abrir un payload desconocido, unas pocas señales estructurales lo identifican de inmediato, incluso antes de leer type:
| Señal | Casi con certeza es un… |
|---|---|
total presente y link con relación next | searchset (paginado) |
Todas las entradas tienen request.method | transaction o batch |
Todas las entradas tienen response.status | transaction-response o batch-response |
Todos los fullUrl son urn:uuid: | transaction en construcción en el cliente |
| La primera entrada es un Composition | document |
| La primera entrada es un MessageHeader | message |
| Entradas desnudas, sin request/response/search | collection |
Estas heurísticas son útiles justamente porque type queda enterrado al principio de un fichero que puede ser de decenas de megabytes de JSON minificado, y porque un Bundle mal etiquetado —contenido que parece una transacción pero está tipado como collection— es un defecto real y frecuente.
Dónde acaban los Bundles y empieza NDJSON
Los Bundles tienen un techo natural de tamaño. Como un Bundle es un único documento JSON, un consumidor suele tener que analizarlo entero antes de poder actuar sobre cualquier parte, lo que hace que el consumo de memoria escale con el tamaño de la extracción. Eso está bien para una página de búsqueda o un documento clínico, y es inviable para una extracción poblacional de todas las Observation de un sistema de salud.
Por eso la especificación de FHIR Bulk Data Access no devuelve Bundles. Su salida es NDJSON —un recurso por línea, y cada fichero contiene recursos de un solo tipo—, que se puede procesar línea a línea con memoria constante. Si vas a pasar de la integración por paciente al análisis poblacional, ese cambio es lo esencial que hay que interiorizar; La exportación Bulk Data de FHIR explicada recorre el flujo $export de principio a fin.
Trabajar con un Bundle en la práctica
Leer un Bundle a ojo deja de funcionar hacia la segunda pantalla de JSON. Las preguntas prácticas —qué tipos de recurso hay y cuántos de cada uno, qué recursos apuntan a cuáles, hay algo referenciando algo que no está en el fichero, cómo se ven todas las Observation en forma de tabla— son preguntas estructurales dolorosas de responder con un editor de texto.
El Explorador de Bundles FHIR está pensado para exactamente eso. Detecta automáticamente lo que pegas —un Bundle, un recurso suelto, NDJSON de una exportación masiva o un array JSON de recursos— y ofrece un recuento por tipo de recurso, un mapa de referencias y una tabla plana exportable a CSV. Los valores repetidos pueden unirse en una celda separados por punto y coma o expandirse a una fila por repetición, de modo que nada se pierde en silencio. Admite hasta 25 MB y 20.000 recursos, y cuando alcanza un límite indica exactamente cuántos recursos no se analizaron en lugar de truncar sin avisar.
Dos propiedades importan en el ámbito sanitario. Primera: es de solo lectura y local. Las referencias se resuelven únicamente contra los recursos presentes en lo que has pegado, nunca descargando nada por red, porque un Bundle son datos de paciente y recuperar en silencio un recurso referenciado desde un servidor externo supondría filtrarlos. Segunda: no es un validador; no te dirá que un perfil exige un elemento que omitiste. Para comprobar conformidad, combínalo con el Validador de Recursos FHIR.
Conclusión
Un Bundle es un contenedor cuyo comportamiento lo determina íntegramente Bundle.type. Un searchset es una página de resultados, con total, enlaces de paginación y modo de búsqueda por entrada. Una transaction es un conjunto atómico de instrucciones, donde cada entrada lleva un request y las identidades provisionales las reescribe el servidor. Un collection es un montón de recursos sin semántica de procesamiento. Un document es un artefacto clínico congelado anclado por un Composition como primera entrada. Aprende a leer type, fullUrl y la presencia o ausencia de request, response y search, y la estructura de cualquier payload FHIR se vuelve evidente en segundos; y cuando no lo sea, ábrelo en el Explorador de Bundles FHIR y deja que la estructura se muestre sola, sin que los datos salgan de tu navegador.