---
{
  "title": "$batch-validate: Valide todos los recursos almacenados frente a sus perfiles, a escala",
  "description": "Aidbox 2607 reemplaza la antigua API de validación por lotes con la operación $batch-validate — valide un tipo de recurso completo ya existente en su base de datos, de forma síncrona o asíncrona, y analice exactamente qué recursos no son conformes y por qué.",
  "date": "2026-07-13",
  "author": "Andrew Listopadov",
  "reading-time": "9 min read",
  "tags": ["Aidbox", "FHIR Profiling", "Compliance", "Database"],
  "utm-campaign": "feature",
  "utm-content": "batch-validate",
  "tldr": "$batch-validate comprueba todos los recursos de un tipo ya almacenados en Aidbox frente a su esquema FHIR y a cualquier perfil que usted indique, de forma síncrona o asíncrona. Los resultados se agregan en un formato compacto e indexado por incidencias, de modo que validar 100 GB de datos no conformes no añade 100 GB a su base de datos. Disponible desde Aidbox 2607.",
  "seo-tags": ["FHIR validation", "batch validation", "FHIR profile validation", "Aidbox", "FHIR conformance", "healthcare data quality"]
}
---

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

---

Un servidor FHIR acumula registros médicos complejos y profundamente estructurados procedentes de todos los sistemas que lo alimentan, y es fácil asumir que todos son correctos.
Confiar en los datos es una cosa; poder verificarlos —de forma robusta y a escala— es otra.
Con millones de recursos, la validación manual de cada uno resulta tediosa y probablemente poco razonable.
Y rara vez se valida una sola vez, porque los perfiles no son estáticos.
Las guías de implementación como US Core y las guías HL7 Da Vinci publican nuevas versiones con regularidad, cada una de las cuales añade elementos, estrecha las cardinalidades o modifica los conjuntos de valores a los que se vinculan.
Cada vez que adopta una nueva versión de perfil —o publica una propia— regresa la misma pregunta: qué parte de lo que ya almacena sigue siendo conforme.
¿Cómo verifica todo eso, de forma repetida, sin que se convierta en un proyecto en sí mismo?

La operación `$validate` de FHIR podría, en teoría, automatizarse, pero habría que obtener cada recurso y enviarlo de vuelta mediante POST, recogiendo y filtrando los resultados.
Este enfoque tiene muchos problemas y es difícil de escalar.
Lo ideal sería poder pedirle al servidor que valide un tipo de recurso específico e informarle de los errores en una sola llamada, con una capacidad adecuada para escalar tanto horizontal como verticalmente.

Eso es exactamente lo que hace `$batch-validate`.
Se incluye en Aidbox 2607 y reemplaza por completo la anterior API de validación por lotes.

## Por qué reconstruimos la validación por lotes

Aidbox ha contado con validación asíncrona por lotes durante años, expuesta a través de un conjunto de RPCs (`aidbox.validation/batch-validation` y similares).
Funcionaba, pero tenía varios problemas.
Por un lado, **cada error de validación se almacenaba como su propio recurso `BatchValidationError`.**
Además, era bastante lento, lo que convertía la validación de grandes cantidades de recursos en una tarea innecesariamente larga.

Por último, validar un conjunto de datos de gran tamaño podía generar tantos resultados que estos rivalaban en tamaño con los propios datos.
Cien gigabytes de recursos no conformes podían producir algo cercano a cien gigabytes de recursos de error.
El mecanismo al que se recurría para *comprender* un problema de calidad de datos empeoraba el problema de almacenamiento.

Además, era exclusivamente asíncrono, tenía forma de RPC en lugar de operación FHIR, y le entregaba un montón de recursos de error para consultar en vez de una respuesta directa.

`$batch-validate` conserva la parte positiva —validar lo que ya está almacenado, en paralelo— y corrige el resto.

|                | Validación por lotes anterior                 | `$batch-validate`                                                   |
|----------------|-----------------------------------------------|---------------------------------------------------------------------|
| Interfaz       | RPCs propietarios                             | Operación FHIR (`Parameters` de entrada y salida)                   |
| Modos          | Solo asíncrono                                | Síncrono **o** asíncrono                                            |
| Almacenamiento de resultados | Un recurso `BatchValidationError` por error | Una fila por incidencia **distinta** más una tabla reducida de IDs de recursos |
| Coste de almacenamiento | Crece con el número de errores        | Acotado — los cuerpos de los recursos nunca se copian               |
| Salida         | Un montón de recursos de error para consultar | Resumen de incidencias ordenado por gravedad con desglose bajo demanda |
| Escalabilidad  | Fija                                          | Fragmentos particionados por hash, en streaming, paralelos entre nodos e indexables |

Los antiguos RPCs `aidbox.validation/*` y los recursos `BatchValidationRun` / `BatchValidationError` ya no existen. Se trata de un cambio que rompe la compatibilidad; si los utilizaba, migre a la operación descrita a continuación.

## Cómo utilizarlo

`$batch-validate` se ejecuta sobre un único tipo de recurso.
El único parámetro obligatorio es `_since`, un límite inferior sobre `meta.lastUpdated`.
Obliga a que cada ejecución declare una ventana temporal en lugar de analizar todo el conjunto de datos por accidente — para validar todo, pase la época.

Por tanto, para validar todas las Observation actualizadas en abril de 2026, podemos llamar a `$batch-validate` de la siguiente manera:

```yaml
POST /fhir/Observation/$batch-validate
Content-Type: application/json

resourceType: Parameters
parameter:
  - {name: _since, valueInstant: "2026-04-01T00:00:00Z"}
  - {name: _until, valueInstant: "2026-05-01T00:00:00Z"}
```

Por defecto, la llamada es **síncrona**: bloquea y devuelve un resumen `Parameters` con los conteos principales y una entrada por incidencia distinta, ordenadas de mayor a menor gravedad.

```yaml
resourceType: Parameters
parameter:
  - {name: task-id,   valueString: "b1f9..."}
  - {name: validated, valueUnsignedInt: 1804646}   # resources checked
  - {name: valid,     valueUnsignedInt: 1317494}   # no issues
  - {name: invalid,   valueUnsignedInt: 487152}    # total invalid resources
  - {name: invalid-resources, valueUrl: "/fhir/$batch-validate/b1f9.../invalid-resources"}
  - name: issue
    part:
      - {name: code,        valueCode: invalid-slice-cardinality}
      - {name: expression,  valueString: category}
      - {name: profile,     valueString: "http://hl7.org/fhir/us/core/StructureDefinition/us-core-observation-lab"}
      - {name: count,       valueUnsignedInt: 486018}   # resources hitting this exact issue
      - {name: diagnostics, valueString: "Observation.category: element count is outside the allowed range"}
```

`count` es el número de recursos distintos que presentan exactamente esa incidencia — la forma más rápida de saber si un problema afecta a seis recursos o a seiscientos mil.

Las llamadas síncronas son adecuadas para un conjunto reducido de recursos, cuando se desea la respuesta de inmediato.
El trabajo sigue ejecutándose en paralelo: `number-of-chunks` (configurado por llamada) divide los recursos en ese número de fragmentos, y el parámetro [`scheduler-executors`](https://www.health-samurai.io/docs/aidbox/reference/all-settings#scheduler-executors) controla cuántos se ejecutan simultáneamente en el nodo.
Juntos permiten equilibrar la granularidad de los fragmentos frente a la carga de una sola máquina — ese es su control de escalado vertical.

Sin embargo, cuando se desea validar un conjunto de datos considerablemente mayor, puede ser conveniente hacerlo de forma asíncrona.

## Modo asíncrono para grandes conjuntos de datos

Para que cualquier llamada a `$batch-validate` sea asíncrona, basta con añadir la cabecera `Prefer: respond-async`.
Aidbox programa entonces el trabajo en su motor de tareas, distribuyéndolo entre nodos y permitiendo tanto el escalado horizontal como el vertical.

```yaml
POST /fhir/Observation/$batch-validate
Prefer: respond-async

resourceType: Parameters
parameter:
  - {name: _since, valueInstant: "1970-01-01T00:00:00Z"} # basically will validate every Observation in the database
```

Recibirá un `202` con una cabecera `Content-Location` que puede consultar para ver el progreso:

```http
GET /fhir/$batch-validate/b1f9...
```

Mientras se ejecuta, obtendrá `202` con una cabecera `X-Progress: 45%`.
Al finalizar, recibirá el mismo resumen `Parameters` que devuelve una llamada síncrona.
Tanto las llamadas síncronas como las asíncronas persisten sus resultados bajo un `task-id`, por lo que no hay diferencia en la forma de analizar los resultados.

## Explorar los recursos no válidos

El resumen le indica qué incidencias existen y cuántos recursos presenta cada una.
Para ver los recursos reales, siga el enlace `invalid-resources` — filtrelo a una sola incidencia con `_issue` y pagine con `_count` / `_page`:

```http
GET /fhir/$batch-validate/b1f9.../invalid-resources?_count=50&_page=1
```

Cada recurso no válido se devuelve con un `fullUrl` **específico de versión** que apunta a la versión exacta que fue validada, el cuerpo del recurso y un `OperationOutcome` que lista todas las incidencias de ese recurso:

```yaml
- name: resource
  part:
    - {name: fullUrl, valueUrl: "/Observation/obs-42/_history/7"}
    - name: resource
      resource: {resourceType: Observation}
    - name: outcome
      resource:
        resourceType: OperationOutcome
        issue:
          - {severity: fatal, code: invalid, expression: [Observation.category], diagnostics: "..."}
```

El outcome lista el conjunto **completo** de incidencias de un recurso, incluso cuando `_issue` restringe qué recursos se devuelven — de modo que nunca se corrige un problema solo para descubrir un segundo en la siguiente pasada.

## Pruebe un perfil antes de aplicarlo

La razón más habitual para ejecutar esto es un nuevo perfil: quiere saber qué falla antes de convertirlo en un requisito.
Pase uno o más canónicos de `profile`, y cada recurso se validará contra ellos además de frente a su esquema base.

```yaml
resourceType: Parameters
parameter:
  - {name: _since,  valueInstant: "1970-01-01T00:00:00Z"}
  - {name: profile, valueCanonical: "http://hl7.org/fhir/us/core/StructureDefinition/us-core-observation-lab"}
```

Varios perfiles son conjuntivos (AND): un recurso es conforme solo si cumple todos ellos, y las incidencias son la unión de todas.
Así puede activar US Core sabiendo exactamente qué fallaría, en lugar de descubrirlo en producción.

## Ajuste el validador por ejecución

A veces se desea un análisis estructural rápido sin preocuparse aún por la terminología; otras veces se quiere ser más estricto que los valores predeterminados del sistema.
Cada parámetro `disable-*` / `strict-*` sobreescribe una configuración del validador solo para esa ejecución — omítalo para mantener el comportamiento configurado en el sistema.

| Parámetro                        | Efecto                                                                      |
|----------------------------------|-----------------------------------------------------------------------------|
| `disable-terminology-validation` | Omite las comprobaciones de enlaces codificados / terminología              |
| `disable-primitive-validation`   | Omite las comprobaciones de tipos primitivos y formatos                     |
| `disable-slicing-validation`     | Omite la validación de slices                                               |
| `disable-constraint-validation`  | Omite **todos** los invariantes FHIRPath (o los comprueba todos si es `false`) |
| `disable-constraint`             | Omite invariantes específicos por clave (p. ej. `us-core-8`)               |
| `strict-profile-resolution`      | Trata un perfil no resuelto como un error en lugar de omitirlo             |
| `strict-extension-resolution`    | Trata una extensión no resuelta como un error                              |

`strict-profile-resolution` merece especial atención.
Sin él, una URL de perfil que no se resuelve se omite, de modo que una errata se convierte en un informe de «conformidad» que es silenciosamente incorrecto.
Actívelo cuando desee que la ejecución falle de forma explícita.

## Diseñado para escalar

Internamente, `$batch-validate` particiona los recursos por hash en un número fijo de fragmentos (`number-of-chunks`, por defecto 12).
Cada fragmento valida su porción `mod(hash(id), N)`, y los fragmentos se agregan en un único resultado:

```mermaid
flowchart LR
    A["POST /fhir/Observation/$batch-validate"] --> B{Hash-partition by id}
    B --> C[chunk 0]
    B --> D[chunk 1]
    B --> E[chunk ...]
    C --> F[(Aggregated results)]
    D --> F
    E --> F
```

Cada fragmento se procesa en streaming, por lo que el uso de memoria permanece acotado independientemente del tamaño del conjunto de datos.
Una ejecución síncrona ejecuta sus fragmentos en un pool de trabajadores dedicado — un pool de hilos fijo que Aidbox crea para esa ejecución y destruye al finalizar, dimensionado por el parámetro [`scheduler-executors`](https://www.health-samurai.io/docs/aidbox/reference/all-settings#scheduler-executors) y separado de los hilos que atienden el tráfico regular de la API.
Como máximo, ese número de fragmentos se ejecuta simultáneamente, por lo que no hay límite superior en `number-of-chunks` — un número mayor simplemente encola en lugar de aumentar el uso de memoria.
Una ejecución asíncrona no utiliza este pool local; en su lugar, se ejecuta en el pool propio del motor de tareas, escribiendo una fila de planificador por fragmento para que cualquier nodo pueda reclamarla.
Dado que no toma prestados hilos de solicitud ni conexiones del pool, es seguro frente a un sistema en producción; la CPU es el recurso que se consume, por lo que se recomienda el modo asíncrono para un primer análisis de gran envergadura.

La ruta asíncrona es también la que permite que una ejecución escale entre máquinas.
Cada fragmento es un trabajo en un planificador que reside en el Postgres compartido, de modo que se pueden ejecutar varias instancias de Aidbox en diferentes máquinas contra una misma base de datos y estas se reparten los fragmentos entre ellas — una ejecución se acelera a medida que se añaden nodos.
Un fragmento es reclamado exactamente por una instancia, por lo que nunca se ejecuta dos veces; y si una instancia falla a mitad de la ejecución, el planificador reclama su fragmento y lo reintenta en otra, con escrituras idempotentes para que un fragmento reclamado nunca cuente doble.

```mermaid
flowchart LR
    A["POST + Prefer: respond-async"] --> Q[(Shared Postgres chunk queue)]
    Q -->|claim| N1[Aidbox node 1]
    Q -->|claim| N2[Aidbox node 2]
    Q -->|claim| N3[Aidbox node ...]
    N1 --> R[(Aggregated results)]
    N2 --> R
    N3 --> R
```

Dado que `N` es fijo para una ejecución, el predicado de partición es una expresión constante que puede indexarse.
Para un conjunto de datos muy grande validado con un número elevado de fragmentos, un índice de expresión coincidente convierte cada fragmento de un escaneo completo en un escaneo de índice selectivo:

```sql
CREATE INDEX CONCURRENTLY observation_batch_validate_10000
  ON observation (mod(abs(hashtextextended(id, 0)), 10000));
```

Luego ejecute con `number-of-chunks: 10000`.
El módulo del índice debe coincidir con el número de fragmentos, o PostgreSQL no lo utilizará — compruébelo con `EXPLAIN`.

## Compacto por diseño

Esta es la razón por la que validar 100 GB de datos incorrectos ya no le cuesta otros 100 GB.
Aidbox almacena los resultados en un formato agregado e indexado por incidencias en un esquema dedicado `aidbox_batch_validation`:

| Tabla              | Contiene                                                                   |
|--------------------|----------------------------------------------------------------------------|
| `issue`            | una fila por error **distinto**                                            |
| `invalid_resource` | una pequeña fila `(issue, resource_id, version)` por recurso — solo IDs   |
| `chunk_stat`       | una fila por fragmento con sus métricas                                    |

Todas las ocurrencias que comparten perfil, tipo de recurso, ruta normalizada, código y restricción se colapsan en una única incidencia cuyo conteo es el número de recursos distintos que la presentan.
Aidbox nunca copia los cuerpos de los recursos no válidos ni sus `OperationOutcome`: el desglose vuelve a leer cada cuerpo desde el historial en la versión validada y reconstruye el outcome a partir de los campos almacenados.
Son los problemas distintos, no el volumen bruto, los que determinan el coste de almacenamiento.

## Pruébelo

`$batch-validate` está disponible en Aidbox 2607.
Apúntelo a un tipo de recurso, pase un `_since` de época, y obtendrá un mapa ordenado por gravedad de los problemas de calidad de sus datos en una sola llamada — y luego podrá desglosar los recursos exactos detrás de cada uno.

¿Quiere verlo funcionar sin escribir nada?
Hemos publicado un cuaderno interactivo que ejecuta el flujo completo de principio a fin: carga un conjunto de Patients de muestra, crea un perfil basado en US Core Patient, los valida en una sola llamada y muestra los resultados — la distribución entre válidos e inválidos, los Patients no válidos por incidencia y dónde se concentran los problemas.
Lea el cuaderno de [**validación por lotes**](/docs/aidbox/notebooks/64c853e6-f13a-4246-bb0b-044020b3b01a) aquí en la documentación y ábralo en su propio Aidbox para ejecutarlo de principio a fin.

Referencia completa: [Validación de recursos por lotes](https://www.health-samurai.io/docs/aidbox/modules/profiling-and-validation/batch-resource-validation).