# Health Samurai

> Health Samurai builds FHIR-native healthcare infrastructure. Our main product is Aidbox — a FHIR server and healthcare development platform used by healthcare organizations worldwide to build, deploy, and scale healthcare solutions.

## Key facts

- **Official YouTube channel:** [Health Samurai on YouTube](https://www.youtube.com/@HealthSamurai) — product demos, webinars, conference talks, and Aidbox / Formbox updates.
- **Company:** Health Samurai, Inc. delivers FHIR-native platforms and professional services; team has built healthcare IT solutions since 2004.
- **Aidbox:** Enterprise FHIR server and data platform on PostgreSQL — REST API for FHIR R4/R5, SQL on FHIR, SMART on FHIR, subscriptions, bulk data, terminology services, HIPAA-aligned deployment options. Development tier is free; production pricing is flat-rate (see [Pricing](https://www.health-samurai.io/price.md)).
- **Formbox (Aidbox Forms):** Digital healthcare forms platform — FHIR SDC-aligned capture, no-code builder, workflows, and a large library of clinical form templates (4,000+ on the marketing site).
- **Compliance & trust:** ISO 27001:2022 certified organization; Aidbox FHIR API tooling has been part of ONC certification (e.g. G10) offerings — see product pages for current certification scope.
- **Open source & docs:** Aidbox-related open source on GitHub (https://github.com/Aidbox); full technical documentation at [Aidbox docs](https://www.health-samurai.io/docs/aidbox).

## Videos & playlists

Channel: [@HealthSamurai](https://www.youtube.com/@HealthSamurai). Key playlists:

- [Aidbox Forms / Formbox — tutorials and walkthroughs](https://www.youtube.com/playlist?list=PLEOOqZS1Ntwa7hhEmPnO5BhS-8lgevfzQ) — Step-by-step guides and feature demos for Formbox.
- [FHIR DevDays 2025 — Health Samurai talks and demos](https://www.youtube.com/playlist?list=PLEOOqZS1NtwbUhqN653j-cuGHmPfGF6ck) — Talks and demos from HL7 FHIR DevDays 2025.
- [FHIR community interviews](https://www.youtube.com/playlist?list=PLEOOqZS1NtwaHpIABTJBtZa_ivBhEw4Sx) — Interviews with Grahame Grieve, Nikolai Ryzhikov, Lloyd McKenzie, Ewout Kramer, Maria Manuel Salazar, and others.
- [Agents on FHIR — Agentic coding in Healthcare and FHIR](https://www.youtube.com/playlist?list=PLEOOqZS1NtwY54jljDikeRdeRM8iOMAUq) — Agentic coding and AI agents in healthcare and FHIR.
- [Analytics on FHIR Conference (Dec 2025)](https://www.youtube.com/playlist?list=PLEOOqZS1NtwYW98ow-DppdykL3xrhqIoD) — Analytics on FHIR Conference, 04–05 December 2025.
- [FHIR SDC Conference (25 June 2025)](https://www.youtube.com/playlist?list=PLEOOqZS1NtwbhWQAT7JjJ_aNzbM5tKxwV) — FHIR SDC Conference recordings.
- [FHIR Meetups](https://www.youtube.com/playlist?list=PLEOOqZS1NtwYtdDRNjrO6XfRp9JiUxFPH) — FHIR community meetups and talks.
- [SQL on FHIR](https://www.youtube.com/playlist?list=PLEOOqZS1Ntwa7HnP3PgTLT7zPDJydsdx1) — SQL on FHIR technical content and demos.
- [FHIR Access Control Meetup 2024](https://www.youtube.com/playlist?list=PLEOOqZS1NtwalGOditsQzRxwlw53aPwZE) — Health Samurai / HealthDevHub meetup on access control, data segmentation, and authorization.

## Products

- [Aidbox — FHIR Server & Database](https://www.health-samurai.io/aidbox.md): Enterprise FHIR server powered by PostgreSQL. Full FHIR R4/R5 compliance, SQL on FHIR, SMART on FHIR, Subscriptions, Bulk Data, and Terminology Services.
- [Aidbox Forms / Formbox](https://www.health-samurai.io/medical-form.md): FHIR SDC-compliant digital healthcare form builder. Design, manage, and run forms with built-in FHIR data capture.
- [Termbox — FHIR Terminologies](https://www.health-samurai.io/termbox.md): Production-grade SaaS for FHIR terminology management. SNOMED CT, LOINC, RxNorm, ICD-10, and more.
- [MDMbox — Master Patient Index & MDM](https://www.health-samurai.io/mdmbox.md): FHIR-native Master Patient Index (eMPI) and Master Data Management software for healthcare. Deduplicates patient records and creates a single source of truth across EHRs, LIS, telehealth, and claims systems via probabilistic matching.
- [Smartbox — ONC-Certified FHIR API Tools](https://www.health-samurai.io/smartbox.md): ONC-certified API tools for EHR compliance and certification. SMART on FHIR app launcher and authorization platform.
- [eRxbox — E-Prescription](https://www.health-samurai.io/erxbox.md): FHIR-native e-prescription module with prescription network integration.
- [Payerbox — CMS-0057-F Compliance](https://www.health-samurai.io/cms-0057-f.md): Turnkey CMS-0057-F compliance platform for health plans. Prior Authorization, Patient Access, Provider Access, and Payer-to-Payer APIs.
- [Payerbox Prior Authorization](https://www.health-samurai.io/prior-auth.md): FHIR-native PA channel for delegated entities serving MA, MMC, QHP, and CHIP programs.
- [RCMbox — Revenue Cycle Management](https://www.health-samurai.io/rcmbox.md): Headless RCM backend for healthcare billing teams scaling claims, ERA, and billing workflows across states and payers.
- [Interbox — Integration Engine](https://www.health-samurai.io/interbox.md): Durable, queue-driven integration runtime. HL7v2 modernization and custom pipelines delivering FHIR to Aidbox.
- [Healthcare Data Platform Toolkit](https://www.health-samurai.io/healthcare-data-platform-toolkit-aidbox): Aggregate, store, manage, and analyze FHIR data in a secure, compliant, and performant way.
- [EHR Development Toolkit](https://www.health-samurai.io/ehr-toolkit): Develop custom EHRs or enrich existing ones with pluggable modules — Form Builder, MPI, and standardized FHIR API.
- [C-CDA / FHIR Converter](https://www.health-samurai.io/c-cda-to-fhir-converter): Bidirectional conversion between C-CDA and FHIR formats.

## Solutions

- [For Digital Health Startups](https://www.health-samurai.io/startups): Speed up development and reduce time to market for digital health startups.
- [For Payers](https://www.health-samurai.io/payers): Comply with CMS Interoperability and Patient Access rule in 3-6 months.
- [Telemedicine Platform](https://www.health-samurai.io/telemedicine): Build custom telehealth solutions on FHIR.
- [Professional Services](https://www.health-samurai.io/services): FHIR-first software development with dedicated teams and domain expertise.

## Documentation

- [Aidbox Documentation](https://www.health-samurai.io/docs/aidbox): Complete Aidbox reference, guides, and API documentation.
- [Documentation llms.txt](https://www.health-samurai.io/docs/llms.txt): LLM-friendly index of all documentation.

## Pricing

- [Aidbox Pricing](https://www.health-samurai.io/price.md): Development (free), Production ($1,900/mo flat), Enterprise (custom).

## Company

- [About Health Samurai](https://www.health-samurai.io/company.md): Building FHIR-native healthcare infrastructure since 2014.
- [Contact Us](https://www.health-samurai.io/contacts.md): Get in touch — hello@health-samurai.io
- [Open Source](https://www.health-samurai.io/opensource): Health Samurai open source products and projects.
- [Careers](https://www.health-samurai.io/careers): Join the team building the future of healthcare interoperability.

## Case Studies

> 8 real-world stories of healthcare companies building on Aidbox.

- [Cleo EHR: How we built modern EHR for dermatology clinics](https://www.health-samurai.io/case-study/cleoehr) (FHIR, Case Study)
- [Prenosis: Prenosis Immunix™: First FDA-Authorized AI Sepsis Diagnostic on FHIR](https://www.health-samurai.io/case-study/prenosis-develops-immunix-for-precision-medicine-with-aidbox) (FHIR, Case Study)
- [4medica: 4medica modernizes clinical data infrastructure with Aidbox, powering next-generation healthcare solutions](https://www.health-samurai.io/case-study/4medica-modernizes-clinical-data-infrastructure-with-aidbox) (FHIR, Case Study)
- [Narus Health: Narus Health: Care Management on Aidbox FHIR](https://www.health-samurai.io/case-study/narushealth) (FHIR, Case Study)
- [MedClient EHR: Building an Inpatient EHR for a mid-size Hospital](https://www.health-samurai.io/case-study/medclient) (FHIR, Case Study)
- [Deep 6 AI: Migrating to Aidbox: How Deep 6 AI enhanced performance of its AI pipeline for clinical trial recruitment](https://www.health-samurai.io/case-study/how-deep-6-ai-enhanced-ai-pipeline-performance-for-clinical-trial-recruitment-with-aidbox) (FHIR, Case Study)
- [Keebler Health: Keebler Health: AI Risk Adjustment on Aidbox FHIR](https://www.health-samurai.io/case-study/how-keebler-health-used-aidbox-to-build-an-ai-native-risk-adjustment-platform) (FHIR, Case Study)
- [BodyLogicMD: Extending FHIR: How BodyLogicMD implemented FHIR across its multi-clinic healthcare network](https://www.health-samurai.io/case-study/bodylogicmd-implements-fhir-across-its-multi-clinic-healthcare-network-with-aidbox-fhir-server) (FHIR, Case Study)

## Blog

> 189 articles about FHIR, healthcare interoperability, and Health Samurai products.

- [All Articles](https://www.health-samurai.io/blog.md): Full article index
- [RSS Feed](https://www.health-samurai.io/blog/rss.xml)

### Recent Articles

- [Payer-to-Payer API: The Member Opt-In Is a Fall 2026 Deliverable](https://www.health-samurai.io/articles/payer-to-payer-api-member-opt-in-capture.md) (2026-09-09): The member opt-in is the only gate on the Payer-to-Payer API. What the rule requires, what to ask the member, what CMS tells you not to ask, and the four decisions to make before enrollment season closes.
- [Aidbox, Formbox & Payerbox 2608: Streaming Large Binary Files, Voice Agents, and Payer Data Integration](https://www.health-samurai.io/articles/2608-release.md) (2026-09-07): Release 2608 adds large Binary streaming, Cloud SQL IAM authentication, and scoped purge to Aidbox, plus Formbox Voice Agents and a new Payerbox data integration contract.
- [We standardized how to get health data. We never standardized what an agent may do with it.](https://www.health-samurai.io/articles/ai-agent-guardrails-for-fhir.md) (2026-08-24): FHIR settled who may read a patient's record. It never settled what an AI agent may do with that record afterward. Here is the gap, and a runnable Aidbox example that closes part of it.
- [Introduction to FHIR Terminology: The Basics](https://www.health-samurai.io/articles/introduction-to-fhir-terminology-the-basics.md) (2026-08-20): The second post in a series on FHIR terminology: coded values, code systems, value sets, and concept maps
- [Why FHIR SDC and Standard Terminologies Matter](https://www.health-samurai.io/articles/fhir-sdc-standard-terminologies.md) (2026-08-13): How FHIR SDC and standard terminologies like SNOMED CT, LOINC, and RxNorm turn digital forms into interoperable, analytics-ready healthcare data.
- [Introduction to FHIR Terminology](https://www.health-samurai.io/articles/introduction-to-fhir-terminology.md) (2026-08-11): The first post in a series on FHIR terminology, for readers new to coded values, code systems, and value sets.
- [Aidbox, Formbox & Payerbox 2607: SMART Health Cards, Binary Storage, and UM Integration](https://www.health-samurai.io/articles/2607-release.md) (2026-08-07): Release 2607 adds SMART Health Cards, validation for data already stored in Aidbox, native Binary REST handling, external base64Binary storage, and PAS routing into external utilization management systems.
- [Using FHIR as a framework for agentic coding](https://www.health-samurai.io/articles/fhir-as-a-framework-for-agentic-coding.md) (2026-08-04): 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.
- [Termbox on Databricks Lakebase](https://www.health-samurai.io/articles/termbox-on-databricks-lakebase.md) (2026-07-30): Termbox, now runs on Databricks Lakebase Postgres. Terminology stops being a service you integrate from the outside and becomes part of the platform where your analytics already lives.
- [2026 CMS HL7 FHIR Connectathon: Burden Reduction and PDex Results](https://www.health-samurai.io/articles/cms-fhir-connectathon-2026-results.md) (2026-07-28): Payerbox took part in two tracks at the 7th Annual CMS HL7 FHIR Connectathon: Burden Reduction and PDex. What we tested against live EHR and payer systems, what broke, and what we changed.
- [Mapping Kahn onto FHIR](https://www.health-samurai.io/articles/kahn-framework-fhir-validation.md) (2026-07-21): What separates good FHIR data from bad? The health-data world settled a precise vocabulary for it a decade ago — the Kahn framework. This article walks through it and maps each part onto FHIR: what the validator already covers, and what needs a whole dataset.
- [FHIR in Germany: Navigating ISiK, MII, and EHDS](https://www.health-samurai.io/articles/fhir-adoption-in-germany.md) (2026-07-21): A joint Health Samurai and Gefyra guide to the German FHIR landscape: institutions, laws, profile families, sector obligations, and what the EHDS changes for implementers.
- [Calculating CMS Quality Measures as SQL on FHIR — No CQL Engine Required](https://www.health-samurai.io/articles/cms-quality-measures-as-sql-on-fhir.md) (2026-07-20): Run CMS/HEDIS eCQMs as SQL on FHIR — no CQL engine. ViewDefinitions and SQLQuery Libraries pack into one FHIR package you can move to any SQL-on-FHIR server, making measure calculation portable and standard.
- [Reducing Complexity in Patient Intake Forms with FHIR SDC](https://www.health-samurai.io/articles/reducing-complexity-patient-intake-forms-fhir-sdc.md) (2026-07-16): How conditional logic, carefully selected required fields, collapsible sections, response amendments, and data extraction turn large patient intake forms into clear clinical workflows.
- [FHIR needs data quality profiles](https://www.health-samurai.io/articles/fhir-data-quality-sql-on-fhir.md) (2026-07-15): A green validator tells you nothing about whether a dataset is usable. OMOP solved this a decade ago with the Data Quality Dashboard. FHIR now has every piece needed to build its own — on SQLQuery plus a few extensions.
- [$batch-validate: Validate Every Stored Resource Against Its Profiles, at Scale](https://www.health-samurai.io/articles/batch-validate-existing-data.md) (2026-07-13): Aidbox 2607 replaces the old batch-validation API with the $batch-validate operation — validate a whole resource type already in your database, sync or async, and drill into exactly which resources are non-compliant and why.
- [Aidbox, Formbox & Payerbox 2606: Analytics, Bulk Export, and API Control](https://www.health-samurai.io/articles/2606-release.md) (2026-07-06): Release 2606 adds SQLView to the Analytics UI, chart visualization, _elements for bulk export, per-resource API configuration, newer Da Vinci prior auth versions, and Formbox reliability fixes.
- [Building FHIR Provider Directory for Medicare Plan Finder](https://www.health-samurai.io/articles/cms-provider-directory-fhir-pipeline.md) (2026-07-06): CMS requires Medicare Advantage plans to publish a PDex Plan-Net FHIR provider directory. With bulk $export and _typeFilter, getting the right data out takes one call, and the rest is ordinary code.
- [@atomic-ehr/codegen: US Core Profiles in Python](https://www.health-samurai.io/articles/atomic-ehr-codegen-python-us-core-profiles.md) (2026-07-03): Generate typed US Core profile classes in Python from the FHIR IG with @atomic-ehr/codegen — typed factories, extensions, slices, and profile-aware validation.
- [Performance at Scale: Baseline](https://www.health-samurai.io/articles/performance-at-scale-baseline.md) (2026-06-29): A baseline note for our performance at scale series: what we measure first, why the starting point matters, and how we keep the benchmark honest.

## News

> 89 news updates from Health Samurai.

- [Aidbox is now available on Azure Marketplace](https://www.health-samurai.io/news/aidbox-now-available-on-azure-marketplace) (2026-08-05)
- [Health Samurai Tests Payerbox at the CMS HL7 FHIR Connectathon](https://www.health-samurai.io/news/health-samurai-tests-payerbox-at-cms-hl7-fhir-connectathon-2026) (2026-07-15)
- [Health Samurai brings the Aidbox FHIR server to Databricks](https://www.health-samurai.io/news/aidbox-fhir-server-on-databricks) (2026-06-29)
- [KONZA Health and Health Samurai partner on C-CDA-to-FHIR conversion for digital HEDIS and CMS-0057-F](https://www.health-samurai.io/news/konza-health-and-health-samurai-partner-on-c-cda-to-fhir-conversion) (2026-06-23)
- [Health Samurai launches RCMbox, a self-hosted billing backend for healthcare product teams](https://www.health-samurai.io/news/health-samurai-launches-rcmbox-self-hosted-billing-backend) (2026-05-06)
- [Health Samurai Becomes an Organizational Member of HL7](https://www.health-samurai.io/news/health-samurai-becomes-organizational-member-of-hl7) (2026-04-28)
- [Health Samurai at Emids Healthcare Summit 2026: CEO Pavel Smirnov to co-moderate roundtable on interoperability](https://www.health-samurai.io/news/health-samurai-at-emids-healthcare-summit-2026) (2026-04-06)
- [HealthLX and Health Samurai partner to accelerate FHIR-based Prior Authorization and interoperability infrastructure](https://www.health-samurai.io/news/healthlx-and-health-samurai-partner-to-accelerate-fhir-based-prior-authorization-and-interoperability-infrastructure) (2026-04-01)
- [Health Samurai launches Formbox to replace paper, PDFs, and fragmented form workflows in healthcare](https://www.health-samurai.io/news/health-samurai-launches-formbox-to-replace-paper-pdfs-and-fragmented-form-workflows-in-healthcare) (2026-03-02)
- [Washington State Department of Health Partners with Health Samurai to Power Statewide Public Health Reporting with Aidbox](https://www.health-samurai.io/news/washington-state-department-of-health-partners-with-health-samurai-to-power-statewide-public-health-reporting-with-aidbox) (2026-01-26)

## Webinars & Events

> 204 talks across 11 event series.

- [SDC in Production Meetup](https://www.health-samurai.io/events/sdc-in-production-webinar): An online meetup on FHIR Structured Data Capture (SDC) in real clinical workflows. Eight talks from eleven speakers cover the spec itself, open-source tooling, voice-native and voice AI interfaces on top of SDC, and production deployments at NHS scale, in Finnish clinical decision support, German ICU and anaesthesia, and Canadian interoperability.
  - [SDC as product infrastructure in Canadian healthcare](https://www.health-samurai.io/events/sdc-in-production-webinar/08-sdc-as-product-infrastructure-in-canada)
  - [Introduction to FHIR SDC](https://www.health-samurai.io/events/sdc-in-production-webinar/01-introduction-to-fhir-sdc)
  - [Voice AI for PROMs collection](https://www.health-samurai.io/events/sdc-in-production-webinar/02-voice-ai-for-proms-collection)
  - [Voice-native smart forms and GP CCMP](https://www.health-samurai.io/events/sdc-in-production-webinar/03-voice-native-smart-forms-and-gp-ccmp)
  - [FHIR Questionnaire tools from the National Library of Medicine](https://www.health-samurai.io/events/sdc-in-production-webinar/04-nlm-questionnaire-tooling)
  - ... and 3 more talks
- [SDC Conference 2025](https://www.health-samurai.io/events/sdc-conference-2025): A virtual conference dedicated to FHIR Structured Data Capture (SDC) - standards for creating complex healthcare forms with adaptive behavior, auto-population, and clinical workflows.
  - [FHIR Questionnaire-related tools from the National Library of Medicine](https://www.health-samurai.io/events/sdc-conference-2025/08-nlm-fhir-questionnaire-tools)
  - [Usage of parameterised valueSets in Questionnaires](https://www.health-samurai.io/events/sdc-conference-2025/06-parameterised-valuesets-in-questionnaires)
  - [Low-Code FHIRPath Tools for Clinical Form Designers](https://www.health-samurai.io/events/sdc-conference-2025/07-low-code-fhirpath-tools)
  - [SDC in the real world: Capturing patient-reported outcomes](https://www.health-samurai.io/events/sdc-conference-2025/09-sdc-real-world-patient-outcomes)
  - [Introduction to FHIR SDC](https://www.health-samurai.io/events/sdc-conference-2025/01-introduction-to-fhir-sdc)
  - ... and 4 more talks
- [SQL on FHIR WG Meetings](https://www.health-samurai.io/events/sql-on-fhir): Weekly meetings of the HL7 SQL on FHIR working group — spec discussions, implementer demos, and open design questions. Recordings are public; no registration needed.
  - [SQL on FHIR WG Meeting — September 8, 2026](https://www.health-samurai.io/events/sql-on-fhir/2026-09-08-meeting)
  - [SQL on FHIR WG Meeting — September 1, 2026](https://www.health-samurai.io/events/sql-on-fhir/2026-09-01-meeting)
  - [SQL on FHIR WG Meeting — August 18, 2026](https://www.health-samurai.io/events/sql-on-fhir/2026-08-18-meeting)
  - [SQL on FHIR WG Meeting — August 11, 2026](https://www.health-samurai.io/events/sql-on-fhir/2026-08-11-meeting)
  - [SQL on FHIR WG Meeting — August 4, 2026](https://www.health-samurai.io/events/sql-on-fhir/2026-08-04-meeting)
  - ... and 121 more talks
- [SQL on FHIR Conference 2025 — Recap](https://www.health-samurai.io/events/fhir-analytics-2025): Recap and talks from the 2025 SQL on FHIR community conference. For the current state of FHIR analytics, see the [companion article](/articles/sql-on-fhir-interoperable-analytics).
  - [Natural Language Queries to SQL-on-FHIR](https://www.health-samurai.io/events/fhir-analytics-2025/05-natural-language-queries)
  - [Bridging Forms and Analytics: AI-Assisted FHIR Questionnaire and ViewDefinition Authoring](https://www.health-samurai.io/events/fhir-analytics-2025/06-bridging-forms-and-analytics)
  - [FlatQuack: FHIR Resources to SQL Tables with DuckDB](https://www.health-samurai.io/events/fhir-analytics-2025/07-flatquack-duckdb)
  - [How We Used SQL on FHIR to Shrink LLM Context by 92%](https://www.health-samurai.io/events/fhir-analytics-2025/08-shrink-llm-context)
  - [Semantic Intelligence with Kodjin: Democratizing Real-time FHIR Analytics](https://www.health-samurai.io/events/fhir-analytics-2025/09-kodjin-semantic-intelligence)
  - ... and 6 more talks
- [Agents on FHIR](https://www.health-samurai.io/events/agents-on-fhir): Weekly community calls on building AI agents on top of FHIR — demos, design arguments, and what breaks in practice. Recordings are public; no registration needed.
  - [Agents on FHIR — September 10, 2026](https://www.health-samurai.io/events/agents-on-fhir/2026-09-10-meeting)
  - [Agents on FHIR — August 20, 2026](https://www.health-samurai.io/events/agents-on-fhir/2026-08-20-meeting)
  - [Agents on FHIR — August 13, 2026](https://www.health-samurai.io/events/agents-on-fhir/2026-08-13-meeting)
  - [Agents on FHIR — August 6, 2026](https://www.health-samurai.io/events/agents-on-fhir/2026-08-06-meeting)
  - [Agents on FHIR — July 23, 2026](https://www.health-samurai.io/events/agents-on-fhir/2026-07-23-meeting)
  - ... and 17 more talks
- [FHIR Analytics 2024](https://www.health-samurai.io/events/fhir-analytics-2024): A conference about SQL on FHIR, analytics tools, and transforming FHIR data for population-level queries. See our full write-up on [interoperable FHIR analytics](/articles/sql-on-fhir-interoperable-analytics).
  - [Transforming FHIR for efficient population queries using Open Health Stack](https://www.health-samurai.io/events/fhir-analytics-2024/02-open-health-stack)
  - [FlatQuack: FHIR resources to SQL tables with DuckDB](https://www.health-samurai.io/events/fhir-analytics-2024/06-flatquack-duckdb)
  - [SQL on FHIR in PostgreSQL](https://www.health-samurai.io/events/fhir-analytics-2024/07-sql-on-fhir-postgresql)
  - [A Technical Tour of the SQL on FHIR Spec](https://www.health-samurai.io/events/fhir-analytics-2024/08-sql-on-fhir-spec-tour)
  - [FHIR Analytics & SQL on FHIR: An Introduction](https://www.health-samurai.io/events/fhir-analytics-2024/09-fhir-analytics-intro)
  - ... and 5 more talks
- [FHIR Meetup](https://www.health-samurai.io/events/fhir-meetups): Regular community meetups covering FHIR topics including security, profiling, terminology, scaling, and more.
  - [FHIR Access Control: Real-world Challenges and Solutions](https://www.health-samurai.io/events/fhir-meetups/12-fhir-access-control)
  - [FHIR Topic-based Subscriptions](https://www.health-samurai.io/events/fhir-meetups/11-fhir-topic-based-subscriptions)
  - [SMART on FHIR](https://www.health-samurai.io/events/fhir-meetups/10-smart-on-fhir)
  - [Federated FHIR: Patient Identity & Record Linkage](https://www.health-samurai.io/events/fhir-meetups/09-federated-fhir-patient-identity)
  - [FHIR at Scale](https://www.health-samurai.io/events/fhir-meetups/08-fhir-at-scale)
  - ... and 7 more talks
- [Recorded webinars](https://www.health-samurai.io/events/recorded)
  - [What's new in Aidbox — a better way to work with FHIR](https://www.health-samurai.io/events/recorded/whats-new-in-aidbox-a-better-way-to-work-with-fhir)
  - [Aidbox Product Updates | September 2023](https://www.health-samurai.io/events/recorded/02-aidbox-updates-september-2023)
  - [Aidbox Product Updates | August 2023](https://www.health-samurai.io/events/recorded/03-aidbox-updates-august-2023)
- [HL7 FHIR® Camp 2025, Portugal](https://www.health-samurai.io/events/fhir-camp-2025): HL7 FHIR® Camp 2025 reimagines the traditional conference experience.
  - [HL7 FHIR® Camp 2025, Portugal](https://www.health-samurai.io/events/fhir-camp-2025/event)
- [FHIR® Camp 2024, Portugal](https://www.health-samurai.io/events/fhir-camp-2024): FHIR® Camp 2024 is an anti-conference inspired by the early FHIR DevDays.
  - [FHIR® Camp 2024, Portugal](https://www.health-samurai.io/events/fhir-camp-2024/event)
- [FHIR Camp 2023, Portugal](https://www.health-samurai.io/events/fhir-camp-in-cascais-portugal): The first-ever offline FHIR Camp in Cascais, Portugal.
  - [FHIR Camp 2023, Portugal](https://www.health-samurai.io/events/fhir-camp-in-cascais-portugal/event)

## Downloads

- [Complete Guide to Patient Matching Models](https://www.health-samurai.io/downloads/fine-tuning-patient-matching-model): This guide shows how to configure and tune a probabilistic matching model for MDMbox on your own: from the first definition of what a “correct match” ...
- [Aidbox Brochure](https://www.health-samurai.io/downloads/aidbox-brochure): Explore Aidbox in 5 minutes. Download this brochure to get a quick overview about the Aidbox FHIR Platform applications, special discounts, profession...
- [SDC (Structured Data Capture) Guide](https://www.health-samurai.io/downloads/sdc-guide): In this guide, you will explore how SDC extends the FHIR Questionnaire to build dynamic forms featuring conditional logic, automatic pre-population, a...
- [Aidbox FHIR Platform Performance Report](https://www.health-samurai.io/downloads/aidbox-performance-report): In this report, you’ll learn typical scenarios involving data import, search, and export and explore Aidbox’s key strengths in terms of performance, r...
- [EHR Development Toolkit Brochure](https://www.health-samurai.io/downloads/ehr-development-toolkit-brochure): Download our EHR brochure and discover how Aidbox can save you up to 70% of your time when kickstarting your EHR development.
- [Aidbox for Startups Toolkit Brochure](https://www.health-samurai.io/downloads/aidbox-for-startups-toolkit-brochure): Aidbox helps digital health startups speed up development and reduce time to market. Simply delegate technical tasks to the Aidbox FHIR platform and f...
- [Healthcare Data Platform Toolkit Brochure](https://www.health-samurai.io/downloads/healthcare-data-platform-toolkit-brochure): Future-proof your healthcare solution and move to FHIR with confidence. Get a single space for your team to aggregate, store, manage, and analyze FHIR...
- [Aidbox + Cloudticity Solution Brief](https://www.health-samurai.io/downloads/aidbox-cloudticity-solution-brief): With Aidbox and Cloudticity, you can delegate infrastructure management tasks — from security, integration, management, and sharing, to cloud infrastr...
- [Standardized HL7 FHIR API for EHRs Cheatsheet](https://www.health-samurai.io/downloads/standardized-hl7-fhir-api-for-ehrs-cheatsheet): This cheatsheet is designed for the US EHR vendors who need to build a read-only HL7 FHIR® API for patient and population services and comply with §17...

---

# Aidbox FHIR Server

See [/aidbox.md](/aidbox.md) for full details.


---

# Smartbox — ONC-Certified FHIR API Tools

> ONC-certified FHIR API tools for EHR compliance and certification.

Smartbox is the ONC-certified API module built on Aidbox. It helps EHR vendors meet ONC Health IT certification requirements with production-ready FHIR APIs:

- **FHIR API** - §170.315(g)(10) Standardized API for Patient and Population Services
- **C-CDA API** - §170.315(g)(9) and §170.315(g)(6) Care Plan / Consolidated CDA
- **Search API** - §170.315(g)(7) Application access — all data request
- **Export API** - §170.315(b)(10) EHI Export (Bulk Data)
- **SMART on FHIR** - App launch, OAuth2 / OpenID Connect
- **Step-by-step certification guidance** - Documentation, test scripts, ASTB support

## Use Cases

- EHR vendors pursuing ONC Health IT certification
- Health systems integrating with certified EHRs
- Digital health apps requiring SMART on FHIR launch
- Patient-facing apps that need EHI Export

## Links

- [Smartbox product page](/smartbox)
- [Contact Sales](/contacts.md)
- [Health Samurai on YouTube](https://www.youtube.com/@HealthSamurai) — Smartbox demos and ONC certification talks


---

# Aidbox Pricing

> Flexible pricing for healthcare organizations of all sizes.

## Plans

- **Development** - Free for development and testing
- **Production** - Pay-as-you-go for production workloads
- **Enterprise** - Custom pricing with dedicated support

## Contact

[Contact our sales team](/contacts.md) for detailed pricing information.


---

# Payerbox Prior Authorization — for Delegated Entities

> FHIR-native PA channel for delegated entities serving MA, MMC, QHP, and CHIP programs.

Payerbox adds a FHIR-native prior authorization channel to your utilization management platform in 3-6 months — without changing your rules engine or workflows:

- **Keep your rules engine** - Plug FHIR PA into your existing UM stack
- **Da Vinci PAS / CRD / DTR** - Standards-based prior auth APIs
- **Multi-payer ready** - Built for delegated entities serving multiple payers
- **MA, MMC, QHP, CHIP** - Designed for delegated programs and lines of business
- **3-6 month go-live** - Pre-built mappings and integration patterns
- **Managed or self-hosted** - SaaS or deploy in your environment

## Use Cases

- Delegated UM vendors meeting payer FHIR PA requirements
- Health plans automating prior authorization with Da Vinci IGs
- Provider-side PA submission with CRD / DTR
- CMS-0057-F prior authorization mandate compliance

## Links

- [Prior Authorization product page](/prior-auth)
- [Payerbox / CMS-0057-F](/cms-0057-f.md)
- [Contact Sales](/contacts.md)
- [Health Samurai on YouTube](https://www.youtube.com/@HealthSamurai) — Payerbox PA demos and Da Vinci talks


---

# Aidbox FHIR Server

> Enterprise-grade FHIR server and healthcare development platform.

Aidbox is a FHIR-native backend for healthcare applications. It provides:

- **FHIR R4/R5 API** - Full FHIR compliance with REST API
- **SQL on FHIR** - Query FHIR data with SQL
- **SMART on FHIR** - OAuth2/OpenID Connect authentication
- **Subscriptions** - Real-time notifications
- **Bulk Data** - Large-scale data import/export
- **Terminology Services** - ValueSet, CodeSystem operations

## Use Cases

- Electronic Health Records (EHR)
- Health Information Exchange (HIE)
- Clinical Decision Support
- Patient Portals
- Research Data Platforms

## Links

- [Pricing](/price.md)
- [Documentation](/docs/aidbox)
- [Contact Sales](/contacts.md)
- [Health Samurai on YouTube](https://www.youtube.com/@HealthSamurai) — Aidbox demos and talks


---

# eRxbox — Electronic Prescriptions

> FHIR-native, certification-ready e-prescription module by Health Samurai.

eRxbox is the Aidbox E-Prescription Module. Build a compliant, scalable eRx solution in days:

- **FHIR-native** - Prescriptions, medications, and patient data modeled as FHIR resources
- **NCPDP compliance** - SCRIPT 2017071 standard support
- **Prescription network integration** - NewRx, RxRenewal, RxChange, CancelRx, RxFill
- **Certification-ready** - Pre-validated mappings and workflows
- **EPCS support** - Controlled substance prescribing with DEA-compliant authentication
- **Pharmacy directory** - national pharmacy directory lookup and selection

## Use Cases

- EHRs adding e-prescription capability
- Telehealth platforms with prescriber workflows
- Digital health apps for chronic care
- Clinical applications requiring e-prescription network connectivity

## Links

- [eRxbox product page](/erxbox)
- [Contact Sales](/contacts.md)
- [Health Samurai on YouTube](https://www.youtube.com/@HealthSamurai) — eRxbox demos and talks


---

# Case Studies

> See how healthcare organizations use Aidbox.

Read our [blog](/blog.md) for detailed case studies and technical articles.


---

# Interbox — Healthcare Integration Engine

> Durable, queue-driven runtime for modern healthcare integrations. HL7v2 modernization, custom workflows, FHIR delivery to Aidbox.

Interbox orchestrates healthcare data with a reliable, queue-driven runtime. It provides:

- **HL7v2 modernization** - Ingest HL7v2, transform to FHIR, route to Aidbox or other targets
- **Durable workflows** - Queue-driven runtime with retries, idempotency, and observability
- **Custom pipelines** - Author transforms and routes for any healthcare data shape
- **FHIR delivery** - Native pipeline target for Aidbox FHIR server
- **Pluggable adapters** - SFTP, MLLP, HTTP, S3, message queues
- **Observable by default** - Metrics, traces, and dead-letter queues built in

## Use Cases

- Hospitals modernizing HL7v2 feeds into FHIR
- Digital health platforms ingesting from multiple EHRs
- Population health pipelines aggregating clinical data
- HIE and data exchange networks
- Custom ETL between legacy systems and FHIR

## Links

- [Interbox product page](/interbox)
- [Aidbox FHIR Server](/aidbox.md)
- [Contact Sales](/contacts.md)
- [Health Samurai on YouTube](https://www.youtube.com/@HealthSamurai) — Interbox demos and HL7v2 modernization talks


---

# RCMbox — Headless Revenue Cycle Management

> Headless RCM backend for healthcare billing teams scaling operations across states and payers.

RCMbox is a headless revenue cycle management platform. Go live faster and expand into new states and payers with a reusable backend:

- **Modular workflow building blocks** - Compose claims, eligibility, ERA, and posting workflows
- **Claims lifecycle orchestration** - 837/835 generation, submission, status, denials
- **Remittance and ERA foundation** - ERA parsing, posting rules, denial workflows
- **Routing and integration layer** - Clearinghouse, payer, and EHR connectors
- **Headless / API-first** - Bring your own UI; integrate with existing billing tools
- **Multi-state, multi-payer** - Designed for organizations expanding across markets

## Use Cases

- Digital health companies scaling billing across states
- Multi-specialty groups consolidating RCM operations
- Care models that don't fit off-the-shelf RCM tools
- Organizations replacing engineering-heavy internal billing stacks

## Links

- [RCMbox product page](/rcmbox)
- [Contact Sales](/contacts.md)
- [Health Samurai on YouTube](https://www.youtube.com/@HealthSamurai) — RCMbox demos and billing talks


---

# Payerbox — CMS-0057-F Compliance

> All four CMS-0057-F FHIR APIs, pre-built on Da Vinci IGs. Managed or self-hosted.

Payerbox is a turnkey CMS-0057 / CMS-0057-F compliance platform for health plans. It delivers the four mandated FHIR APIs:

- **Prior Authorization API** - Da Vinci PAS / CRD / DTR for prior auth automation
- **Patient Access API** - Patient claims, encounters, and clinical data
- **Provider Access API** - Provider access to attributed member data
- **Payer-to-Payer API** - Payer-to-payer member history exchange
- **Da Vinci IG conformance** - Pre-built on US Core, CARIN BB, PDex, PAS
- **Managed or self-hosted** - SaaS or deploy in your environment

## Use Cases

- Health plans meeting CMS-0057-F (former CMS-0057) requirements
- Medicare Advantage, Medicaid, CHIP, and QHP plans
- Payer-to-payer data exchange initiatives
- Provider directory and attribution workflows

## Links

- [Payerbox / CMS-0057-F product page](/cms-0057-f)
- [Prior Authorization for delegated entities](/prior-auth.md)
- [Contact Sales](/contacts.md)
- [Health Samurai on YouTube](https://www.youtube.com/@HealthSamurai) — Payerbox demos and CMS-0057 talks


---

# Termbox — FHIR Terminologies

> Production-grade FHIR terminology server. Manage SNOMED CT, LOINC, RxNorm, ICD-10, and custom code systems with a standards-compliant API.

Termbox is a SaaS terminology service from Health Samurai. It provides:

- **Standard code systems** - SNOMED CT, LOINC, RxNorm, ICD-10, CPT, and more, pre-loaded and kept up to date
- **Custom code systems** - Upload and version your own CodeSystems and ValueSets
- **FHIR Terminology API** - `$lookup`, `$validate-code`, `$expand`, `$translate`, ConceptMap operations
- **High performance** - Fast ValueSet expansion at scale
- **SaaS or self-hosted** - Managed service or deployed in your environment

## Use Cases

- Clinical decision support
- EHR and clinical applications
- Population health and analytics
- Quality measures and reporting
- Interoperability and HIE

## Links

- [Termbox product page](/termbox)
- [Contact Sales](/contacts.md)
- [Health Samurai on YouTube](https://www.youtube.com/@HealthSamurai) — Termbox demos and talks


---

# Contact Health Samurai

> Get in touch with our team.

## Sales Inquiries

Email: hello@health-samurai.io

## Technical Support

- Documentation: /docs/aidbox
- Community: https://t.me/aidbox

## Office

Health Samurai, Inc.


---

# Aidbox Forms

> FHIR SDC-compliant healthcare form builder.

Aidbox Forms enables healthcare organizations to:

- Build FHIR Questionnaires visually
- Render forms for patients and clinicians
- Extract structured data as FHIR resources
- Support complex logic and calculations

## Features

- Drag-and-drop form builder
- FHIR SDC compliance
- Conditional logic
- Scoring and calculations
- PDF generation
- Multi-language support

## Links

- [Documentation](/docs/formbox/getting-started)
- [Contact Sales](/contacts.md)
- [Formbox / Aidbox Forms on YouTube](https://www.youtube.com/@HealthSamurai) — walkthroughs and feature updates


---

# MDMbox — Master Patient Index & MDM

> FHIR-native Master Data Management (MDM) and Enterprise Master Patient Index (eMPI) software for healthcare.

MDMbox deduplicates patient records and creates a single source of truth across EHRs, LIS, telehealth platforms, and claims systems. It provides:

- **Probabilistic matching** - Configurable algorithms tuned for healthcare identifiers
- **Configurable merge logic** - Survivorship rules, golden record assembly, manual review queues
- **FHIR-native** - Standard FHIR API for import, query, and Patient `$match`
- **Cross-source identity** - Merge records across EHR, LIS, telehealth, and claims systems
- **Audit trail** - Full provenance for every merge and unmerge decision

## Use Cases

- Health systems consolidating patient data
- Payers building member-360 records
- HIEs and data exchange networks
- Digital health platforms with multi-source ingestion
- Population health and analytics

## Links

- [MDMbox product page](/mdmbox)
- [Contact Sales](/contacts.md)
- [Health Samurai on YouTube](https://www.youtube.com/@HealthSamurai) — MDMbox demos and talks


---

# About Health Samurai

> Building FHIR-native healthcare infrastructure since 2014.

Health Samurai is a healthcare technology company focused on FHIR interoperability. We build Aidbox, an enterprise FHIR server used by healthcare organizations worldwide.

## Mission

Make healthcare data interoperable through FHIR standards.

## Official YouTube

- [YouTube @HealthSamurai](https://www.youtube.com/@HealthSamurai) — demos, webinars, and event recordings.

## Products

- [Aidbox FHIR Server](/aidbox.md)
- [Aidbox Forms](/medical-form.md)

## Contact

- Website: https://health-samurai.io
- Email: hello@health-samurai.io


---

---
{
  "title": "Payer-to-Payer API: The Member Opt-In Is a Fall 2026 Deliverable",
  "description": "The member opt-in is the only gate on the Payer-to-Payer API. What the rule requires, what to ask the member, what CMS tells you not to ask, and the four decisions to make before enrollment season closes.",
  "date": "2026-09-09",
  "author": "Mike Kulakov",
  "reading-time": "14 min read",
  "tags": [
    "Compliance",
    "Integrations"
  ],
  "tldr": "Nothing moves through the Payer-to-Payer API until the member opts in, and you collect that opt-in by asking the member, not through an API. Medicare Annual Enrollment for 2027 closes December 7, 2026, and the duty starts January 1, 2027, which makes the capture path a fall 2026 deliverable. FHIR is the format of the request, not the format of the capture, so you can start collecting in the systems you already run. Here is what to ask, what CMS tells you not to ask, and the four decisions to make first.",
  "utm-campaign": "fhir_expert",
  "utm-content": "p2p-opt-in"
}
---
## The gate on the Payer-to-Payer API

When a member leaves one plan for another, the Payer-to-Payer API is how their history follows them. The new plan asks the old one for up to five years of claims, clinical data and prior authorizations. Nothing moves until the member says yes. That permission is the entire gate, and no API collects it for you. This article is about that gate, and it is written for Medicare Advantage; for the rest of the rule, start with our [CMS-0057-F overview](/articles/understanding-the-cms-0057-f-interoperability-and-prior-authorization-final-rule).

Right now the calendar matters more than the engineering. Medicare Annual Enrollment for 2027 runs October 15 through December 7, 2026, a window [fixed in regulation](https://www.ecfr.gov/current/title-42/section-422.62). Coverage for everyone who signs up begins January 1, 2027, the day the Payer-to-Payer duty takes effect under [42 CFR 422.121](https://www.ecfr.gov/current/title-42/section-422.121). By then every existing member is owed the chance to opt in, and everyone who joined during Annual Enrollment is owed the same offer within a week of coverage starting.

Both offers are discharged by talking to the member. The conversation has to come back with a specific set of elements, among them who the previous insurer was, under whose name that coverage sat, and the permission itself. Collecting it is what this article calls capture. A plan that reaches January without a way to do it has converted the work into a backfill campaign across the whole membership. That campaign runs in the same weeks as the API go-live.

## What the rule requires

For Medicare Advantage the duties sit in the Payer-to-Payer half of [42 CFR 422.121](https://www.ecfr.gov/current/title-42/section-422.121). Medicaid, CHIP and the QHP issuers on the federally facilitated exchanges each get their own version of the same section. Medicaid and CHIP differ in three ways worth knowing. There the State collects the opt-in rather than the plan. An exchange between a State and its own contracted plans needs no opt-in at all. And managed care plans pick the duty up through their State contract rather than directly from CMS.

For a Medicare Advantage plan the duty runs like this. Offer the opt-in and a way to change it, and start identifying the member's previous and concurrent payers, both no later than a week after coverage starts. Make reasonable efforts when the member does not respond. Then, once you hold the permission and enough identifying information, send the data request within a week, attesting that the member is enrolled and has opted in. Repeat it to concurrent payers quarterly while the member is in both plans. Explain all of it in plain language when you ask, once a year after, and in an accessible place on your public website. What you request is the Patient Access data set from the last five years, minus provider remittances, member cost sharing and denied prior authorizations, plus the unstructured documentation attached to prior authorizations.

Three points are easy to read the wrong way.

**The first week runs on asking, not on getting an answer.** The deadline is to put the question to the member, not to have their answer in hand. CMS proposed "at enrollment" and finalized "start of coverage" because, as it put it, payers might not have contact with patients before enrollment. The [preamble to the final rule](https://www.federalregister.gov/documents/2024/02/08/2024-00895/medicare-and-medicaid-programs-patient-protection-and-affordable-care-act-advancing-interoperability) adds that collection "may take longer than the enrollment process". So measure the share of new members you asked inside seven days, not the share who answered. The first is your process, the second is member behavior. A dashboard built on the second reads red every month while telling you nothing.

**The second week is the one that can fail quietly.** It does not start until you hold both the opt-in and enough information to identify the other payer, and from there the request has a week to go out. That is latency inside your own queue, the only deadline here your systems can miss with no person involved, and it deserves a monitor.

**The rule does not tell you where to ask.** CMS declined to prescribe a process and pointed instead at "an already established point of contact with the patient". So the opt-in does not have to sit on the enrollment application. For Medicare Advantage the deadline does not assume it will, since the clock runs from the start of coverage rather than from enrollment. The welcome call, the ID card mailing, the first portal login and a service script are all available to you. Pick the touchpoint you own and can instrument.

## Where FHIR starts, and where it does not

CMS did not write a technical standard for this. The rule requires FHIR R4, US Core and the FHIR Bulk Data specification, by pointing at [the API standards ONC has adopted](https://www.ecfr.gov/current/title-45/section-170.215). It recommends the Da Vinci PDex implementation guide without requiring it. So PDex conformance is a trading-partner agreement rather than something CMS checks. What PDex adds is the shape of the exchange, and that shape is what tells you which elements you have to collect.

The requesting payer starts with `$bulk-member-match` and submits, per member, patient demographics, the prior coverage and the permission, on the HRex Patient Demographics, Coverage and [Consent](https://hl7.org/fhir/us/davinci-hrex/STU1.1/StructureDefinition-hrex-consent.html) profiles. The responder evaluates each member on its own and returns three groups: matched, not matched, and consent-constrained, that last one being members it found but whose consent it cannot honor. The matched group feeds `$davinci-data-export` ([PDex bulk exchange](https://hl7.org/fhir/us/davinci-pdex/STU2.2/payertopayerbulkexchange.html)).

**FHIR is the format of the request, not the format of the capture.** Nothing in the rule or the guides requires a FHIR Questionnaire to ask the questions or a FHIR server to hold the answers. PDex makes storing the Consent record optional even for the receiver. It checks only four things to validate a request: that the member matched, that the responder can honor the scope the member chose, that the consent period is valid, and that the payer asking is the payer named in the consent. On the requesting side there is no storage requirement at all. What you owe is a conformant HRex Patient, Coverage and Consent at the moment the request goes out, and everything on the near side of that transformation is yours to design.

Connectivity is not a prerequisite either. The member reads a payer's name off a card, but the exchange needs an organization with an identifier and a live endpoint. There is no national payer identifier and no national directory to get from one to the other. CMS says payers would likely have to contact the previous payer directly to find out whether it supports the API at all. A [proposed rule from April 2026](https://www.govinfo.gov/content/pkg/FR-2026-04-14/pdf/2026-07205.pdf#page=7) would start closing that gap, requiring payers to report their Payer-to-Payer and other API endpoints to CMS within 60 days of a final rule. The mechanism is still to be determined, so treat it as a direction of travel rather than something to plan against. Either way it is an argument for collecting elections now, because they turn into requests as endpoints appear.

## Should you capture in FHIR anyway?

Not required is not the same as not worth it. FHIR-native capture can take either of two shapes. If the page the member sees is yours, it can write the Consent itself when they submit, and no form engine is involved. If you want the form to be data as well, the member-facing form is a FHIR Questionnaire, the answers land as a QuestionnaireResponse, and the Consent is derived from that. Two arguments carry both.

**You need a FHIR-readable consent store on the serving side regardless.** Provider Access lets you answer a provider only if the member has not opted out, and on the Payer-to-Payer responding side you validate the consent that arrives with each request. Your APIs already read a consent store on every call. Capture somewhere else and you own a synchronization job. There is also the evidence problem. HRex Consent wants `source` pointing at a DocumentReference, the record of what the member agreed to rather than a yes-or-no flag. A QuestionnaireResponse is already that record, timestamped and tied to the question set and its version.

**A Questionnaire does not need the vendor's release slot.** The case against FHIR-native is convenience: members are already in the enrollment vendor's flow, so adding two questions there must be faster. That holds only if the vendor can ship it, and adding a scope choice, a representative path and a signature to a member-facing flow is not a small change request. Ownership is what decides this. A portal you control can write the Consent on submit. A form you host can be pointed at from the welcome email, the portal, an SMS, or a link a representative reads out. A portal you do not control will do neither without the same change request. The touchpoint stays where it was either way; what changes is who can fix the wording in November.

The real argument against is migration. If you already run a consent table the business treats as the system of record, going FHIR-native means moving it. Worth separating out, because the capture channel, the form and the system of record are three decisions rather than one.

## Four decisions to make before enrollment season

**1. Where capture happens, and what holds the answer afterwards.** The channel is where the question reaches the member: the enrollment vendor, the portal, the service desktop, or paper. The system of record is whatever your APIs read once the member has answered, and it does not have to be the same system. If permissions already sit in a table you run, that table is a system of record you inherited rather than a channel, and the channel is whatever fills it. Neither decision has to wait for the API build. Whatever you pick, add a route for members who are not on a portal. CMS "strongly recommend[s] that there be a way for patients to record their permission telephonically or otherwise". The language and disability access duties in [45 CFR part 92](https://www.ecfr.gov/current/title-45/part-92) apply to the form and the script.

**2. What the consent period is.** CMS says the election is "valid indefinitely with that payer" until the member withdraws it, but HRex Consent makes both a start date and an end date mandatory. So the end date is your policy rather than a member answer, and it has to appear on the form the member signs.

**3. Which scope value the consent carries, and whether the member picks it.** HRex recognizes two policy values whose names run opposite to the intuition: `#sensitive` grants everything including what law treats as sensitive, `#regular` grants everything except that. You do not have to put the choice in front of the member, because the rule asks for a yes or a no and scope is not among the things you must explain.

What you cannot skip is the decision, because the value still travels in the outgoing Consent. A form that does not ask fixes it by policy, and that is a legal question rather than a design one. Sending `#sensitive` asserts the member authorized disclosure of records protected under 42 CFR part 2 and state law. The rule permits the exchange only where the disclosure is not prohibited by other law. How much data actually reaches you pulls the other way. A responder without security labels cannot separate the sensitive subset, so PDex has it exclude the member rather than filter. The member who asked for less receives nothing, while the member who said everything gets a full history.

**4. Which PDex version your stored records are shaped for.** A single enrollment contact can collect the Payer-to-Payer opt-in and the Provider Access opt-out together, so this decision covers both records. PDex 2.2.0 adds a category coding that identifies which API each consent record belongs to, mandatory on the Provider Access consent profile, widens the signer to personal representatives and adds three search parameters. Payer-to-Payer records are unaffected by that, because they use HRex Consent and that profile is identical in both releases. For Provider Access, shape what you store to 2.2.0 rather than to 2.1: 2.2.0 is the current release, and records written to the 2.1 shape this autumn buy a migration. CMS's own recommendation still names PDex 2.0.0, so this is a bet on the guide rather than on the rule. Worth checking separately: both consent profiles reference a US Core 7.0.0 Patient, while the rule itself points at US Core 3.1.1. The version your consent records assume may not be the version the rest of your build targets.

## What to ask the member

"Required" means the profile is invalid without it. "Must support" means the receiving system has to be able to process the element if you send it, which is not the same as an obligation to ask the member for it. "Recommended" marks an element CMS names in the preamble that the profile does not constrain at all.

<div class="narrow">

| What to capture | Why | Where it lands |
|---|---|---|
| Legal name | required, for member matching | `Patient.name.family`, `.given` |
| Date of birth | required, for member matching | `Patient.birthDate` |
| Sex, and birth sex where you hold it | must support, for member matching | `Patient.gender`, `us-core-birthsex` |
| Address | must support, for member matching | `Patient.address` |
| Phone | recommended: CMS names it among the elements appropriate to identify a patient | `Patient.telecom` |
| Previous insurer, as printed on the card | required, identifies who to ask | `Coverage.payor` to Organization |
| Member ID from that card | must support: CMS names this as the identifier to collect | `Coverage.identifier` (member number), `Coverage.subscriberId` |
| Whether that plan was in the member's own name or a spouse's or parent's | required: the ID on a card is often the subscriber's, not the patient's | `Coverage.relationship`, `dependent`, `policyHolder` |
| Any other plan, current or within five years | required by the rule: you have to find every previous and concurrent payer, and re-ask concurrent ones quarterly | one Coverage per payer |
| The permission itself | required, the gate on the whole API | `Consent.provision.type` = permit |
| Scope: everything, or everything except what law treats as sensitive | required, the only sensitivity control the profile has | `Consent.policy.uri`, bound to the two HRex values |
| Who answered, and a representative's relationship and basis of authority | required: HIPAA makes you [treat a representative as the member](https://www.ecfr.gov/current/title-45/section-164.502) and [verify their authority](https://www.ecfr.gov/current/title-45/section-164.514) | `Consent.performer` |
| The retained record of what was signed or read out | required: the profile wants evidence, not a flag | `Consent.source` to DocumentReference |
| The consent period | required, but you supply it: decision 2 above | `Consent.provision.period` |
| Which organization discloses and which receives | required, but you supply it: your identity plus the previous payer's | `Consent.provision.actor` |

</div>

That is the whole of it. Every element HRex Consent requires either appears above or is fixed by the profile itself. The profile sets the status, the scope, the disclosure category, the permit, the disclose action and the two actor roles for you. The consent period, the two organization actors and the retained record come from your own configuration rather than from the member, and the signed artifact can stay wherever your documents live today. Only one non-obvious question here can be answered by nobody but the member: whether the previous coverage was in their own name.

## What not to ask

Three of the four below come from the preamble. The fourth is a design choice that follows from it.

- **Coverage start and end dates.** CMS allows that they "may be useful in some instances", then discourages them: "patients are unlikely to know or remember those exact dates, nor are they likely easy to find." A required date field buys little matching accuracy and costs completion.
- **Recent services and their dates.** Discouraged as burdensome, since the member would have to get them from the payer you are about to ask.
- **Social Security numbers.** To be used to identify patients "only when necessary (and permissible by law)".
- **The specific plan name.** Commenters urged CMS not to require it: plan names are long, unintuitive, and members switch plans while staying with the same payer. CMS did not rule either way, so dropping it is a design choice. What routing needs is the payer, not the product.

What CMS does endorse is short: the patient's name, member ID, date of birth, physical address and phone number, plus the previous payer's name and a patient ID number or similar identifier.



<div class="narrow">

![Member-facing consent form headed ABC Health Plan: Payer-to-Payer Data Exchange Consent. It collects the member's first and last name separately, date of birth and current member ID; the previous insurance company name as printed on the card, the member ID with that plan, and whether that plan was in the member's own name or a spouse's, parent's or someone else's; a choice between sharing all health information including sensitive records or only non-sensitive information; and a signature block asking whether the member or an authorized representative is completing it.](p2p-member-consent-form.png "One implementation of the capture step, not a requirement of the rule. Fictional plan, no real member data.")

</div>

## When the member does not answer, or changes their mind

Withdrawal only works forward. It stops future requests, the quarterly exchange with concurrent payers included, but nothing asks you to delete what you already received. Left alone the election stands indefinitely with that payer and does not travel: the next plan collects its own. Nor does revocation travel on the wire, so if a member calls the previous payer directly, both sides need an operator path for a permission change learned by phone.

Reasonable efforts have a floor: CMS recommends following up once before concluding the member is choosing not to opt in, and encourages giving them a way to decline so the follow-up stops. Record the attempts and the declines, or you cannot defend either.

## One conversation, two records

The Provider Access opt-out notice carries the same trigger, a week after coverage starts, which is what lets one contact carry both and turns two outreach programs into one.

The records stay separate and are shaped differently: the PDex provider consent profile leaves the end date and the source document optional, while HRex Consent requires both, plus two organization actors. The category coding in PDex 2.2.0 keeps them separable in one store. Neither flag may ever gate a Patient Access response, where the member's own app authorization is the permission event.

## How Health Samurai helps

Payerbox is Health Samurai's CMS-0057-F platform for health plans, and it is live with payers today. The design principle is integrate, do not replace, and the consent capture module we are building now follows it. You keep asking members where you already reach them, and the consent table you already run stays the system of record.

See the full solution on the [Payerbox page for CMS-0057-F](/cms-0057-f). To work through your own capture path before enrollment season closes, [book a call](/contacts). We will look at where your opt-in lives today, what your form has to collect, and what it takes to turn that into a conformant request against whatever your trading partners stand up.


---

---
{
  "title": "Aidbox, Formbox & Payerbox 2608: Streaming Large Binary Files, Voice Agents, and Payer Data Integration",
  "description": "Release 2608 adds large Binary streaming, Cloud SQL IAM authentication, and scoped purge to Aidbox, plus Formbox Voice Agents and a new Payerbox data integration contract.",
  "date": "2026-09-07",
  "author": "Valeria Fursa",
  "reading-time": "5 min read",
  "tags": ["Aidbox", "Forms", "Integrations"],
  "utm-campaign": "release",
  "utm-content": "2608-release"
}
---

Release 2608 brings large Binary streaming, Google Cloud SQL IAM authentication, and scoped purge operations to Aidbox. It introduces Formbox Voice Agents for automated patient calls and a Payerbox data contract for inbound integrations, alongside updates to payer interoperability, prior authorization, analytics, and provider directory publishing.

## Large Binary Files and Cloud Deployment

Aidbox can [stream large Binary files](https://www.health-samurai.io/docs/aidbox/api/rest-api/other/binary#streaming-large-files) between a client and external blob storage when offload is configured for the `data` element. It keeps only a small buffer in memory, so files can be larger than the memory available to the instance.

The [`blobNamePrefix`](https://www.health-samurai.io/docs/aidbox/configuration/storage-and-api-configuration/offload-base64binary-to-external-storage) parameter writes offloaded files as `{prefix}/{uuid}`. The prefix can separate environments or tenants in a shared Azure, AWS, or GCP bucket.

[Storage and API configuration](https://www.health-samurai.io/docs/aidbox/configuration/storage-and-api-configuration) can be provisioned through an [init bundle](https://www.health-samurai.io/docs/aidbox/configuration/init-bundle).

The [Cloud SQL Java Connector integration](https://www.health-samurai.io/docs/aidbox/tutorials/other-tutorials/how-to-run-aidbox-with-cloud-sql-java-connector) connects Aidbox to Google Cloud SQL for PostgreSQL through the Cloud SQL JDBC socket factory, with IAM database authentication in place of a stored database password.

## Subscription Event Delivery

Aidbox can copy a request [correlation id into a topic-based subscription notification](https://www.health-samurai.io/docs/aidbox/modules/topic-based-subscriptions/aidbox-topic-based-subscriptions#correlation-id). The header name is configured with `module.topics.correlation-id-header`. NATS destinations receive the value as a native message header.

For Kafka, [`keyByResourceId`](https://www.health-samurai.io/docs/aidbox/tutorials/subscriptions-tutorials/kafka-aidboxtopicdestination) uses the FHIR resource id as the message key. Events for the same resource go to the same partition and keep their order. Webhook destinations remain immutable in general, but the [`endpoint` of a `webhook-at-least-once` destination can be updated](https://www.health-samurai.io/docs/aidbox/tutorials/subscriptions-tutorials/webhook-aidboxtopicdestination#update-the-endpoint) when it is the only changed parameter.

## Data Cleanup and API Behavior

[Organization `$purge`](https://www.health-samurai.io/docs/aidbox/access-control/authorization/scoped-api/organization-based-hierarchical-access-control/organization-purge) deletes selected or all data belonging to an organization and its nested organizations. [Group `$purge`](https://www.health-samurai.io/docs/aidbox/api/bulk-api/group-purge) works at the patient level: it deletes every Patient member of a Group together with the resources in each patient's compartment, either synchronously or asynchronously. Authorization is checked for every member before deletion begins, so a single denial leaves the group unchanged.

Other Aidbox changes:

- Create and update responses return an absolute FHIR `Location` header. Set [`fhir.location-header-compliant-mode`](https://www.health-samurai.io/docs/aidbox/reference/all-settings#fhir.location-header-compliant-mode) to `false` to restore the previous relative form.
- New settings control [search parameter usage statistics](https://www.health-samurai.io/docs/aidbox/deployment-and-maintenance/indexes/search-parameter-usage-stats#configuration). [`fhir.search.param-stats.enabled`](https://www.health-samurai.io/docs/aidbox/reference/all-settings#fhir.search.param-stats.enabled) controls collection, while [`fhir.search.param-stats.flush-interval`](https://www.health-samurai.io/docs/aidbox/reference/all-settings#fhir.search.param-stats.flush-interval) sets how often buffered samples are written to PostgreSQL. Both support hot reload.
- [GraphQL references to contained resources](https://www.health-samurai.io/docs/aidbox/api/graphql-api#contained-references) resolve inline through the `resource` field instead of returning `null`.
- Fixes are included for Aidbox UI, Multibox, and trace delivery through the OTEL connector.

For batch and transaction requests, [`Prefer: return=minimal`](https://www.health-samurai.io/docs/aidbox/api/batch-transaction#control-the-response-size) now returns a response bundle without resource bodies, matching HAPI. Entries retain `response.status`, `location`, `etag`, and `lastModified`; failed entries retain their `OperationOutcome`. Clients that need the previous empty-body behavior should use `Prefer: return=hs-headers-only`. Single-resource endpoints are unchanged.

## Voice Agents and the New Formbox UI

Release 2608 brings two major changes to Formbox. [Voice Agents](https://www.health-samurai.io/docs/formbox/voice-agents) turn existing forms into automated patient voice calls: an agent asks the form's questions, captures the responses, and uses the form's existing content as the basis for the conversation. The [new Formbox UI](https://www.health-samurai.io/docs/formbox/aidbox-forms-interface) is now the default, with a cleaner, more streamlined way to work with forms; the legacy interface remains available for teams that need it.

Formbox also gets several smaller improvements this release: textareas in long forms keep a stable scroll position while users type, dates entered through an embedded iframe renderer are preserved correctly, custom attributes with boolean, integer, and decimal values are handled correctly on save, and larger, more resource-intensive PDF forms can be imported and converted into Questionnaire JSON.

## A Published Data Contract for Payerbox

The [Data Integration Reference](https://www.health-samurai.io/docs/payerbox/data-integration) defines the inbound data contract for Payerbox. It covers 24 [USCDI v3.1 datasets](https://www.health-samurai.io/docs/payerbox/data-integration/uscdi) mapped to US Core 6.1.0 and four [Provider Directory datasets](https://www.health-samurai.io/docs/payerbox/data-integration/provider-directory) mapped to Plan-Net 1.2.0. Each dataset has a downloadable CSV template. Coded columns link to their value sets, such as the [OMB Ethnicity Categories ValueSet](https://healthsamurai.github.io/fhir-valueset-viewer/#url=http://hl7.org/fhir/us/core/ValueSet/omb-ethnicity-category).

Payerbox targets CARIN Blue Button 2.1.0 and PDex Plan-Net 1.2.0, replacing versions 2.0.0 and 1.1.0. CARIN BB 2.1.0 adds the Non-Financial Basis profiles used in Payer-to-Payer and Provider Access exports. Supported versions are listed on the [Implementation Guides](https://www.health-samurai.io/docs/payerbox/api-reference/implementation-guides) page.

## Interoperability and Prior Authorization Updates

For Payer-to-Payer and Provider Access, [`$davinci-data-export`](https://www.health-samurai.io/docs/payerbox/api-reference/operations/davinci-data-export) accepts kick-off parameters on the query string and returns `400` for an inverted `_since` and `_until` window. [`$bulk-member-match`](https://www.health-samurai.io/docs/payerbox/api-reference/operations/bulk-member-match) and [`$provider-member-match`](https://www.health-samurai.io/docs/payerbox/api-reference/operations/provider-member-match) no longer fail when a `MemberBundle` references resources that exist only on the requesting side. The member-match change requires `BOX_FHIR_VALIDATION_SKIP_REFERENCE=true`, as described in the [deployment guide](https://www.health-samurai.io/docs/payerbox/run-payerbox/deploy).

Payerbox rejects updates to a denied prior authorization regardless of how the utilization management system wrote the decision back; the [`Claim/$submit` update flow](https://www.health-samurai.io/docs/payerbox/api-reference/operations/claim-submit#update-flow) describes the behavior. [`$submit-attachment`](https://www.health-samurai.io/docs/payerbox/api-reference/operations/submit-attachment) uses the PAS `PASTempCodes` code system for `supportingInfo.category`. The previous system URL did not exist in the implementation guide and failed terminology validation.

For CRD, [`order-sign`](https://www.health-samurai.io/docs/payerbox/api-reference/operations/cds-hook-order-sign), [`order-dispatch`](https://www.health-samurai.io/docs/payerbox/api-reference/operations/cds-hook-order-dispatch), and [`appointment-book`](https://www.health-samurai.io/docs/payerbox/api-reference/operations/cds-hook-appointment-book) return a Coverage Information system action when coverage cannot be determined. Along with the explanatory card, the action annotates the draft order with `covered=conditional`, `info-needed=OTH`, and a human-readable reason, as required by Da Vinci CRD 2.1.0. [`order-select`](https://www.health-samurai.io/docs/payerbox/api-reference/operations/cds-hook-order-select) remains unchanged and returns the card only.

## PAS Analytics and MPF Publishing

The PAS metrics package, `io.healthsamurai.pas-metrics` 0.1.6, is available for download with an Aidbox Notebook that charts each metric. Package details are available in the [PAS Metrics documentation](https://www.health-samurai.io/docs/payerbox/analytics/pas-metrics).

The MPF provider directory pipeline publishes data by contract year. `InsurancePlan.period` carries the published year, and providers that are not in network during that year are excluded. Network scope is derived from the configured plans on every run: administrators configure `InsurancePlan` ids, and the network `Organization` ids come from each plan's `network[]`. The **Network Organization IDs** setting has been removed, and previously stored values are ignored. The [MPF Publications](https://www.health-samurai.io/docs/payerbox/fhir-app-portal/mpf-publications) documentation describes the updated flow.

## Read the Full Release Notes

This post covers the main changes across the three products. For the complete changelog and configuration details, read the release notes for:

- [Aidbox 2608](https://www.health-samurai.io/docs/aidbox/overview/release-notes#august-2026-latest-2608)
- [Formbox 2608](https://www.health-samurai.io/docs/formbox/release-notes)
- [Payerbox 2608](https://www.health-samurai.io/docs/payerbox/releases#august-2026-2608)


---

---
{
  "title": "We standardized how to get health data. We never standardized what an agent may do with it.",
  "description": "FHIR settled who may read a patient's record. It never settled what an AI agent may do with that record afterward. Here is the gap, and a runnable Aidbox example that closes part of it.",
  "date": "2026-08-24",
  "author": "Eugene Vestel",
  "reading-time": "12 min read",
  "tags": ["AI / Agents", "Compliance", "FHIR Standard", "Aidbox"],
  "tldr": "FHIR's authorization model answers access questions: may this client read this resource? It has no vocabulary for use questions — may the reader keep a copy, send it to a model provider, train on it, act on an inference. Prompts are not enforcement, so the controls have to run on the server: redact on read, audit every call, step up on writes, and require out-of-band approval for anything irreversible. HealthClaw Guardrails is an MIT-licensed proxy that implements those four controls, and this post walks through running it in front of Aidbox.",
  "utm-campaign": "ai",
  "utm-content": "agent-guardrails"
}
---

A woman I know has three chronic conditions, four prescribers, and records in six systems. Before every new appointment she does the same work: log into three portals, screenshot her medication list, retype her history onto a clipboard form, and hope the front desk enters it correctly. She has done this for eleven years. She is not a bad patient. She is doing unpaid integration labor because nobody else in the system is positioned to do it.

That is the bottleneck. Not diagnosis, not access to data in the legal sense. The patient is the only party who has the whole picture and the only party with no tooling to act on it.

An AI agent is the first thing that plausibly closes that gap. It can read across six systems, reconcile a medication list, fill the form, and chase the referral. This is not speculative. Connect an MCP client to a FHIR server, hand it a base URL and a token, and the demo works on the first try.

Which is exactly the problem. The demo works, and nothing in the stack tells the patient what that agent may do with what it just read.

## What FHIR was designed to answer, and what it wasn't

FHIR's authorization model was built for a world where the client was an application and a human sat behind it. That world started around 2011, hardened through Argonaut and SMART on FHIR, and became law through the 21st Century Cures API rules. It solved a real problem, and it solved it well. A patient can now get their data.

Look at what the scopes actually say. `patient/Observation.rs` means this client may read and search Observations for this patient. That is an access question, and FHIR answers it precisely.

Now ask the questions an agent raises:

- May this reader keep a copy after the session ends?
- May it send the payload to a model provider in another jurisdiction?
- May the provider retain it in a prompt cache?
- May it be used to train a model that someone else sells?
- May the reader infer a diagnosis and act on the inference somewhere the patient never sees?

FHIR does not answer any of these, because none of them are access questions. They are use questions, and use was somebody else's problem when the spec was written. `Consent` gets closest: it expresses permission to disclose, with purpose-of-use and actor provisions. It still describes a permission granted at the boundary. Nothing in the resource travels with the payload, and nothing at the recipient enforces it. R6 introduces `Permission` with an `$evaluate` operation, which is a genuine step and is still in ballot.

So the honest statement of the gap: the standard governs who may read. It does not govern what the reader may do afterward, and it has no vocabulary for a reader that is a model rather than a person.

This is not a criticism of FHIR. Grahame and everyone who built it were solving 2014's problem, and the problem was real. It is a statement about what 2026 needs that does not exist yet.

## Nobody is building the patient's side of this

There is real work happening here, and it deserves naming. The CARIN Alliance has a code of conduct for consumer-directed exchange. Josh Mandel's SMART Health Links gave patients a genuine mechanism to share a record on their own terms. The Consent IG exists. HL7 working groups are engaged.

But look at who the AI governance tooling is being built by and for. Vendors are building agent safety into their own products, scoped to their own liability. Health systems are writing AI policies that govern the system's agents inside the system's boundary. Both are rational. Neither produces anything the patient controls.

The asymmetry is the point. When a health system deploys an agent, the system decides the redaction policy, holds the audit trail, and sets the approval rules. When a patient uses an agent on their own records, the patient decides none of those things. They accept whatever the app vendor chose, usually without seeing it, and the app vendor has no incentive to choose conservatively.

We are about to hand patients the most capable tool they have ever had for navigating this system, with no governance layer they own. That is worth being uncomfortable about.

## What the copy is actually worth

Here is why this is not an abstract concern. When a fully identified FHIR resource leaves the covered entity and lands in a general-purpose AI vendor, HIPAA usually stops applying. There is no business associate agreement with a consumer chatbot. What governs the data there is FTC enforcement and a patchwork of state law, which is a weaker regime for the individual.

Four things can happen to that copy.

**It gets sold.** Health data brokerage is an established market. Buyers include advertisers, pharmaceutical marketers, and clinical trial recruitment vendors who pay for condition-level cohorts. The FTC's 2023 actions against GoodRx and BetterHelp both involved health data reaching advertising platforms from companies that had told users it would not. No hospital was involved in either case.

**It reaches an underwriter.** The ACA bars health insurers from underwriting on pre-existing conditions. It does not bar life, disability, or long-term care insurers, who do underwrite on health history. Employer exposure runs through wellness programs and self-funded plan administration, where the separation between plan data and employer data is thinner than most employees assume. GINA and the ADA constrain some of this. Neither was drafted with conditions inferred from a chat transcript in mind.

**It puts you in a cohort.** Once a third party can infer your diagnoses, you can be placed in an outreach program you never opted into, or shown a narrower set of provider options than exists. Whether steering happens at scale today is arguable. The incentive is not, and the patient has no visibility either way.

**It trains a model.** Depending on the vendor's terms, inputs may become training data, and consumer tiers differ sharply from enterprise tiers on exactly this point. This is the only item on the list that cannot be undone. A broker's copy can be deleted. Weights cannot be un-trained.

None of these require anyone to act in bad faith. They are the default behavior of a system where the patient granted read access and nothing downstream is constrained.

## Instructions are not enforcement

The common response is to write better instructions. Put the rules in the system prompt. Tell the agent to redact identifiers, log its actions, and ask before writing.

A prompt is a request, not a control. The model decides whether to honor it, and anyone who can put text in front of the model gets a vote. Clinical notes, scanned documents, and portal messages are all text an attacker can influence. A control the agent can talk its way past is not a control.

Enforcement has to run where the agent cannot reach it. In practice that means the server, and it means four things.

**Redact on read.** Strip identifier-class fields before the resource reaches the model. Names to initials, identifiers masked, addresses removed, birth dates truncated to year. The agent gets clinical content and not identity. This is a compensating control, not a legal de-identification determination, and it should be described that way. It does not make a record with a rare diagnosis and an unusual date sequence unlinkable. What it changes is the default, and the default is currently "hand over everything."

**Audit everything.** Every read and write emits a durable record naming the tenant, the agent, the resource, and the time. A request log is not this. When a compliance officer asks which agent, acting for which patient, read which resources, an access log carrying a shared service-account identity cannot answer. Two design rules matter: the trail is append-only, and the audit detail is PHI-free, so the record you hand to a reviewer is safe to hand over.

**Step up on writes.** At the protocol level, a token that can `GET /Observation` usually can `POST /Observation`. The transport does not distinguish "summarize my labs" from "record a blood pressure of 190/120." Requiring a separate short-lived credential for writes does not stop a determined attacker by itself. It makes writes a distinct, auditable event class instead of a side effect of a chatty session.

**Require out-of-band approval for anything irreversible.** For a clinical write, block until a human confirms. For a real-world action such as a call, a text, or a submitted form, the bar is higher: commit should only submit the action, with execution gated behind an approval the agent's own toolchain cannot produce. If the agent can supply the artifact that represents human consent, there is no human in the loop.

The obvious objection: SMART scopes and `Consent` already do some of this. Partly true, and it is where the pattern should eventually live. Scopes constrain what a client may request. What they cannot do is constrain the shape of the response, produce an agent-attributed audit record, or hold a write until a person taps approve. Those are runtime behaviors, and today no server does them by default.

## One implementation, built in the open

HealthClaw Guardrails is an MIT-licensed proxy between any AI agent and any FHIR server. It exposes an MCP server with 29 tools and a REST facade, and enforces the four controls above plus tenant isolation. The design rule is that no safety property depends on client behavior.

```mermaid
flowchart LR
    A[AI Agent] --> B[MCP Server]
    B --> C[Guardrail Proxy]
    C --> D["Any FHIR Server<br/>(Aidbox, HAPI, Epic, ...)"]
    C -.- E["PHI redaction<br/>Audit trail<br/>Step-up auth<br/>Human-in-the-loop<br/>Tenant isolation"]
```

Two details are where this pattern usually leaks.

**The write path.** `fhir_propose_write` validates and previews without committing. `fhir_commit_write` requires a step-up token and returns HTTP 428 until a human confirms. For real-world actions, `action_commit` returns `202 awaiting_confirmation` and does nothing else; execution consumes a single-use credential through a separate approval path, claimed atomically so it cannot be replayed. An earlier version gated this with an `X-Human-Confirmed` request header. We removed it from the action rail, because a header is spoofable by the caller that sets it. That header still gates clinical FHIR writes today, and we document it as a compensating control rather than proof a human acted. Being precise about which guarantees are cryptographic and which are conventions is most of the value here.

**Upstream URL rewriting.** Responses are rewritten so the backing server's base URL never reaches the client. An agent that learns the real endpoint will try to route around the proxy.

## Run it in front of Aidbox

The full example, including `docker-compose.yaml`, seed data, and a scripted walkthrough, lives at [aidbox-integrations/healthclaw-guardrails](https://github.com/Aidbox/examples/tree/main/aidbox-integrations/healthclaw-guardrails) in the Aidbox examples repo.

Get a free Aidbox license at [aidbox.app](https://aidbox.app), then:

```bash
git clone https://github.com/Aidbox/examples
cd examples/aidbox-integrations/healthclaw-guardrails
cp .env.example .env          # paste AIDBOX_LICENSE, set STEP_UP_SECRET
docker compose up -d
./scripts/seed-aidbox.sh      # one Patient, three Observations, one Condition
```

Three services come up: Aidbox on 8080 as the system of record, the guardrail proxy on 5000, and the MCP endpoint on 3001 that the agent connects to. Aidbox is configured with an `AccessPolicy` scoping the guardrail's client to the FHIR endpoint, and the proxy is pointed at it:

```yaml
healthclaw:
  environment:
    FHIR_UPSTREAM_URL: http://aidbox:8080/fhir
    STEP_UP_SECRET: ${STEP_UP_SECRET}
    READ_AUTH_ENABLED: "true"
```

Nothing about the Aidbox side is unusual. That is deliberate. The guardrail layer is additive, and the FHIR server underneath keeps behaving like a FHIR server.

### The same read, with and without governance

Direct to Aidbox, the record is fully identified, as it should be:

```bash
curl -u "$AIDBOX_CLIENT:$AIDBOX_SECRET" \
  http://localhost:8080/fhir/Patient/pt-demo
```

```json
{ "resourceType": "Patient", "id": "pt-demo",
  "name": [{"given": ["Maria"], "family": "Alvarez"}],
  "identifier": [{"system": "urn:mrn", "value": "MRN-88214"}],
  "birthDate": "1974-03-11",
  "address": [{"line": ["221 Baker St"], "city": "Pittsburgh"}] }
```

Through the proxy, same resource, same Aidbox:

```bash
curl -H "X-Tenant-ID: demo" \
  http://localhost:5000/r6/fhir/Patient/pt-demo
```

```json
{ "resourceType": "Patient", "id": "pt-demo",
  "name": [{"given": ["M."], "family": "A."}],
  "identifier": [{"system": "urn:mrn", "value": "***masked***"}],
  "birthDate": "1974",
  "meta": {"tag": [{"code": "redacted"}]} }
```

Aidbox still holds the complete record. Redaction is a property of the path the agent uses, not a modification of the data.

### The read left a record

```bash
curl -H "X-Tenant-ID: demo" "http://localhost:5000/r6/fhir/AuditEvent?_count=1"
```

Returns an `AuditEvent` naming the tenant, the agent, `Patient/pt-demo`, and the timestamp, with no PHI in the detail. `$export` emits the trail as NDJSON for a SIEM.

### A write, blocked twice

Ask the agent to record a blood pressure. First attempt, no step-up token, returns 401. Mint a token and retry, and it returns 428 pending human confirmation. Only after confirmation does the Observation reach Aidbox. Verify it landed by querying Aidbox directly, going around the proxy:

```bash
curl -u "$AIDBOX_CLIENT:$AIDBOX_SECRET" \
  "http://localhost:8080/fhir/Observation?subject=Patient/pt-demo&code=85354-9"
```

The resource is there, and the audit trail records who proposed it, who approved it, and when. That sequence is the whole argument. The agent did useful work. It could not finish alone.

### Grade the deployment

```bash
curl "http://localhost:5000/r6/fhir/\$conformance?format=text"
```

```text
HealthClaw Guardrail Conformance — http://localhost:5000 [tenant=demo]
  Grade: A   (7/7 properties)

  [PASS] PHI Redaction            [PASS] Human-in-the-Loop
  [PASS] Immutable Audit Trail    [PASS] Tenant Isolation
  [PASS] Step-Up Authorization    [PASS] Medical Disclaimers
  [PASS] Error Fidelity
```

Error fidelity is the least obvious property: unknown search parameters and unsupported modifiers must be rejected or surfaced, never silently dropped. A filter that disappears quietly widens a query, and a widened query on a patient record is a disclosure. The same harness runs in CI as a merge gate, so a regression shows up as a grade change instead of an incident. A safety claim you can run is worth more than one you can read.

## What this buys the patient

Back to the clipboard. In CareAgents, the consumer app built on this layer, the request is "I'm seeing a new doctor next week, fill out my intake form from my records." The agent populates the form using SDC `$populate`, and then stops.

Each medication and each allergy requires individual patient confirmation. "No known allergies" requires an explicit attestation and is never inferred from an empty list, because an empty list and a real negative are different clinical statements. The server re-derives the item list at submit time, so a crafted request cannot skip a row. Approval produces a provenance-stamped PDF behind a signed, expiring link.

Eleven years of retyping becomes a review-and-approve. The agent does the labor. The patient keeps every decision. The server makes that division non-negotiable rather than a promise in a privacy policy.

## Why this layer has to be open

**A safety property you cannot inspect is a marketing claim.** Every vendor says their agent is safe with PHI. Open source turns that into something a hospital security team can read, run, and attack. That is a different kind of assurance and it is the only kind that survives contact with a security review.

**Health IT standards get won by open reference implementations.** FHIR spread because HAPI, public test servers, Synthea, and connectathons made adoption cheap and faking it hard. Agent governance will standardize the same way or not at all.

**The threat model is larger than any one team.** Prompt injection through clinical documents, tenant-isolation bugs, silently dropped search parameters. These get found by many adversarial readers or they do not get found. We publish our own: an audit-write failure that rolled back the caller's transaction while returning success, and a sign-in input that truncated 8-digit codes to 6. In a closed product those are quiet patches. Here they became regression tests.

**A layer meant to outlive every model vendor should not belong to one.** Frontier APIs and open-weight local models are increasingly swappable behind a single adapter. What persists across model generations is the governance layer. If it belongs to a vendor, the patient's guarantees expire when that vendor's business model changes.

## What the community should do next

Three things, and none of them are a product.

**Write the missing profile.** We need a way to express, in FHIR, what a reader may do with a resource after receiving it. Not just permission to disclose, but retention, redisclosure, inference, and training. R6 `Permission` is the right place to start the conversation. Bring it to the working groups.

**Agree on a conformance contract for agent access.** Redact, audit, step up, human-approve, isolate tenants, preserve error fidelity. Argue with that list. Replace it with a better one. But settle on something a deployment can be graded against, so "our agent is safe" stops being an unfalsifiable sentence.

**Make the patient the one holding the policy.** Right now the redaction rule, the audit trail, and the approval gate all belong to whoever wrote the app. That is backwards for consumer-directed exchange, and fixing it is a design problem the community has not seriously taken up.

Health data should be easy to get. That fight is largely won. The next one is making it safe to act on, with the patient holding the controls rather than reading about them afterward.

HealthClaw Guardrails is MIT-licensed and small enough to read in an afternoon: a Python FHIR facade, a TypeScript MCP server, roughly 1,170 Python and 112 Node tests, and the conformance harness gating CI. It is a reference implementation and an argument, not a product. If you run Aidbox, clone the example and try to break it. The most useful thing you can send us is a payload that gets past a control it should not.

Example: [github.com/Aidbox/examples](https://github.com/Aidbox/examples/tree/main/aidbox-integrations/healthclaw-guardrails) · Repo: [github.com/aks129/HealthClawGuardrails](https://github.com/aks129/HealthClawGuardrails) · Live conformance: [app.healthclaw.io/r6/fhir/$conformance](https://app.healthclaw.io/r6/fhir/$conformance) · Consumer app: [careagents.cloud](https://careagents.cloud)

*Eugene Vestel writes at FHIR IQ and hosts the* Out of the FHIR *podcast. Health Samurai builds Aidbox, a FHIR platform for healthcare teams.*


---

---
{
  "title": "Introduction to FHIR Terminology: The Basics",
  "description": "The second post in a series on FHIR terminology: coded values, code systems, value sets, and concept maps",
  "date": "2026-08-20",
  "author": "Orlando Osorio",
  "tags": ["Terminology", "FHIR", "Tutorial"],
  "extra-scripts": [
    "/assets/js/demo/termbox/base.js",
    "/assets/js/demo/termbox/basics.js"
  ]
}
---

This is the second post in the [Introduction to FHIR Terminology Series](/blog/introduction-to-fhir-terminology). We'll go over the core concepts that make up the FHIR Terminology Module. We'll see concrete examples of what problems it can solve. And give an overview of how the different pieces fit together.

## Use Cases

As we [mentioned](/blog/introduction-to-fhir-terminology#why) before, we've seen newcomers to FHIR underutilize the terminology solutions. We believe part of the reason is a lack of awareness of what problems it can solve, some people might associate _Terminology_ with topics like standards, conformance, compliance, regulations. So we think a good place to start is to show a few concrete use cases that are easily implemented by using a terminology server. In fact, all these examples are running live against our sandbox instance.

Each tab shows one live example, from search boxes and dropdowns to mapping and translation; open the drawer at the bottom of the box to see the actual requests and responses[^1].


<div id="examples"></div>

## Background

When a physician writes "heart attack" and another writes "myocardial infarction", they mean the same thing. But for a software system, or any workflow at scale, these are just two different strings of text. Being able to share meaning between parties is paramount for interoperability. This is what healthcare terminologies are for. One of the earliest examples of a healthcare terminology is the International Classification of Diseases (ICD), adopted in 1893 to codify causes of death. It predates software and it's still in use today.

We'll be working with a set of constructs that fall under the definition of _a terminology_: taxonomies, ontologies, nomenclatures, code sets, vocabularies, classifications. For practical purposes, we'll call them _terminologies_ and will define them as a set of terms with a stable machine-readable identifier. They might be infinite, have synonyms, properties, relationships, structure, etc. But the main characteristic we'll need is for each element to have a stable identifier (_code_) and some textual label (_term_/_display_).

In "normal" web development, implementers use terminologies all the time (even if they're not called that). They're frequently implemented as enums or a table of items with a CRUD admin view (it's very common for the enums to be their own codes and for the items tables to use the database id as their identifier). The examples above could very well have been implemented using these patterns.

FHIR provides a [Terminology Module](https://build.fhir.org/terminology-module.html) designed to solve these and other problems. One interesting aspect is that the same abstractions are able to express standard, compliant, authoritative, large ontologies as well as enum-like, short, local lists of codes.

## Coded Values

Many FHIR resources will have fields whose domain of possible values is taken from a list (a terminology). Their values are _codes_ assigned elsewhere that identify a defined _concept_. For example:

```yaml
resourceType: Patient
name:
  - given: [Jane]
    family: Doe
gender: female
```

Notice that, in this case, `name` is an unbound property: it can take any human name, while `gender` can only take one of four values: `male`, `female`, `other`, `unknown`. In this case, `female` is a _coded value_.

A coded value is mainly a pair composed of "system" and "code", where `system` is a URL[^2] that identifies the code system (terminology) that defines the codes. This decision has significant implications on how discoverable coded values are: instead of finding a string in the wild that happens to look like a LOINC code, coded values are accompanied by their code system identifier. In the gender case above, the system is implicit (Administrative Gender, `http://hl7.org/fhir/administrative-gender`), we'll look at why shortly.

FHIR [defines](https://build.fhir.org/terminologies.html) 3 main data types to represent these codes[^3]: `code`, `Coding`, and `CodeableConcept`.

- [`code`](https://build.fhir.org/datatypes.html#code): The instance represents the code only. The system is implicit.
- [`Coding`](https://build.fhir.org/datatypes.html#Coding): The datatype contains a code and a system that identifies where the definition of the code comes from.
- [`CodeableConcept`](https://build.fhir.org/datatypes.html#CodeableConcept): A type that represents a concept by plain text and/or one or more `coding` elements.

Most coded elements in FHIR use `CodeableConcept` since it provides the greatest flexibility. It allows multiple codings and allows for free text to convey, for example, what a data enterer actually typed or cases where no code is available. It also helps with transition, since you can include old and new codes in the same structure (see Observation example below).

The `Coding` datatype is used when the intention is to reference a specific code, not to express a concept generally. The use of `Coding` is rare as it's less flexible than `CodeableConcept` and doesn't allow free text or translations.

The `code` datatype usually appears in enum-like scenarios. Typically these elements have a binding strength of `required`, i.e.: this code is required to come from the specified value set and there's no need to convey an alternative code. Usually these elements might be involved in business logic behavior, like dispatching on a value, exact comparison, etc. For example: a resource status, client code might ignore `retired` resources or act only on `active` ones.

### Binding

In the previous datatypes, the codes are of type `string`, the way to restrict the set of values they can take is via [bindings](https://build.fhir.org/terminologies.html#binding). The spec _binds_ the element to a _value set_, meaning it can only take values from that list.

Bindings have two main properties: `valueSet` and `strength`. `valueSet` defines which codes are valid for this element. `strength` refers to how the binding should be understood: `required`, `extensible`, `preferred`, `example`.

<div class="narrow">

![Binding example](binding.png "Patient spec fragment. Notice the highlighted elements, their datatypes, binding value sets and strengths")

</div>

In the example above, `Patient.gender` is bound to the `Administrative Gender` value set, with a `required` strength; therefore, `gender` can only be: `male`, `female`, `other`, `unknown`. And `maritalStatus` is bound to `Marital Status Codes`, with an `extensible` strength, meaning: the codes should come from that value set unless the expected code is not covered.

### Example coded values

{% tabs %}
{% tab title="Patient" %}
```yaml
resourceType: Patient
name:
  - given: [Jane]
    family: Doe
gender: female # code / AdministrativeGender (implicit)
maritalStatus: # CodeableConcept
  text: Married
  coding:
    - system: http://terminology.hl7.org/CodeSystem/v3-MaritalStatus
      code: M
      display: Married
```
{% endtab %}
{% tab title="Location" %}
```yaml
resourceType: Location
name : South Wing Neuro OR 1
status: suspended # code / LocationStatus (implicit)
operationalStatus: # Coding
  system: http://terminology.hl7.org/CodeSystem/v2-0116
  code: H
  display: Housekeeping
type: # CodeableConcept
  coding:
    - system: http://terminology.hl7.org/CodeSystem/v3-RoleCode
      code: RNEU
      display: Neuroradiology unit
form: # CodeableConcept without codings
  text: Room
```
{% endtab %}
{% tab title="Observation" %}
```yaml
resourceType: Observation
status: final # code / ObservationStatus (implicit)
code: # CodeableConcept with local and standard codings
 coding:
   - system: http://acmelabs.org
     code: 104177
     display: "Blood culture"
   - system: http://loinc.org
     code: 600-7
     display: Bacteria identified in Blood by Culture
valueCodeableConcept: # CodeableConcept
  coding:
    - system: http://snomed.info/sct
      code: 3092008
      display: Staphylococcus aureus
interpretation:
  text: Positive
  coding:
    - system: http://terminology.hl7.org/CodeSystem/v3-ObservationInterpretation
      code: POS
```
{% endtab %}
{% endtabs %}

## Resources

We'll now briefly look at the FHIR resources involved in the Terminology Module. In practice, you rarely create these directly. They're usually maintained elsewhere and loaded[^4] into (or from) a terminology server.

The Terminology Module defines four main terminology resources: [CodeSystem](https://build.fhir.org/codesystem.html), [ValueSet](https://build.fhir.org/valueset.html), [ConceptMap](https://build.fhir.org/conceptmap.html), and [NamingSystem](https://build.fhir.org/namingsystem.html). We'll dedicate one post to each one of these. For now we'll give a brief overview of `CodeSystem` and `ValueSet`.

### CodeSystem

CodeSystem is arguably the most important resource in FHIR Terminology. It's used to describe a code system, its attributes, and contents. In its most common form, it contains a list of concepts, each concept with its code, designations, and properties.

#### Examples

{% tabs %}
{% tab title="AdministrativeGender" %}
```yaml
resourceType: CodeSystem
url: http://hl7.org/fhir/administrative-gender
version: 5.0.0
content: complete
status: active
concept:
  - code: male
    display: Male
  - code: female
    display: Female
  - code: other
    display: Other
  - code: unknown
    display: Unknown
```
{% endtab %}
{% tab title="ES Translation" %}
```yaml
resourceType: CodeSystem
content: supplement
supplements: http://hl7.org/fhir/administrative-gender
concept:
- code: male
  designation:
  - language: es
    value: Masculino
- code: female
  designation:
  - language: es
    value: Femenino
- code: other
  designation:
  - language: es
    value: Otro
- code: unknown
  designation:
  - language: es
    value: Desconocido
```
{% endtab %}
{% tab title="RxNorm fragment" %}
```yaml
resourceType: CodeSystem
url: http://www.nlm.nih.gov/research/umls/rxnorm
version: '03022026'
publisher: National Library of Medicine
content: complete
filter:
- code: concept
  operator: [is-a, generalizes, descendent-of]
  value: comma-separated list of concept codes for direct equality testing
- code: NDC
  operator: [=,in, exists]
  value: NDC code
property:
- {code: TTY, type: string}
- {code: NDC, type: string}
- {code: RXN_STRENGTH, type: string}
- {code: dose_form_of, type: code}
- {code: has_ingredient, type: code}
- {code: has_tradename, type: code}
# ...
concept:
- code: '5640'
  display: ibuprofen
  property:
  - code: TTY
    valueString: IN
  - code: has_tradename
    valueCode: '1100067'
  - code: ingredient_of
    valueCode: '1152223'
  - code: part_of
    valueCode: '821036'
  # ...
# ...
```
{% endtab %}
{% tab title="ICD-10-CM fragment" %}
```yaml
resourceType: CodeSystem
url: http://hl7.org/fhir/sid/icd-10-cm
version: '2025'
content: complete
property:
- description: Indicator [...] transactions ("billable code")
  type: integer
  code: valid
filter:
- description: Identify valid billable codes.
  value: 0 = "header" – not valid [...]
  code: valid
  operator: ["="]
valueSet: http://hl7.org/fhir/sid/icd-10-cm/vs
concept:
- code: Chapter-1
  display: Certain infectious and parasitic diseases (A00-B99)
  concept:
  - code: Section-A00-A09
    display: Intestinal infectious diseases (A00-A09)
    concept:
    - code: A00
      display: Cholera
      concept:
      - code: A00.0
        display: Cholera due to Vibrio cholerae 01, biovar cholerae
        property:
        - code: valid
          valueInteger: 1
# ...
```
{% endtab %}
{% endtabs %}

### ValueSet

A ValueSet resource specifies a set of codes drawn from one or more code systems.

In our experience, this is one of the most misunderstood resources in FHIR terminology. One possible reason is that the difference between code systems and value sets is not always clear to implementers. It doesn't help that, in FHIR core, most CodeSystem resources have an equivalent ValueSet that includes all their codes. For example: `http://hl7.org/fhir/administrative-gender` and `http://hl7.org/fhir/ValueSet/administrative-gender`.

The main distinction is that code systems contain the definition of the concepts, their origin, while value sets _select_ codes from the code systems. Thus, a value set can have a subset of a code system, or even mix multiple.

For example, RxNorm is a drug terminology that includes concepts for brand names, ingredients, dose forms, etc. We can define a value set of the ingredients in RxNorm. The concepts included would still be RxNorm concepts, but we would only have ingredients.

Another common use case for value sets is adding a null option. Let's say we're using ISO 3166 (Country codes) in a form. It correctly shows every country and their codes, but there's no "Unknown" country code. We can define a value set that _includes_ all codes from ISO 3166 and the `UNK` concept from [NullFlavor](http://terminology.hl7.org/CodeSystem/v3-NullFlavor).

#### Examples

{% tabs %}
{% tab title="RxNorm Ingredients" %}
```yaml
resourceType: ValueSet
url: http://example.org/rxnorm-ingredients
title: RxNorm Ingredients
compose:
  include:
  - system: http://www.nlm.nih.gov/research/umls/rxnorm
    filter:
    - property: TTY
      op: =
      value: IN
```
{% endtab %}
{% tab title="LOINC Cholesterol" %}
```yaml
resourceType: ValueSet
url: http://hl7.org/fhir/ValueSet/example
title: LOINC Codes for Cholesterol in Serum/Plasma
compose:
  include:
  - system: http://loinc.org
    concept:
    - code: 14647-2
      display: Cholesterol [Moles/Volume]
    - code: 2093-3
      display: Cholesterol [Mass/Volume]
    - code: 35200-5
      display: Cholesterol [Mass Or Moles/Volume]
    - code: 9342-7
      display: Cholesterol [Percentile]
```
{% endtab %}
{% tab title="AdministrativeGender" %}
```yaml
resourceType: ValueSet
url: http://hl7.org/fhir/ValueSet/administrative-gender
status: active
compose:
  include:
  - system: http://hl7.org/fhir/administrative-gender
```
{% endtab %}
{% tab title="Yes / No / Dont Know" %}
```yaml
resourceType: ValueSet
url: http://hl7.org/fhir/ValueSet/yesnodontknow
name: YesNoDontKnow
compose:
  include:
  - valueSet: ["http://terminology.hl7.org/ValueSet/v2-0136"]
  - system: http://terminology.hl7.org/CodeSystem/data-absent-reason
    concept:
    - code: asked-unknown
      display: Don't know
```
{% endtab %}
{% endtabs %}

## Operations

The main way of interacting with the terminology module and its resources is via operations. The R6 spec defines 8 terminology-specific operations. Throughout this series we'll look at most of them in detail. In this post we'll introduce `$expand`, the one powering most of our examples above.

### ValueSet/$expand

Given a value set, returns the list of codes that it defines. This operation is ideal for populating UI elements: dropdowns, search fields, code pickers, etc. Besides the value set, you can specify a text search filter, pagination, what properties and designations to bring back, what language to return, among other parameters.

Take our first example: a typeahead search field on SNOMED's clinical findings. Let's say we want to search for "diabetes mellitus" and get 10 results. Here's what a request could look like:

{% tabs %}
{% tab title="hurl" %}
```hurl
GET https://tx-sandbox.health-samurai.io/fhir/ValueSet/$expand
[Query]
url: http://snomed.info/sct?fhir_vs=isa/404684003
count: 10
filter: diabetes mellitus
```
{% endtab %}
{% tab title="curl" %}
```bash
curl -G 'https://tx-sandbox.health-samurai.io/fhir/ValueSet/$expand' \
  --data-urlencode 'url=http://snomed.info/sct?fhir_vs=isa/404684003' \
  --data-urlencode 'count=10' \
  --data-urlencode 'filter=diabetes mellitus'
```
{% endtab %}
{% endtabs %}

<div class="narrow">

| Param | Value | Explanation |
| --- | --- | --- |
| `url` | `http://snomed.info/sct?fhir_vs=isa/404684003` | Implicit URL for SNOMED's clinical finding concepts[^5] |
| `count` | `10` | How many concepts to match |
| `filter` | `diabetes mellitus` | Text to match |

</div>

We can also provide a value set inline on the request, this allows us to craft queries on a code system via properties. For example, let's say we want to get all brand medications that have both caffeine and acetaminophen as ingredients.

{% tabs %}
{% tab title="hurl" %}
```hurl
POST https://tx-sandbox.health-samurai.io/fhir/ValueSet/$expand
Content-Type: application/json
{
  "resourceType": "Parameters",
  "parameter": [
    {
      "name": "valueSet",
      "resource": {
        "resourceType": "ValueSet",
        "status": "active",
        "compose": {
          "include": [
            {
              "system": "http://www.nlm.nih.gov/research/umls/rxnorm",
              "filter": [
                { "property": "TTY", "op": "=", "value": "BN" },
                { "property": "tradename_of", "op": "=", "value": "CUI:161" },
                { "property": "tradename_of", "op": "=", "value": "CUI:1886" }
              ]
            }
          ]
        }
      }
    }
  ]
}
```
{% endtab %}
{% tab title="curl" %}
```bash
curl -X POST 'https://tx-sandbox.health-samurai.io/fhir/ValueSet/$expand' \
  -H 'Content-Type: application/json' \
  -d '{
    "resourceType": "Parameters",
    "parameter": [
      {
        "name": "valueSet",
        "resource": {
          "resourceType": "ValueSet",
          "status": "active",
          "compose": {
            "include": [
              {
                "system": "http://www.nlm.nih.gov/research/umls/rxnorm",
                "filter": [
                  { "property": "TTY", "op": "=", "value": "BN" },
                  { "property": "tradename_of", "op": "=", "value": "CUI:161" },
                  { "property": "tradename_of", "op": "=", "value": "CUI:1886" }
                ]
              }
            ]
          }
        }
      }
    ]
  }'
```
{% endtab %}
{% endtabs %}

<div class="narrow">

| Filter | Value | Explanation |
| --- | --- | --- |
| `TTY` | `BN` | Only brand names |
| `tradename_of` | `CUI:161` | Contains acetaminophen |
| `tradename_of` | `CUI:1886` | Contains caffeine |

</div>

## Next

In our next post we'll look at canonicals. How are terminology resources identified, what conventions are used, how versioning and resolution works.

- Previous: [Introduction to FHIR Terminology](/articles/introduction-to-fhir-terminology)
- Next: Canonicals — _coming soon_

<!-- footnotes -->

[^1]: The most efficient way of implementing some of these examples is using [batching](https://build.fhir.org/http.html#transaction), we're keeping the requests isolated for easier inspection.

[^2]: We'll dive deeper into how these URLs work in a future post about canonicals.

[^3]: The spec actually defines 4 datatypes (and 3 additional ones). But, `CodeableReference` is defined in terms of `CodeableConcept` and the additional ones are special cases. That's why we focus on the first three. See https://build.fhir.org/terminologies.html. 

[^4]: In a future post we'll look at a tutorial for setting up a terminology server to power a healthcare app from scratch.

[^5]: See https://terminology.hl7.org/en/SNOMEDCT.html for how to use SNOMED with FHIR Terminology. We'll dedicate a future post to SNOMED CT.


---

---
{
  "title": "Why FHIR SDC and Standard Terminologies Matter",
  "description": "How FHIR SDC and standard terminologies like SNOMED CT, LOINC, and RxNorm turn digital forms into interoperable, analytics-ready healthcare data.",
  "date": "2026-08-13",
  "author": "Maria Ryzhikova",
  "reading-time": "5 min read",
  "tags": ["FHIR Standard", "Forms", "Terminology"],
  "tldr": "FHIR SDC standardizes how clinical forms are structured and completed. Combined with terminologies such as SNOMED CT, LOINC, and RxNorm, every response becomes semantically consistent data — ready for interoperability, analytics, and AI from the moment of capture. Formbox and Termbox deliver this ecosystem out of the box.",
  "utm-campaign": "fhir_expert",
  "utm-content": "sdc-terminologies"
}
---

FHIR SDC is much more than a standard for electronic forms. It provides a framework for capturing structured healthcare data that can be exchanged, analyzed, and reused across systems.

When combined with standard clinical terminologies such as **SNOMED CT, LOINC, RxNorm, ICD-10**, and **UCUM**, FHIR SDC enables healthcare organizations to collect semantically consistent data that is immediately ready for interoperability, analytics, AI, and clinical decision support.

The real value of digital forms comes not from replacing paper — it comes from capturing **high-quality, standardized clinical data**.

## Structured data starts with FHIR SDC

FHIR provides a common information model for healthcare data. FHIR SDC (Structured Data Capture) builds on this foundation by defining how clinical forms should be represented, completed, and integrated into healthcare workflows.

FHIR SDC standardizes:

- Clinical questionnaires using `Questionnaire`
- Form responses using `QuestionnaireResponse`
- Pre-population from existing patient records
- Extraction of collected data into FHIR resources
- Integration with terminology services through **ValueSets** and **CodeSystems**

As a result, every completed form produces structured data that can immediately participate in interoperability workflows instead of remaining isolated inside a single application.

## Why standard terminologies matter

A standardized data model alone is not enough.

Imagine one clinician records *Heart Attack*, another enters *Myocardial infarction*, and a third selects a local hospital-specific code. Humans understand these values represent the same clinical concept. Software often does not.

This is why healthcare relies on standardized terminologies:

| Terminology | Purpose |
|-------------|---------|
| **SNOMED CT** | Clinical concepts |
| **LOINC** | Laboratory tests and observations |
| **RxNorm** | Medications |
| **ICD-10** | Diagnoses and reporting |
| **UCUM** | Units of measure |

Using standardized codes makes data:

- Interoperable across healthcare systems
- Reliable for reporting and population health analytics
- Ready for clinical decision support
- Consistent for AI and machine learning
- Easier to exchange between organizations

Instead of exchanging text, systems exchange universally understood clinical concepts.

## FHIR SDC brings forms and terminology together

One of the key strengths of FHIR SDC is its native support for terminology services.

Choice questions can reference **FHIR ValueSets**, allowing answer options to be retrieved dynamically from terminology servers instead of being hardcoded into forms.

This approach provides significant advantages:

- Always up-to-date clinical vocabularies
- Centralized terminology management
- Reusable ValueSets across multiple forms
- Consistent coding across clinical workflows
- Easier maintenance of large form libraries

Rather than maintaining thousands of dropdown options manually, organizations simply reference the appropriate ValueSets.

## Better data capture leads to better analytics

The quality of analytics depends entirely on the quality of the captured data.

When healthcare organizations collect structured data using FHIR SDC and standard terminologies, they create datasets that can be immediately reused for:

- Clinical reporting
- Quality measurement
- Population health management
- Regulatory reporting
- Research
- AI applications and machine learning

Instead of spending months cleaning and mapping inconsistent datasets, organizations can begin analyzing data as soon as it is captured.

## Formbox + Termbox: a complete FHIR SDC ecosystem

FHIR SDC defines how structured healthcare data should be captured. Standard terminology services ensure that the captured data is semantically consistent. Together, they provide the foundation for interoperable healthcare systems and trustworthy analytics.

**Formbox** and **Termbox** deliver this ecosystem out of the box.

With **Formbox**, organizations can design FHIR SDC-compliant forms, connect questions to standard ValueSets, pre-populate forms with existing FHIR data, capture structured responses, and automatically extract information into FHIR resources.

**Termbox** complements this workflow with an enterprise-grade FHIR Terminology Server, providing high-performance terminology operations such as ValueSet expansion, code validation, terminology lookup, and centralized management of clinical vocabularies including SNOMED CT, LOINC, ICD-10, RxNorm, UCUM, and many others.

Together, Formbox and Termbox enable healthcare organizations to:

- Design standards-based clinical forms
- Capture semantically interoperable data
- Validate terminology at the point of data entry
- Exchange information using FHIR
- Generate analytics-ready datasets from day one
- Build AI-ready healthcare applications without custom terminology infrastructure

The result is an end-to-end workflow where every response is captured in a standardized format, validated against trusted clinical vocabularies, and immediately ready for interoperability and analytics. Formbox supports FHIR-native form creation and data capture, while Termbox provides centralized terminology management and validation — creating a complete ecosystem for standards-based healthcare applications.

**Want to see it in action?** Watch how to code a FHIR SDC form and connect a ready-to-use ValueSet from Termbox in this short tutorial:

{% embed url="https://www.youtube.com/watch?v=FpQ2Q92bI9Y" %}

## Build once. Reuse everywhere.

FHIR SDC was never intended to be just a standard for electronic forms. It is a framework for collecting structured, computable healthcare data.

When combined with standardized clinical terminologies, every completed form becomes immediately useful — not only for patient care, but also for interoperability, analytics, research, AI, and regulatory reporting.

**Formbox and Termbox bring this vision to life as a complete FHIR SDC ecosystem**, enabling healthcare organizations to move seamlessly from form design to standards-based data capture, terminology validation, and interoperable FHIR data exchange.

Because the future of healthcare isn't just digital — it's **structured, standardized, and reusable**.

Ready to experience FHIR SDC in practice? Explore the **[public Form Builder](https://form-builder.aidbox.app/)**, create your own FHIR Questionnaire, connect standard ValueSets from Termbox, and see how easy it is to build interoperable, analytics-ready healthcare forms.


---

---
{
  "title": "Introduction to FHIR Terminology",
  "description": "The first post in a series on FHIR terminology, for readers new to coded values, code systems, and value sets.",
  "date": "2026-08-11",
  "author": "Orlando Osorio",
  "tags": ["Terminology", "FHIR", "Tutorial"],
  "extra-scripts": [
    "/assets/js/demo/termbox/base.js",
    "/assets/js/demo/termbox/intro01.js"
  ]
}
---

This is the first post in a series about FHIR terminology. It's aimed at beginners. We'll start with the basics and then move into more advanced topics. We'll favor practical examples and real use-cases, and some posts will include interactive demos.

## Why

At Health Samurai, we've helped teams implement FHIR solutions for a long time. We've noticed that terminology understanding is often not as strong as it could be, and this causes implementers to go for complex approaches to solve problems that have simpler solutions within the FHIR terminology space. Although we've seen this across the board, it's most common on teams new to FHIR.

We think a possible explanation is that FHIR resources often map cleanly onto concepts a team already understands. Resources like `Patient`, `Condition`, and `Encounter` have intuitive counterparts in the healthcare domain. Once you get into `CodeSystem` and `ValueSet`, it becomes less obvious, especially since many systems treat coded values as database row ids or enums and have ad-hoc tables with admin CRUDs for maintaining lists of concepts.

The best way to learn FHIR is arguably to read [the spec](https://hl7.org/fhir). But the spec is formal and structured, organized as reference material, it gets dense fast, and sometimes it's hard to find a concrete example that makes it _click_.

With this series, we're hoping to provide a thread that beginners can follow into FHIR terminology, using concrete examples you can run, inspect, and adapt. 

## Contents

We'll start with an introduction to terminology in healthcare, then move on to the foundations of FHIR terminology. We'll go over specific concepts and resources, like coded values, code systems, value sets, and concept maps.

A few posts will focus on specific terminologies like SNOMED, LOINC, ICD; we'll also go over the HL7 terminology ecosystem: THO, UTG, and other acronyms.

Later on, we plan to dive deeper into more advanced topics like human language translation, supplements, syntactic code systems, and more.

### Interactive Examples

We'll use concrete examples and use cases throughout the series. Each one is live and runs against our Termbox sandbox instance. The demos will have a panel where you can explore the HTTP requests being run. Try the one below.

<div id="intro01-content">
  <label class="tx-label" for="intro01-input">SNOMED CT finding lookup</label>
  <div class="tx-context">Searching SNOMED CT for findings: descendants of <code>404684003</code> (Clinical finding)</div>

  <div class="tx-search-wrap">
    <input id="intro01-input" class="tx-search-input" type="text" autocomplete="off" placeholder="e.g. diabetes mellitus">
    <ul class="tx-suggestions" id="intro01-suggestions" hidden></ul>
  </div>

  <div class="tx-detail" id="intro01-detail" hidden></div>
</div>

## Next

This is the first post in the series. More are on the way:

1. [Introduction to FHIR Terminology](/articles/introduction-to-fhir-terminology) - _this post_
2. [The Basics](/articles/introduction-to-fhir-terminology-the-basics)
3. Canonicals - _coming soon_

We'll keep this list updated as posts publish. Questions or feedback? Please leave a comment.


---

---
{
  "title": "Aidbox, Formbox & Payerbox 2607: SMART Health Cards, Binary Storage, and UM Integration",
  "description": "Release 2607 adds SMART Health Cards, validation for data already stored in Aidbox, native Binary REST handling, external base64Binary storage, and PAS routing into external utilization management systems.",
  "date": "2026-08-07",
  "author": "Valeria Fursa",
  "reading-time": "6 min read",
  "tags": ["Aidbox", "Integrations", "Forms"],
  "utm-campaign": "release",
  "utm-content": "2607-release"
}
---

Release 2607 brings several changes to how FHIR data is issued, validated, stored, and exchanged.

Aidbox adds SMART Health Cards, validation for resources already stored in the database, native REST handling for Binary resources, and external storage for large `base64Binary` payloads.

On the payer side, PAS requests can be routed into external utilization management systems, including HealthEdge GuidingCare. Formbox gets improvements to form completion, validation, and extraction workflows.

## SMART Health Cards

Aidbox 2607 introduces [SMART Health Cards](https://www.health-samurai.io/docs/aidbox/api/rest-api/other/smart-health-cards).

The `$health-cards-issue` operation creates a verifiable health credential from FHIR data and signs it as a compact JWS. Patients can present the resulting credential as a QR code or file.

Aidbox publishes the verification public key through a JWKS endpoint, so compatible SMART Health Cards verifiers can validate the credential independently.

## Validate Data Already in Aidbox

FHIR validation often happens when data enters the system. That does not cover records imported without validation or data that needs to be checked again after profiles change.

The reworked [`$batch-validate`](https://www.health-samurai.io/docs/aidbox/modules/profiling-and-validation/batch-resource-validation) operation runs validation against resources already stored in Aidbox.

It supports synchronous and asynchronous execution and stores results indexed by validation issue, with drill-down to the affected resources.

This is particularly useful after bulk imports or profile changes, when teams need to understand the quality of data already in production rather than wait for validation errors to surface during the next write.

## Better Handling of Binary Data

Two changes in 2607 address files and other binary payloads stored in FHIR.

The [`/fhir/Binary`](https://www.health-samurai.io/docs/aidbox/api/rest-api/other/binary) endpoints now follow FHIR REST content-negotiation rules.

Clients can send raw content to `POST` or `PUT` using its actual `Content-Type`, rather than wrapping the payload in FHIR JSON first. On read, Aidbox can return either the decoded binary content or the FHIR Binary resource, depending on the requested `Accept` type.

Large `base64Binary` values no longer need to live inside PostgreSQL either.

With [`dataOffloadToExternalStorage`](https://www.health-samurai.io/docs/aidbox/configuration/storage-and-api-configuration/offload-base64binary-to-external-storage), fields such as `Binary.data`, `DocumentReference.content.attachment.data`, and `Patient.photo.data` can be stored in external blob storage.

Aidbox keeps the blob location and hash with the resource and restores the content when the resource is read through the API. Clients still receive the normal FHIR representation.

The setting is configured per API through `$create-api` or `$configure-api`. Azure Blob Storage is currently supported.

For systems storing images, documents, and other large payloads in FHIR, this keeps those bytes out of PostgreSQL tables, history, backups, and replication without forcing applications to use a separate file-access API.

## Bundle and Bulk Import Performance

[FHIR Bundle](https://www.health-samurai.io/docs/aidbox/api/rest-api/bundle) processing and [`aidbox.bulk/load-from-bucket`](https://www.health-samurai.io/docs/aidbox/api/bulk-api/bulk-import-from-an-s3-bucket) both receive performance improvements in 2607.

Bundle validation has also been fixed, along with [patient filtering in consent-aware bulk export](https://www.health-samurai.io/docs/aidbox/api/bulk-api/export#consent-based-patient-filtering).

Other Aidbox improvements include better [AccessPolicy debugging](https://www.health-samurai.io/docs/aidbox/tutorials/security-access-control-tutorials/debug-access-control), expanded [`X-Original-Uri`](https://www.health-samurai.io/docs/aidbox/api/rest-api/fhir-search) support, correct handling of `backport-filter-criteria` in [FHIR topic-based subscriptions](https://www.health-samurai.io/docs/aidbox/modules/topic-based-subscriptions/fhir-topic-based-subscriptions), and fixes in Multibox, Resource Browser, and the [Metrics server](https://www.health-samurai.io/docs/aidbox/modules/observability/metrics/monitoring/use-aidbox-metrics-server).

[`AidboxMigration`](https://www.health-samurai.io/docs/aidbox/configuration/migrations) also gets an `execution-type` parameter for SQL that must run outside a transaction, including PostgreSQL statements such as `CREATE INDEX CONCURRENTLY`.

## PAS to External Utilization Management

Payerbox 2607 adds a configurable path between [Da Vinci PAS](https://www.health-samurai.io/docs/payerbox/prior-auth/pas) and a payer's [utilization management system](https://www.health-samurai.io/docs/payerbox/prior-auth/um-integration).

Requests can be routed according to `Claim.insurer`.

The `pas-passthrough` connector forwards PAS requests to an external implementation of `Claim/$submit` and `Claim/$inquire`, while preserving identifiers and avoiding duplicate submissions during retries.

A [separate connector](https://www.health-samurai.io/docs/payerbox/prior-auth/um-integration#choosing-a-connector) integrates with HealthEdge GuidingCare through its REST API. Payer-configured FHIR `ConceptMap` resources handle value translation between PAS and GuidingCare.

Both routes are configured through [`UMTenantConfig`](https://www.health-samurai.io/docs/payerbox/api-reference/configuration-resources/um-tenant-config).

Payers do not have to replace their existing UM workflow to expose a Da Vinci PAS interface. Payerbox can handle the FHIR-facing exchange while decisioning stays in the system already used by the organization.

## Validation at the PAS and CRD Boundary

Not every integration mismatch needs to reject an otherwise usable request.

For PAS, configurable [lenient validation](https://www.health-samurai.io/docs/payerbox/prior-auth/pas#validation-strictness) can treat display-name differences and referenced-resource profile mismatches as warnings. Structural errors, profile violations, and missing references remain blocking.

[CRD](https://www.health-samurai.io/docs/payerbox/prior-auth/crd#validation-strictness) gets similar flexibility for hook-context references that exist in the EHR but are not available inside Payerbox.

Strict validation remains available where the complete referenced context is expected.

## Payer-to-Payer and Provider Access

[`$davinci-data-export`](https://www.health-samurai.io/docs/payerbox/api-reference/operations/davinci-data-export) now removes remittance and enrollee cost-sharing information from exported `ExplanationOfBenefit` and `Coverage` resources.

That includes totals, payments, benefit balances, prices, adjudication amounts, `costToBeneficiary`, subrogation, and other monetary elements.

Clinical and administrative content, extensions, and contained resources remain in the export.

## PAS Analytics with SQL on FHIR

The `io.healthsamurai.pas-metrics` package, version 0.1.2, is available on request.

It contains 10 [`ViewDefinition`](https://www.health-samurai.io/docs/payerbox/analytics/sql-on-fhir) resources and 15 `Library` resources implementing metrics suggested by the PAS Implementation Guide.

That gives teams a starting point for tracking PAS workflow performance without first building a separate reporting model and ETL pipeline around the operational data. Results land in the same [flat views](https://www.health-samurai.io/docs/payerbox/analytics/flat-views) that back the rest of Payerbox analytics.

See the [Payerbox analytics documentation](https://www.health-samurai.io/docs/payerbox/analytics) or contact Health Samurai to request the package.

## FHIR App Portal

The [app-detail page](https://www.health-samurai.io/docs/payerbox/fhir-app-portal/admin-portal) now includes privacy-policy and terms-of-service links.

Missing values are shown as "Not provided," and applications requesting patient data without a privacy policy are flagged with a warning.

Administrators can also add a free-text note when declining an app, alongside the predefined decline reason.

For [provider-directory publishing](https://www.health-samurai.io/docs/payerbox/run-payerbox/provider-directory-pipeline), [MPF pipeline configuration](https://www.health-samurai.io/docs/payerbox/api-reference/operations/mpf-pipeline-api) now supports the `index.json` file.

## Formbox

Formbox 2607 focuses on several details in the form-filling flow.

NHS-themed forms support custom labels for continue and submit buttons. A "save and exit" action is shown by default in the NHS app and is also available on the web when a redirect-on-submit link is configured.

Date, time, and datetime validation now recognizes incomplete values without clearing what the user has already typed. With 12-hour time fields, users can be prompted for "am" or "pm" while keeping the rest of the input intact.

Time items are supported in `enableWhen` conditions, and open-choice questions can submit custom values entered through "specify other."

Template-based extraction gets improvements to layout, subject-reference generation, and allocated ID dependencies. Form Builder now clears calculated-expression validation errors once they are resolved.

Pagination has also been improved in the forms grid and added to the form gallery.

## Before Upgrading

Three Aidbox changes may require migration work.

The `/fhir/FHIRSchema` endpoint has been removed. FHIR profiles should be defined with standard `StructureDefinition` resources, which the [FHIR Schema validator](https://www.health-samurai.io/docs/aidbox/modules/profiling-and-validation/fhir-schema-validator) compiles internally.

The Zen `seed` and `seed-v2` engines and the `SeedImport` resource type are gone as well. Configuration that needs to load at startup should use [Init Bundle](https://www.health-samurai.io/docs/aidbox/configuration/init-bundle).

C-CDA conversion has moved out of Aidbox. The built-in converter and `/ccda/*` endpoints are no longer part of the server.

## Read the Full Release Notes

This post highlights the main changes across the three products. For the complete changelog, read the release notes for:

- [Aidbox 2607](https://www.health-samurai.io/docs/aidbox/overview/release-notes)
- [Formbox 2607](https://www.health-samurai.io/docs/formbox/release-notes)
- [Payerbox 2607](https://www.health-samurai.io/docs/payerbox/releases)

The product documentation includes the exact configuration details, fixes, and migration guidance to review before upgrading.


---

---
{
  "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"
}
---

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


---

---
{
  "title": "Termbox on Databricks Lakebase",
  "description": "Termbox, now runs on Databricks Lakebase Postgres. Terminology stops being a service you integrate from the outside and becomes part of the platform where your analytics already lives.",
  "date": "2026-07-30",
  "author": "Guillermo Rodríguez",
  "reading-time": "4 min read",
  "tags": [
    "Terminology",
    "Analytics",
    "Database",
    "Infrastructure"
  ]
}
---

## Termbox now runs on Databricks Lakebase

[Termbox](/docs/termbox/introduction), the FHIR terminology server, can now run on [Databricks Lakebase](https://docs.databricks.com/aws/en/oltp/) PostgreSQL. The SNOMED, LOINC and RxNorm content, and the value sets you use, now live in the same platform as your other products and the same place your data teams already work.

## The code list problem

Most of the interesting questions in healthcare analytics are, underneath, terminology questions. Data teams solve this using lists of codes. Sometimes exported from a terminology tool months ago. Sometimes typed by hand. Sometimes with complex ELT-like processes for keeping up with terminology sources. Meanwhile, the operational terminology server holds the definition of the concepts, the ones used to validate data, to power the search box in the UI and to map concepts between vocabularies. That creates a gap that affects architectural decisions and entails additional costs.

## Terminology data is different

The reason that pipeline feels wasteful is that terminology behaves nothing like clinical data. It has four properties that, taken together, change where it belongs.

**It's immutable and versioned.** A terminology release is a snapshot, not a mutable dataset. SNOMED CT 20260301 is what it is, permanently; the next release is a new snapshot. This is what lets Termbox be as fast as it is. Content is loaded once and indexed aggressively. There's no write contention, almost no cache invalidation, no reconciliation logic. That's the foundation of the response times we published in the [FHIR TX Benchmark](/blog/fhir-tx-benchmark).

**It's read-mostly, and bursty.** A terminology server has idle times, and enormous burst times: a nightly cohort build that executes thousands of expands, a bulk validation run over a backlog. Lakebase's [autoscaling](https://docs.databricks.com/aws/en/oltp/projects/autoscaling) is a good fit for that kind of system, with scale-to-zero suspending it entirely when the server is idle.

**Its lifecycle is owned by someone else.** You don't decide when SNOMED CT changes, or LOINC, or RxNorm, or ICD-10. The publishers do, on their own schedules, and those schedules are frequent enough that analytics are at risk of running on a stale terminology release. If terminology sits outside your data platform, every one of those releases has to be propagated across a boundary. Keeping it inside means the refresh loop stays in one place.

**It's a shared dependency, not one tenant's data.** Clinical data belongs to a patient, an encounter, an organization. Terminology belongs to everyone. The same LOINC serves ingestion validation, the clinical search box, the cohort builder, and the agent answering questions about lab results.

There's a pattern in those four properties. In transactional systems, terminology use is diffuse and unpredictable, anything might need to validate a code at any moment. In analytics, it's different: narrow, contextualized, repeated. The same handful of value sets, expanded over and over, against the same tables. Which is exactly the case for putting terminology next to the data.

## From the FHIR API to the SQL surface

Right now, everything in Termbox is exposed through the FHIR terminology API: `$lookup`, `$validate-code`, `$expand`, `$subsumes`, and `$translate`. For analytical workloads, the API is the right interface for some jobs and the wrong one for others. Expanding a value set once and joining the result against a billion-row table is not something you want to do over HTTP.

Part of that foundation is already in place. Termbox is powered by a comprehensive relational schema, built around concepts borrowed from RDF and heavily inspired by the FHIR terminology model: concepts, properties and designations are just rows. That schema is stable, and running on Lakebase makes it reachable from the platform where analytics happens.

A schema on its own isn't enough, though. A value set is a definition: includes and excludes, filters, hierarchy traversal, references to other value sets. Turning that definition into the set of codes it actually denotes is a computation, and a heavy one. That's the work `$expand` does on every call. For a value set to be usable from a SQL `JOIN`, that expansion has to be materialized, and kept current as new releases land.

That's the piece we're building: expansions materialized into the lakehouse, documented and stable enough to build pipelines on. One authoritative definition, reachable two ways: `$expand` from the application, a `JOIN` from the notebook. Running on Lakebase is the prerequisite.

## How to run it

Termbox connects to Lakebase as a Databricks [service principal](https://docs.databricks.com/aws/en/admin/users-groups/service-principals), using short-lived OAuth-issued database credentials. Termbox resolves the credential, caches it, and refreshes it transparently before it expires. There's no long-lived database password sitting in your terminology service's configuration.

Full setup is in the [Termbox documentation](/docs/termbox/managed-postgresql). To try it, you can get a free Development License in a couple of clicks.

---

---
{
  "title": "2026 CMS HL7 FHIR Connectathon: Burden Reduction and PDex Results",
  "description": "Payerbox took part in two tracks at the 7th Annual CMS HL7 FHIR Connectathon: Burden Reduction and PDex. What we tested against live EHR and payer systems, what broke, and what we changed.",
  "date": "2026-07-28",
  "author": "Andrei Fedorov, Rostislav Antonov, Sergey Zaborovsky",
  "reading-time": "5 min read",
  "tags": ["Integrations", "Compliance"],
  "utm-campaign": "events",
  "utm-content": "connectathon-2026"
}
---

The [7th Annual CMS HL7 FHIR Connectathon](https://www.hl7.org/events/cms/) ran July 14–16, 2026, a free virtual event organized by the Centers for Medicare & Medicaid Services (CMS) together with HL7 International. Its purpose is hands-on testing: implementers put real FHIR workflows against each other, work through the rough edges together, and get ahead of the CMS and ONC interoperability requirements coming into force.

For health plans, those requirements run on a specific clock. [CMS-0057-F](https://www.health-samurai.io/articles/understanding-the-cms-0057-f-interoperability-and-prior-authorization-final-rule) puts the Prior Authorization API and the Payer-to-Payer API into effect on January 1, 2027.

We brought [Payerbox](https://www.health-samurai.io/cms-0057-f), our FHIR compliance platform for health plans, to two tracks: Burden Reduction and PDex, testing directly against other payer and vendor systems. Here is what we ran, what broke, and what we changed.

## Burden Reduction Track

In the Da Vinci Burden Reduction track ([CRD](https://hl7.org/fhir/us/davinci-crd/), [DTR](https://hl7.org/fhir/us/davinci-dtr/), and [PAS](https://hl7.org/fhir/us/davinci-pas/): the electronic prior authorization stack) Payerbox was on the payer side. Our server hosted the CDS Hooks services, DTR questionnaires, and PAS operations, and EHR vendors tested their provider-side implementations against it. Over two days we ran sessions with six EHR vendors: **Epic**, **MEDITECH**, **Darena Health**, **MEDHOST**, **Oracle Health**, and **Altera Digital Health**. Thanks to all six teams for coming prepared.

The shared scenario was a Home Oxygen Therapy prior authorization. A CRD `order-sign` hook returns a "prior authorization required" card carrying a `coverage-information` system action that points at a DTR questionnaire, the clinician completes the prepopulated form, and the resulting bundle goes to PAS `Claim/$submit`, which returns a `ClaimResponse` within seconds. The utilization management decision follows, and the EHR picks it up through `$inquire`.

Beyond that happy path we exercised the four CRD hooks Payerbox implements (`order-sign`, `order-select`, `order-dispatch`, and `appointment-book`), DTR `$questionnaire-package`, PAS `$inquire`, CDex `$submit-attachment`, and the claim update and cancel flows. The full chain, from CRD card to DTR form to PAS submission, ran end to end with real EHR systems driving it. In several runs, identifiers minted in our CRD response came back to us inside the partner's PAS submission. That is good proof that the pieces connect, and it took real engineering effort on both sides.

![Prior Authorization flow: three column groups. Provider EHR (CDS Hooks client, DTR launcher, PAS client) on the left, Payerbox (CRD CDS service, DTR SMART app, PAS endpoint) in the center, Health Plan (decision service, UM system) on the right, with CRD, DTR, and PAS stage arrows between them.](prior-auth-flow.svg)

### What we fixed

Live EHR clients produced a prioritized defect list. One class of fix shipped mid-event, on day two: we loosened validation strictness so that harmless display-string and reference mismatches warn instead of block, while genuinely incomplete submissions still get a clean error response. A payload that failed in a morning session passed on replay the same afternoon.

The rest landed over the three days that followed:

- Claim cancel end to end, so a cancelled prior authorization is visible through `$inquire`.
- Coverage assertions changed to definite decisions rather than conditional ones.
- Questionnaire prepopulation: seven questionnaires never prefilled because their launch contexts were unbound, which only surfaced when real DTR clients opened real forms.
- CRD hook requests carrying `fhirServer: null` were rejected where an absent key passed; explicit nulls are now treated as absent.
- PAS bundle closure: resources left over from an earlier CRD exchange no longer count as payer-known references, so `$submit` correctly rejects a bundle that is not self-contained.

### What we sent back

We gave partners notes on PAS bundle self-containment (which referenced resources must travel inside the submission bundle) and on DTR launch signaling in CRD 2.1, where the `coverage-information` system action replaces the older card-link pattern, plus several smaller test-data observations.

## PDex Track

In the Da Vinci [Payer Data Exchange (PDex)](https://hl7.org/fhir/us/davinci-pdex/) track we tested Payerbox against three other systems: **InterSystems**, **Hike Health**, and **CareEvolution**. Thanks to all three teams for testing against us. The goal was the full payer-to-payer flow end to end: member matching via `$bulk-member-match`, followed by bulk data export via `$davinci-data-export`.

The teams integrating against us produced a prioritized list of their own. Most of it clustered around a few themes:

- aligning our CapabilityStatement with how the IG expects PDex operations to be declared
- loosening a couple of strict validation and request-body checks that got in partners' way
- tightening our bulk-export filters
- making consent-based routing of matched members easier to read when a member ends up constrained

None of it is structural. These are the interoperability details that only surface against live counterparties. Each is tracked as its own issue and none is closed as we publish, which is the honest state of a track we entered to find exactly this.

We fed observations back as well: some implementations performed no reference or FHIR validation during the event, and one showed a mismatch between synchronous and asynchronous matching counts.

### Provider Access

The same track covers Provider Access, another of the four CMS-0057-F APIs, and our session with InterSystems spanned both. Provider Access reuses most of the payer-to-payer machinery: `$provider-member-match` resolves the members attributed to a requesting provider, and the same `$davinci-data-export` delivers their claims, clinical, and prior authorization data. Worth being explicit about the shape, because it is the part payers most often have to redesign for: this is a bulk export over an attributed group, not an on-demand per-patient lookup. The defect list above came from the payer-to-payer scenarios.

## What this means for payers

CMS-0057-F comes with no exam. There is no authorized testing body and no required test suite the way there is for ONC-certified EHRs, and conformance tooling like Touchstone exists but is not mandated. So nothing certifies that a Prior Authorization or Payer-to-Payer API works. The only evidence is that it has been run against counterparties you do not control.

That is what a connectathon buys, and it is why the defect lists above are the useful part of this post. Every item on them came from a partner's payload, not from our own test suite: an unbound launch context that left a questionnaire blank, a bundle that looked self-contained until a real EHR sent it, a null where our schema expected an absent key. None of it is reachable by testing against yourself.

So when you assess readiness, yours or a vendor's, that is the question worth asking before January 2027: which live counterparties has this been through, and what broke when it met them.

## Try It Yourself

Want to test against Payerbox the same way our connectathon partners did? We can set you up with the scenarios we ran during the event: member matching and bulk export on the PDex side, CRD, DTR, and PAS on the Burden Reduction side. [Request access](https://www.health-samurai.io/cms-0057-f#contact-form) and we will get you started.

If you are building payer-to-payer exchange or electronic prior authorization on FHIR and simply want to compare notes, [get in touch](https://www.health-samurai.io/contacts). We would like to hear from you.


---

---
{
  "title": "Mapping Kahn onto FHIR",
  "description": "What separates good FHIR data from bad? The health-data world settled a precise vocabulary for it a decade ago — the Kahn framework. This article walks through it and maps each part onto FHIR: what the validator already covers, and what needs a whole dataset.",
  "date": "2026-07-21",
  "author": "Nikolai Ryzhikov",
  "reading-time": "8 min read",
  "tags": [
    "SQL on FHIR",
    "Data Quality",
    "Analytics"
  ],
  "tldr": "Ask FHIR people what data quality means and you get the validator: profiles, cardinalities, bindings, invariants. Ask data engineers and you get dbt tests: not-null, unique, accepted values, freshness. Both are half right, and the halves do not overlap where people assume. The line is not conformance-versus-the-rest — the FHIR validator does plausibility too, via minValue/maxValue and invariants. The line is scope: a validator answers every question that fits inside one resource, and no question that does not. Proportions, tolerances, cross-record uniqueness, referential integrity, distributions, freshness — all of it needs a dataset. And watch out for the word validation, which means two unrelated things in these two worlds.",
  "utm-campaign": "analytics",
  "utm-content": "kahn-framework"
}
---

## What makes data good — or bad?

It sounds like a simple question and it is not. Ask a FHIR implementer what data quality means and you get the validator: profiles, cardinalities, terminology bindings, invariants. Ask a data engineer the same question and you get dbt tests: not-null, unique, accepted values, freshness.

Both answers are half right, and the halves do not overlap the way either side assumes. The confusion has a price: teams either rebuild the validator in SQL, or they read a green validation run as "the data is fine" — and both mistakes surface late, usually when a quality measure returns a number nobody can defend.

The good news is that the health-data world already worked out a precise answer to "what is good data" a decade ago — the **Kahn framework**. This article walks through it and maps it onto FHIR: what your validator already covers, and what it structurally cannot.

I went looking for it while [porting OMOP's data quality checks onto SQL on FHIR](/blog/fhir-data-quality-sql-on-fhir). The mechanics were easy; saying *which* checks belong to the validator and which do not was the hard part — and this framework is what draws the line.

## Kahn, in three questions

Kahn et al., [*A Harmonized Data Quality Assessment Terminology and Framework*](https://pmc.ncbi.nlm.nih.gov/articles/PMC5051581/) (2016). It is not a metric set and not a tool — it is a **terminology**, written to merge a dozen incompatible vocabularies different groups had each invented. That is exactly why it stuck. Arguing about names is cheaper than arguing about measurements.

It asks three questions of the data:

| Question | Kahn calls it | Example |
|---|---|---|
| Is it recorded correctly? | **Conformance** | `Sex` only has values M, F or U |
| Is the value there at all? | **Completeness** | 40% of observations carry no value |
| Can the value be believed? | **Plausibility** | A body weight of 1000 kg |

Two details are load-bearing. Completeness is *"without reference to data values"* — it counts how often something is present, never what it says. And plausibility is about **believability, not truth**: whether 78 kg is plausible, not whether the patient actually weighs 78 kg. Nothing in the data can answer the second question, and the framework does not pretend otherwise.

## The trap: "validation" means two different things

Before going further — this word will bite a FHIR audience, so let me defuse it.

In FHIR, *validation* means checking a resource against a StructureDefinition. In Kahn, *validation* means comparing data against an **external** benchmark, as opposed to *verification* against your own expectations. Unrelated concepts, same word, and they overlap just enough to mislead:

- Validating a resource against **US Core** is Kahn-*validation* — the rule came from outside.
- Validating the same resource against **a profile you wrote yourself** is Kahn-*verification* — the rule is yours.

The validator does identical work in both cases. Only the provenance of the yardstick changed. A practical test: **can you run this check with nothing but your own database?** A weight of 1000 kg — yes. Diabetes prevalence matching the national rate — no.

One warning, because the wrong version circulates widely: *verification = conformance, validation = completeness + plausibility* is **not** what the framework says. The two axes genuinely cross. OHDSI's Data Quality Dashboard, the framework's reference implementation, populates all six cells.

## The real line is scope, not category

Here is the part most write-ups get wrong. The split is **not** "the validator does conformance, quality checks do the rest." FHIR validation reaches considerably further than that.

It does plausibility, as long as the question fits inside one resource. `minValue[x]` / `maxValue[x]` is literally a plausible-range constraint:

```json
{
  "path": "Observation.value[x]",
  "minValueQuantity": { "value": 0.5,  "unit": "kg" },
  "maxValueQuantity": { "value": 650,  "unit": "kg" }
}
```

It does temporal plausibility. Every `Period` in FHIR already carries `per-1`:

```
per-1: "If present, start SHALL have a lower or equal value than end"
       start.hasValue().not() or end.hasValue().not() or (start <= end)
```

And it does cross-field logic within a resource, via invariants:

```
obs-7: "If Observation.code is the same as a Observation.component.code
        then the value element associated with the code SHALL NOT be present"
```

So the validator is not confined to structure. What it cannot do is anything that needs a **population or a second resource**:

| The question needs… | Example | Validator |
|---|---|---|
| one resource | weight within 0.5–650 kg; `end` not before `start` | ✅ |
| a proportion | 40% of Observations have no value | ❌ no denominator |
| a tolerance | 5% missing is fine here, 0% required there | ❌ validity is binary |
| another resource | Observation dated before the patient's birth | ❌ references not resolved |
| all records | one MRN per patient | ❌ no dataset in view |
| a distribution | mean weight, prevalence, drift | ❌ needs a population |
| an external benchmark | prevalence matches the national rate | ❌ nothing to compare to |

Which gives the honest one-liner:

> **A validator answers every question that fits inside a single resource, and no question that does not.**

### The one case that sits exactly on the line

An element is `min=1` and absent. The validator reports it, and that looks like a completeness check. By Kahn it is **conformance** — a structural rule was violated, not a frequency expectation.

Real completeness is about *optional* elements populated so sparsely the column is useless. Same SQL either way; different category, different mechanism, decided entirely by whether the element was required.

## What only a dataset can answer

Everything that is a property of the dataset rather than of any record in it. In SQL on FHIR these are ordinary queries over a flattened view — the whole point being that they return the offending rows:

```sql
-- cross-record uniqueness: one MRN per patient
SELECT mrn FROM patient_flat
GROUP BY mrn HAVING count(DISTINCT id) > 1
```

```sql
-- referential integrity: subject points at a Patient that isn't here
-- (a Bulk FHIR export fails this more often than you'd think)
SELECT o.id, o.patient_id
FROM obs o LEFT JOIN pat ON o.patient_id = pat.id
WHERE o.patient_id IS NOT NULL AND pat.id IS NULL
```

```sql
-- cross-resource temporal: observed before the patient was born
SELECT o.id FROM obs o JOIN pat ON o.patient_id = pat.id
WHERE o.effective < pat.birth_date
```

Plus the ones with no single-record analogue at all: **proportions** (40% of values missing), **tolerances** (5% acceptable here, 0% there), **distributions** (mean, quantiles, outliers, drift), **timeliness** (newest record is six weeks old — nothing about any record is wrong, the dataset is stale), and the **roll-up** that becomes the dashboard.

None of this is a gap a better validator could close. It is a different question.

And the reverse deserves saying, because a clean split needs both halves: **SQL checks see only what a projection exposed.** The validator inspects the whole resource — unprojected elements, slicing, extensions, invariants over nested structures, ValueSet expansion with hierarchy. Checks neither do that nor should. A check that re-implements a profile constraint is a second source of truth, and it will drift from the first.

## Where Kahn runs out

Two gaps you hit immediately if you build on it.

**No timeliness.** "Is data still arriving?" is neither conformance, completeness nor plausibility. It is a real and common failure — Databricks' anomaly detection reduces to freshness and completeness — but the framework predates that framing. Either add a fourth category or file freshness under temporal plausibility and live with the awkwardness.

**No accuracy.** Plausibility is believability, not truth. Whether a recorded weight is the patient's actual weight is unanswerable from the data.

Two things OHDSI had to add in practice are worth adopting alongside the taxonomy: a **level** (dataset / column / code) and a **severity** (fatal / convention / characterization). Their severity split across 27 check types is instructive — 12 characterization, 8 convention, 7 fatal. **Most checks describe rather than judge**, which is a useful expectation to set before someone builds a dashboard that colours everything red.

## Where this leaves us

> A validator answers *"is this resource well-formed?"*
> A check answers *"is this dataset fit to use?"*

The second question is meaningless without the first, and the first is insufficient without the second. FHIR has an excellent answer to the first and, so far, no standard mechanism for the second.

The encouraging part is how little needs inventing. The vocabulary is settled. The reference implementation has run in OMOP-land for a decade. And in FHIR the pieces already exist: a ViewDefinition is the dataset, an SQLQuery is the check, and `relatedArtifact` already declares the dependency graph. What is missing is only the semantics — saying *this query is a check, this is what it measures, and this much failure is acceptable*. That is what the [SQL on FHIR data quality work](https://github.com/HL7/sql-on-fhir/issues/375) is about, and it is open.

---

**Sources**

- Kahn MG et al., *A Harmonized Data Quality Assessment Terminology and Framework for the Secondary Use of Electronic Health Record Data*, eGEMs 4(1):1244, 2016 — [PMC5051581](https://pmc.ncbi.nlm.nih.gov/articles/PMC5051581/). All quotations are from this paper.
- [OHDSI Data Quality Dashboard](https://github.com/OHDSI/DataQualityDashboard) — the 27-type classification matrix lives in `inst/csv/OMOP_CDMv5.4_Check_Descriptions.csv`.


---

---
{
  "title": "FHIR in Germany: Navigating ISiK, MII, and EHDS",
  "description": "A joint Health Samurai and Gefyra guide to the German FHIR landscape: institutions, laws, profile families, sector obligations, and what the EHDS changes for implementers.",
  "date": "2026-07-21",
  "author": "Valeria Fursa, Patrick Werner",
  "reading-time": "15 min read",
  "tags": ["FHIR Standard", "FHIR Profiling", "Compliance"],
  "tldr": "Germany turns FHIR adoption into a compliance question. gematik's ISiK, KBV and the MIOs, the MII Kerndatensatz, and now the EU's EHDS each layer their own constraints on FHIR R4. This joint Health Samurai and Gefyra guide maps the institutions, laws, and profile families — and what they mean for the infrastructure you build on.",
  "utm-campaign": "fhir_expert",
  "utm-content": "fhir-germany"
}
---

<div class="narrow" style="display: flex; flex-direction: column; align-items: center; gap: 14px; margin-bottom: 8px">
  <img src="/blog/static/fhir-adoption-in-germany/gefyra-logo.png" alt="Gefyra logo" width="190" style="width: 190px; height: auto" />
  <p style="margin: 0; font-size: 15px; color: var(--color-text-tertiary)">A joint guide by Health Samurai and <a href="https://gefyra.de/?utm_source=health-samurai&utm_medium=referral&utm_campaign=fhir-germany-guide&utm_content=en" target="_blank" rel="noopener">Gefyra GmbH</a></p>
</div>

<div class="not-prose my-8 rounded-2xl border border-primary bg-bg-secondary p-5 sm:p-6">
  <div class="flex flex-col sm:flex-row sm:items-center gap-4">
    <div class="flex items-center gap-4 flex-1">
      <svg viewBox="0 0 20 15" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" class="w-8 h-6 rounded shrink-0"><rect width="20" height="5" y="0" fill="#000"/><rect width="20" height="5" y="5" fill="#DD0000"/><rect width="20" height="5" y="10" fill="#FFCE00"/></svg>
      <div>
        <p class="typo-body16 font-semibold text-primary">Diesen Leitfaden gibt es auch auf Deutsch</p>
        <p class="typo-body14 text-text-secondary">Vollständige deutsche Ausgabe — ISiK, MII, EHDS und der komplette Profil-Stack.</p>
      </div>
    </div>
    <a href="/articles/de-de/fhir-adoption-in-germany" class="group/btn inline-flex items-center gap-2 rounded-md px-4 py-2.5 text-sm font-semibold bg-primary text-white hover:bg-primary-dark transition-colors duration-300 shrink-0 self-start sm:self-center" data-track="click" data-track-label="German version banner" data-track-category="cta">Auf Deutsch lesen <span class="transition-transform duration-300 group-hover/btn:translate-x-1"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14" /><path d="m12 5 7 7-7 7" /></svg></span></a>
  </div>
</div>

Germany is one of the most consequential FHIR markets in Europe. Not because the standard is unfamiliar, but because national infrastructure embeds FHIR into legally regulated workflows. Supporting FHIR R4 is the starting point. It is not the finish line.

This piece maps the institutions, laws, profiles, and sector-specific obligations that decide what FHIR adoption actually means in Germany right now. We wrote it for senior decision-makers who are scoping interoperability work for hospitals, ambulatory and pharmacy vendors, digital health applications, and research data platforms.

## The institutional landscape

Several coordinated bodies govern Germany's digital health infrastructure. Each shapes a distinct part of the FHIR ecosystem.

[gematik GmbH](https://www.gematik.de/) is the national agency for the telematics infrastructure. Its scope covers the electronic patient record (ePA) — Germany's nationwide patient-controlled health record infrastructure — the electronic prescription system (eRezept), the electronic certificate of incapacity for work (eAU), secure healthcare communication services, and the [ISiK](https://www.gematik.de/anwendungen/isik) hospital interoperability framework. gematik publishes [technical specifications](https://gemspec.gematik.de/), with current interoperability specifications typically realized as FHIR implementation guides.

[HL7 Deutschland e.V.](https://www.hl7.de) maintains the [German Base Profiles](https://simplifier.net/packages/de.basisprofil.r4) (de.basisprofil.r4). These provide foundational building blocks for national FHIR implementations by constraining international FHIR R4 for use in Germany. The base profiles define reusable patterns for core resources, data-types like identifiers or address structures, and terminology bindings, which are then adopted and extended by the responsible specification organizations such as gematik, KBV, or Medizininformatik-Initiative in their domain-specific implementation guides.

[Kassenärztliche Bundesvereinigung (KBV)](https://www.kbv.de/) is the self-governing body of physicians and psychotherapists participating in Germany's statutory health insurance system and defines interoperability requirements for ambulatory care workflows. Its subsidiary [Mio42 GmbH](https://mio42.de/) develops the Medical Information Objects (Medizinische Informationsobjekte, MIOs): standardized FHIR-based specifications for structured medical documents and patient-centered records, including the vaccination certificate, maternity record, and pediatric examination booklet.

The [Bundesinstitut für Arzneimittel und Medizinprodukte (BfArM)](https://www.bfarm.de/) plays a central role in Germany's digital health and interoperability landscape. Beyond operating the registry for digital health applications (DiGA), BfArM is responsible for national terminology and classification systems, including SNOMED CT, ICD-10-GM, OPS, and LOINC-related infrastructure. It also hosts the Forschungsdatenzentrum Gesundheit (FDZ Gesundheit), the federal infrastructure for secondary use of statutory health insurance data.

The [Robert Koch Institute (RKI)](https://www.rki.de/) runs DEMIS, Germany's national infectious disease reporting system, which relies on FHIR-based interfaces for interoperable public health reporting.

The [Medical Informatics Initiative (MII)](https://www.medizininformatik-initiative.de/), funded by the Federal Ministry of Education and Research (BMBF), coordinates the Kerndatensatz, a national core dataset for interoperable cross-institutional clinical research. The standard is implemented across the data integration centers of all German university hospitals and defines harmonized FHIR-based data structures organized into multiple domain-specific modules such as diagnoses, laboratory data, medication, consent, and genomics.

The Deutsche Rentenversicherung (DRV) operates rehabilitation reporting flows that lean more on FHIR every year. Across direct care, billing, public health, and research, those institutions are the ones deciding how FHIR applies on the ground.

## The EU layer: EHDS

The European Health Data Space ([EHDS](https://eur-lex.europa.eu/eli/reg/2025/327/oj)) Regulation, which entered into force in March 2025, adds a new European interoperability layer on top of Germany's existing national frameworks. German FHIR-based initiatives such as ePA, ISiK, MIOs, and the MII core dataset will increasingly need to align with emerging EU requirements for cross-border exchange, semantic interoperability, and secondary use of health data.

EHDS has two pillars. The primary-use framework enables cross-border access to health data for care delivery and citizen access through MyHealth@EU. The secondary-use framework governs access to health data for research, innovation, public policy, and regulatory purposes through HealthData@EU and a designated Health Data Access Body in each Member State. Beyond interoperability, EHDS also aims to establish a harmonized European framework and single market for electronic health record systems and digital health services.

The technical interoperability foundation is the European Electronic Health Record Exchange Format (EEHRxF), a FHIR-based framework incorporating specifications such as the [International Patient Summary](https://hl7.org/fhir/uv/ips/) (IPS). Regulatory obligations are introduced gradually over a phased implementation timeline:

* **March 2027**: Deadline for the European Commission to adopt key implementing acts defining the operational and technical details of the EHDS framework.
* **March 2029**: First priority group (patient summaries, ePrescriptions, eDispensations) must be exchangeable across Member States. Most secondary-use rules also start applying.
* **March 2031**: Second priority group (medical imaging studies and reports, lab results, hospital discharge reports) follows.

In parallel, EHR systems placed on the EU market will become subject to a new conformity assessment framework under EHDS. In practice, this introduces a regulatory interoperability and compliance regime comparable in spirit to CE marking for medical devices.

For Germany the institutional mapping is already visible. The national EHDS hub is being established jointly by gematik, BfArM, and the DVKA at GKV-Spitzenverband (the Deutsche Verbindungsstelle Krankenversicherung Ausland). gematik is responsible for the telematics-side technical infrastructure required to connect the German ePA ecosystem to European cross-border exchange services.

As EHDS implementation progresses, the ePA increasingly becomes part of a broader European interoperability network rather than a purely national infrastructure. On the secondary-use side, the Forschungsdatenzentrum Gesundheit (FDZ Gesundheit) at BfArM — established under the Gesundheitsdatennutzungsgesetz (GDNG) — is well positioned to support Germany's future Health Data Access Body responsibilities under EHDS.

For implementers, the consequence is dual-layer conformance. German Base Profiles, ISiK, KBV/MIO specifications, ePA, and MII Kerndatensatz all need to be reconciled with EEHRxF and emerging European core profiles. This alignment work is already visible: gematik's KIG, its interoperability coordination group, has identified EHDS compatibility as an explicit objective for the planned new German core profiles. If successful, these profiles could provide a shared national foundation that reduces project-specific mapping work between German and European specifications. In parallel, key national specifications — including gematik's ISiK framework, the MII Kerndatensatz, and KBV/MIO specifications — are increasingly being assessed and aligned with European requirements. For patient-summary scenarios, this also means alignment with IPS-based data models. Some of that mapping is straightforward. MII is already R4-based and comparatively close to international clinical exchange patterns. Other parts will require explicit translation work, particularly German-specific identifiers, address structures, extensions, and national value sets. For EHR and hospital information system vendors, the ability to manage both national and European compliance layers is likely to become an increasingly important factor in product design, certification, and procurement.

![The German FHIR profile stack: EU cross-border target EEHRxF on top, six national profile families and the planned German Core layer in the middle, German Base Profiles and HL7 FHIR R4 at the base](image-1.svg)

## The legal framework that turns FHIR into a buying decision

The bodies described above do not operate in a vacuum. Their authority and market impact come from specific legal mandates. The German national layer rests on several key laws.

The [Social Code, Book V (SGB V)](https://www.gesetze-im-internet.de/sgb_5) is the foundation of statutory health insurance, and the source of many digital infrastructure obligations. Two paragraphs are particularly load-bearing:

* [§301 SGB V](https://www.gesetze-im-internet.de/sgb_5/__301.html) governs structured billing and reporting from hospitals to statutory health insurers.
* [§373 SGB V](https://www.gesetze-im-internet.de/sgb_5/__373.html) provides the legal basis for ISiK, gematik's mandatory hospital interoperability framework.

The Digital-Gesetz (DigiG), enacted in 2024, accelerated the digitalization of the statutory system. Its most visible consequence: the opt-out model for the electronic patient record. Every statutorily insured person now receives an ePA automatically unless they actively object. The ePA stopped being a technically available option. It became a default channel for clinical data.

The Gesundheitsdatennutzungsgesetz (GDNG), also enacted in 2024, governs the secondary use of health data. It strengthens the legal basis for the Forschungsdatenzentrum Gesundheit (FDZ Gesundheit) at BfArM and creates a regulated path for research, quality assurance, and other secondary uses of health data, including statutory insurance data and, under defined conditions, ePA-derived data.

The [Krankenhauszukunftsgesetz (KHZG, Hospital Future Act)](https://www.bundesgesundheitsministerium.de/krankenhauszukunftsgesetz) was the major funding instrument for hospital digitalization in Germany. Through the Krankenhauszukunftsfonds, it created a €4.3 billion funding framework for hospital digitalization, covering areas such as patient portals, digital documentation, medication management, clinical decision support, information security, and interoperability. Although the main funding period has passed, KHZG continues to shape hospital IT investment through implementation requirements, reporting obligations, and financial deductions for hospitals that fail to provide and use required digital services. On the hospital side, KHZG remains an important implementation and compliance driver for interoperable and FHIR-capable infrastructure.

![Which law empowers which institution: German laws on the left mapped to the institutions they empower, with HL7 Deutschland as the standards body alongside](image-2.svg)

### Consolidated timeline

* January 1, 2024: eRezept becomes mandatory for statutory outpatient prescriptions and pharmacy dispensing.

* 2024: DigiG and GDNG enacted, providing the legal scaffolding for *ePA für alle* and secondary health data use.

* January 15, 2025: ePA opt-out rollout begins with automatic creation for non-objecting insured persons; nationwide availability for healthcare providers followed on April 29.

* 2025/2026: KHZG digitalization deductions are determined based on required digital services and begin to affect reimbursement for non-compliant hospitals.

* Ongoing: gematik continues to publish successive ISiK stages and module versions.

![Two compliance clocks running in parallel: Germany's national digital push in 2024-2025 and the EU EHDS rollout through 2031](image-3.svg)

## The profile stack

Most ambiguity in the German FHIR landscape comes from how profiles are layered. The practical question is which profile family applies in which context, and which national constraints take precedence.

**The [German Base Profiles](https://simplifier.net/packages/de.basisprofil.r4)**, maintained by HL7 Deutschland, sit closest to the base specification. They provide reusable national building blocks for resources and data types, including identifiers such as KVNR, BSNR, and LANR, address structures, and terminology bindings. Almost every downstream profile family builds on them.

**[ISiK profiles](https://www.gematik.de/anwendungen/isik)**, published by gematik, define the mandatory hospital interfaces under §373 SGB V. They build on the German Base Profiles and add hospital-specific constraints for areas such as patient administration, encounters, diagnoses, procedures, and medication.

**KBV** is central to Germany's statutory outpatient care system and the professional rules around ambulatory prescribing, including the ambulatory workflow context of eRezept. In practice management systems, KBV-related requirements are a dominant conformance family. The technical E-Rezept infrastructure and FHIR-based exchange specifications, however, are defined within the gematik/TI specification landscape.

**[ePA](https://www.gematik.de/anwendungen/epa) and [eRezept](https://www.gematik.de/anwendungen/e-rezept) profiles** define data flows and APIs within the telematics infrastructure. For eRezept, the profile stack is split: KBV defines the prescription data profiles for the electronic medicinal prescription, while gematik defines the eRezept workflow, TI infrastructure, and FHIR-based exchange specifications. Together, these cover prescription data objects, ePA document and metadata exchange, medication-related data, and other TI-specific services. Related TI applications and artifacts include the electronic medication plan (eMP), the emergency data set (NFD), and the electronic certificate of incapacity for work (eAU), although these should not all be treated as a single profile family.

**[MIOs](https://mio.kbv.de/)**, developed by Mio42 on behalf of KBV, are standardized FHIR-based specifications for structured medical documents in the ePA context. They build on German national profiling conventions, including the German Base Profiles, and define patient-centered record content such as vaccination, maternity, dental bonus, and pediatric examination documentation.

**The [MII Kerndatensatz](https://www.medizininformatik-initiative.de/en/medical-informatics-initiatives-core-data-set)** defines the FHIR profiles used by data integration centers across German university hospitals for research and secondary use. It is a central conformance target for academic clinical research data exchange in Germany.

**[RKI DEMIS](https://www.rki.de/DE/Themen/Infektionskrankheiten/Meldewesen/DEMIS/demis-node.html)** defines the FHIR profiles for infectious disease reporting under the Protection Against Infection Act (Infektionsschutzgesetz, IfSG), enabling standardized electronic reporting to public health authorities.

## Sector-by-sector obligations

The table below summarizes how the institutional, legal, and profile layers stack up by sector. It is not exhaustive (most production systems span several rows), but it captures the dominant compliance vector for each kind of buyer.

| Sector | Primary profile families | Legal basis | Primary driver |
| :---- | :---- | :---- | :---- |
| Hospitals | German Base, ISiK, MII (research-active sites), §301-aligned reporting | §373 SGB V, §301 SGB V, KHZG | TI participation, KHZG implementation requirements and reimbursement-deduction risk |
| Ambulatory practices | German Base, KBV, ePA, eRezept | SGB V, DigiG | Statutory insurance participation, eRezept mandate, ePA integration |
| Pharmacies | German Base, eRezept, TI-related specifications | SGB V | Prescription dispensation, TI participation |
| DiGA developers | German Base, ePA-aligned data export | §33a SGB V, DiGAV | BfArM listing requirements |
| Public health & labs | RKI DEMIS profiles | IfSG | Statutory disease notification |
| University hospitals & research | MII Kerndatensatz | GDNG, BMBF funding, MII governance | Federated research, secondary use, FDZ access |

## Hospitals: the KHZG-driven decade

For hospitals, FHIR adoption is shaped less by a single mandate than by overlapping pressures: KHZG-funded digitalization projects, TI/ePA integration, research requirements, vendor roadmaps, and the emerging ISiK conformance framework. KHZG-funded services — including patient portals, electronic medication management, clinical decision support, structured documentation, information security, and interoperability — created a major investment push for structured hospital IT. Not every KHZG requirement maps directly to FHIR or ISiK, but many resulting procurement and integration decisions favor systems that can support structured, interoperable, and increasingly FHIR-capable data flows.

ISiK is structured in successive stages and modules rather than as a single static profile set. It is gematik's FHIR-based specification framework for open and standardized hospital interfaces under §373 SGB V, covering areas such as patient administration, clinical documentation, medication, diagnostics, forms, and other care-related workflows. The exact applicability depends on the relevant ISiK module, software category, and confirmation requirements.

§301 SGB V adds a separate administrative billing and reporting layer. It is not a FHIR mandate, but it shapes hospital data models and integration requirements through standardized billing, department, and payer-facing reporting structures. In practice, hospitals often need to reconcile these administrative structures with ISiK-based clinical interfaces and internal system models.

Hospitals that participate in research carry an additional layer. Their data integration centers typically need to support the MII Kerndatensatz for cross-institutional research and secondary use. Research-side and care-side data flows do not automatically share the same infrastructure or conformance targets, so academic medical centers often end up managing overlapping FHIR responsibilities across care delivery and research.

## Ambulatory and pharmacy

For ambulatory physicians, the operative obligations come through participation in Germany's statutory health insurance system. The eRezept mandate, in force since January 2024, requires structured electronic prescription workflows. Its FHIR profile stack is shared across several actors: KBV defines the professional prescription data profiles for ambulatory prescribing, while gematik defines the E-Rezept workflow, TI infrastructure, and FHIR-based exchange specifications. Practice management systems are therefore shaped by KBV requirements, eRezept workflow integration, and gematik/TI conformance.

The ePA adds a second layer. Medication-related ePA services, including the electronic medication plan context, are implemented through FHIR-based specifications and affect how physicians and pharmacists interact with structured medication information. MIOs define standardized FHIR-based document structures for specific ePA use cases. Depending on which MIOs a practice system supports, they can add additional profile requirements beyond eRezept and core ePA integration.

Pharmacies sit at the dispensing endpoint of eRezept. They must support the relevant E-Rezept and TI specifications for retrieving prescriptions, redeeming them, dispensing medication, handling substitution where applicable, and producing the required dispense and billing-related data flows.

## DiGA: digital health applications

Digital Health Applications [listed by BfArM](https://diga.bfarm.de/de) under §33a SGB V are reimbursed by statutory insurance. They must meet defined interoperability requirements, including machine-readable data export and ePA-related data transfer where applicable. Translation: DiGA vendors need structured export capabilities, increasingly aligned with FHIR-based specifications such as the DiGA/MIO Toolkit, so app-generated data can be reused in the ePA context or by treating providers.

## Public health, rehabilitation, and cross-sector care

Infectious disease reporting flows through RKI DEMIS using FHIR profiles for standardized electronic reporting under the Protection Against Infection Act (Infektionsschutzgesetz, IfSG). Healthcare providers and laboratories submitting notifiable conditions have to conform to the relevant DEMIS profiles.

Care doesn't stop at sector boundaries. Transitions between hospitals, ambulatory practices, rehabilitation, and home care require interoperability across systems with different legal, organizational, and technical constraints. Where FHIR is used, shared national profiling conventions such as the German Base Profiles provide a common foundation, but context-specific specifications or extensions are usually still needed.

## Research and secondary use

The Medical Informatics Initiative defines the Kerndatensatz, a national core dataset agreed across German university hospitals and implemented through FHIR profiles for cross-institutional research and secondary use. Data integration centers at university medical sites transform and harmonize clinical routine data according to MII specifications and make it available for federated feasibility queries and research data access through the Forschungsdatenportal Gesundheit (FDPG), under the applicable governance and consent frameworks.

The GDNG and the FDZ Gesundheit at BfArM extend the secondary-use model beyond academic medicine into statutory insurance data and, under defined conditions, ePA-derived data. They open structured pathways for research and other public-interest uses of health data. Read MII and GDNG side by side and the direction is clear: FHIR has become a central interoperability substrate for academic medical research in Germany, while secondary-use infrastructure is expanding into the statutory and national health-data domain. For university hospitals and MII-connected research sites, this creates an additional FHIR conformance layer alongside care-side obligations such as ISiK.

## Terminology: an underestimated constraint

Terminology binding is one of the most frequent sources of late-stage non-conformance in German FHIR projects. The relevant code systems include:

* ICD-10-GM and OPS, the German modifications of ICD-10 and the procedure classification, maintained by BfArM.

* ATC (anatomical-therapeutic-chemical), and PZN (Pharmazentralnummer) for pharmaceutical classification and product identification.

* LOINC, especially for laboratory results and structured clinical data, including document section coding.

* SNOMED CT, available under Germany's national license since 2021 and administered through BfArM as the national release center; it is increasingly mandatory in MII and other semantically rich profiles.

* ZTS (Zentraler Terminologieserver) which provides FHIR terminology resources used across German implementation guides, including value sets, code systems, ICD-10-GM, OPS, and other terminology artifacts governed or released by national terminology owners such as BfArM.

Pick infrastructure without a serious answer to terminology services (value set expansion, code validation, code translation, version management), and project rework becomes the most reliable forecast in this market.

## Cross-cutting compliance: hosting, security, and interconnection

For German health data, the technical conformance question is inseparable from compliance posture. Three constraints recur in nearly every procurement:

* BSI IT-Grundschutz and the BSI C5 (Cloud Computing Compliance Criteria Catalog) attestation, especially for cloud-hosted infrastructure. Under [§393 SGB V](https://www.gesetze-im-internet.de/sgb_5/__393.html), cloud services used in healthcare require a C5 attestation or comparable certification.

* EU data processing and hosting expectations, with stricter German-hosting or sovereignty requirements depending on the use case, especially for ePA- or TI-adjacent data flows.

* GDPR (DSGVO), applied in healthcare with sector-specific guidance from the Bundesbeauftragte für den Datenschutz und die Informationsfreiheit (BfDI) and the state data protection authorities.

Connection to the telematics infrastructure brings extra dependencies: the Konnektor or TI Gateway, eHBA (electronic health professional card), SMC-B (institution card), and the KIM and TI-Messenger families for secure messaging. FHIR systems used in regulated workflows are usually operated alongside, sometimes behind, these components.

## What this means for implementation infrastructure

Reading the German landscape side by side, a few structural requirements emerge for the FHIR backend that sits behind any system serving this market. They aren't abstract preferences. They're the failure modes that show up in production when teams underestimate how much of the German market sits inside regulated workflows.

* First-class support for layered profile validation. A hospital system may need to validate similar clinical data against different profile families depending on the workflow — for example ISiK for care-side interfaces and MII for research-side exchange — with the German Base Profiles in the dependency tree. Validation has to be deterministic and performant with hundreds of profiles loaded simultaneously.

* EEHRxF and EHDS interoperability components for dual-layer conformance. EHDS is not only a cross-border exchange requirement. Electronic health record systems placed on the EU market will need to support the European interoperability and logging components required under the EHDS framework. For German implementations, this means that national profile families such as ISiK, ePA, KBV/MIO, and MII must be designed with a path toward EEHRxF and emerging European core profiles. For patient-summary scenarios, this means alignment with the European Patient Summary and its IPS-based data model.

* Profile package management and version pinning. German FHIR profile packages are updated frequently across different specification families. Production systems need to load specific package versions, switch them per tenant or workflow, and reproduce historical validations on demand.

* Native terminology services covering German code systems. ICD-10-GM, OPS, SNOMED CT under the national license, LOINC, ATC, and ZTS-provided terminology resources all need to be queryable, expandable, versioned, and available to the validation pipeline.

* Multi-tenant architecture for vendors operating services across multiple practices, hospitals, or DiGA customer environments under a single operational footprint.

* Compliance-grade hosting in Germany or the EU, with C5 and BSI IT-Grundschutz aligned operational practices.

* Audit logging, consent representation, and SMART on FHIR support, including reproducible access logs, machine-readable consent states, and standards-based third-party app access where applicable.

* A clear path for connecting to and operating alongside telematics infrastructure components when the workflow demands it.

## Closing

Germany rewards teams that approach FHIR not merely as a technology layer but as part of the compliance substrate. The institutions, laws, and profile families described here are not separate concerns. They are the same problem, viewed from different angles. The readiness of the underlying infrastructure determines how much of that complexity reaches application teams, and how much of the budget can go into the user-facing work that actually differentiates a product.

At Gefyra, we work directly in the German and European FHIR specification landscape. Our team has contributed to and advised on national specification work including ISiK, further gematik specifications, the German Base Profiles, KBV/Mio42 specifications, and the MII core dataset. We are also deeply involved in the European interoperability context, including implementation guide facilitation and standards coordination through HL7 Europe working groups. That perspective allows us to assess FHIR infrastructure independently of any single vendor stack.

At Health Samurai, we build and operate [Aidbox](https://www.health-samurai.io/fhir-server), the FHIR platform behind a number of regulated healthcare workloads in Europe and worldwide. Scoping FHIR infrastructure for the German market, whether for a hospital, a practice-management or pharmacy system vendor, a DiGA, or a research data platform? Get in touch with [Health Samurai for Aidbox infrastructure](https://www.health-samurai.io/contacts), or with [Gefyra](mailto:sh@gefyra.de) for independent specification, profiling, and German/EU interoperability guidance. We're happy to walk through profile coverage, terminology support, hosting options, and reference architectures.


---

---
{
  "title": "Calculating CMS Quality Measures as SQL on FHIR — No CQL Engine Required",
  "description": "Run CMS/HEDIS eCQMs as SQL on FHIR — no CQL engine. ViewDefinitions and SQLQuery Libraries pack into one FHIR package you can move to any SQL-on-FHIR server, making measure calculation portable and standard.",
  "date": "2026-07-20",
  "author": "Aleksandr Kislitsyn",
  "reading-time": "11 min read",
  "tags": [
    "SQL on FHIR",
    "Analytics",
    "Compliance"
  ],
  "seo-tags": [
    "SQL on FHIR",
    "Quality Measures",
    "eCQM"
  ],
  "tldr": "CQL-authored CMS quality measures can be executed as plain SQL on Aidbox/PostgreSQL via SQL on FHIR — no CQL engine at runtime. ViewDefinitions flatten FHIR into tables, ValueSet membership becomes a JOIN, and each measure is one SQL query packaged as a SQLQuery Library and served through the standard Measure/$evaluate-measure API. A full working example (a dozen CMS measures) is on GitHub.",
  "utm-campaign": "analytics",
  "utm-content": "cms-measures-sql"
}
---

## The problem with running eCQMs

Electronic Clinical Quality Measures (eCQMs) are how programs measure care quality — whether eligible patients got their colorectal screening, whether their hypertension is controlled. The CMS and HEDIS measures every value-based-care program reports on are authored in [CQL](https://cql.hl7.org/) (Clinical Quality Language), and the usual way to execute them is a dedicated **CQL engine**: a separate runtime that parses the measure, calls back into your FHIR server for terminology and patient data, and returns a `MeasureReport`.

This works well, but at a cost:

- **A separate compute tier.** A CQL engine is its own runtime — you deploy and scale it alongside your FHIR server, and it does the population computation outside the database. That is real infrastructure, and a scaling concern for large populations.
- **Hard to explain.** When a patient shows up as a care gap, can you show a clinician exactly *why*? That means tracing engine internals and value-set expansions — not reading a query.

So here is the question this post answers: **what if the measure logic were just SQL, running where your data already lives?**

It turns out it can be — and the result is more than a performance trick: with SQL on FHIR, the entire knowledge of how to calculate a measure becomes **portable, standard FHIR artifacts** you can pack into a FHIR package and install on another server. This is an engineering walkthrough of how, with a full working example you can clone and run.

## The idea: measures are set logic, and SQL is a set language

A quality measure is fundamentally set arithmetic over a patient population:

- **Initial Population** — who is eligible (age, encounters, a condition).
- **Denominator / Exclusions** — who is counted, minus who is carved out (hospice, palliative care, frailty…).
- **Numerator** — who met the measure (a screening, a controlled reading).
- **Score** — `numerator / (denominator − exclusions)`.

Every one of those is a set of patients. SQL is very good at sets. The only thing standing between FHIR data and a SQL query is that FHIR resources are deeply nested JSON, and terminology membership ("is this code in the value set?") is not a column you can filter on. [SQL on FHIR](https://www.health-samurai.io/docs/aidbox/modules/sql-on-fhir) solves both.

```mermaid
flowchart TD
    A["Aidbox — FHIR JSONB"] -->|ViewDefinition +<br/>$materialize| B["sof.* flat tables<br/>patient, encounter, condition, observation…"]
    C["ValueSet expansions"] -->|flatten| D["concepts table<br/>(valueset_url, system, code)"]
    B --> E["Measure SQL (CTEs)<br/>IP → exclusions → numerator"]
    D --> E
    E --> F["MeasureReport"]
```

## Layer 1: flatten FHIR into tables with ViewDefinitions

A [ViewDefinition](/blog/what-is-a-viewdefinition) is a FHIR resource that describes how to flatten a resource type into a flat table. Point one at `Encounter`, materialize it, and you get a tidy `sof.encounter_flat` table with `patient_id`, `type_system`, `type_code`, `status`, `period_start`, and so on — your measure logic reads plain columns instead of digging through nested FHIR structures.

The example ships one ViewDefinition per resource type the measures touch — patient, encounter, condition, observation, procedure, and a few more — each materialized into the `sof.*` schema and exposed through a thin wrapper view. That shared flat layer is reused by every measure.

## Layer 2: terminology as a JOIN, not an $expand

The other hard part of a measure is code-set membership: "is this encounter one of the seven kinds of qualifying visit?" In a CQL-engine setup that is typically resolved at runtime against a terminology service. In SQL on FHIR you flatten the [ValueSet](https://www.health-samurai.io/docs/aidbox/terminology-module/fhir-terminology/valueset) expansions **once** into a `concepts` table — one row per `(valueset_url, system, code)` — and membership becomes an ordinary join:

```sql
JOIN concepts c
  ON  c.system = e.type_system
  AND c.code   = e.type_code
  AND c.valueset_url = 'http://cts.nlm.nih.gov/fhir/ValueSet/…'
```

No network round-trip, no per-patient expansion. The value set is expanded offline and indexed, so the "does this patient's code count?" check is set-based and fast.

## Layer 3: the measure is one SQL query

With flat tables and a `concepts` join available, a whole measure becomes a single query made of CTEs — and it reads remarkably close to the measure's plain-English definition. Here is the Initial Population of **CMS130 (Colorectal Cancer Screening)** — patients aged 46–75 with a qualifying encounter during the measurement period:

```sql
WITH mp AS (
  SELECT '2026-01-01'::timestamptz AS mp_start,
         '2026-12-31'::timestamptz AS mp_end
),

qualifying_encounters AS (
  SELECT DISTINCT e.patient_id
  FROM encounter_flat e
  JOIN concepts c
    ON  c.system = e.type_system
    AND c.code   = e.type_code
    AND c.valueset_url IN (
      -- ValueSet URLs shortened for readability; full URLs in the example repo
      'http://cts.nlm.nih.gov/fhir/ValueSet/…',  -- Office Visit
      'http://cts.nlm.nih.gov/fhir/ValueSet/…',  -- Annual Wellness Visit
      '…'                                        -- + 5 more
    )
  CROSS JOIN mp
  WHERE e.status = 'finished'
    AND e.period_start BETWEEN mp.mp_start AND mp.mp_end
),

initial_population AS (
  SELECT p.id AS patient_id
  FROM patient_flat p
  CROSS JOIN mp
  WHERE EXTRACT(YEAR FROM AGE(mp.mp_end, p.birth_date::date)) BETWEEN 46 AND 75
    AND p.id IN (SELECT patient_id FROM qualifying_encounters)
)
-- … denominator exclusions, numerator, and the final score follow as more CTEs
```

The numerator adds a few more CTEs (colonoscopy within 9 years, FOBT during the period, and so on), exclusions union hospice/palliative/frailty, and a final `SELECT` computes the score. The point: **the measure is legible.** You can read it, diff it against the spec, and `SELECT * FROM initial_population` to see exactly who qualified.

### Shared logic stays shared

Exclusions like hospice, palliative care, and advanced-illness-with-frailty recur across many measures. In the example they are factored out into reusable pieces rather than copy-pasted, so fixing the hospice logic fixes it everywhere at once.

### But who writes all this SQL?

The obvious objection: hand-translating a measure's CQL into SQL sounds like a lot of careful, error-prone work — and there are dozens of eCQMs. In practice, this is exactly the kind of task modern AI coding assistants handle well. CQL and SQL are both structured, well-specified languages, and a measure's logic maps cleanly onto the CTE pattern shown above (initial population → exclusions → numerator → score).

That is not a hope — it is how the example itself was built. Each measure was translated from its published CQL to SQL with an AI assistant, then **verified against CMS's reference `MeasureReport` fixtures patient-by-patient** until the numbers matched exactly. The AI does the mechanical translation; the fixtures keep it honest.

{% hint style="info" %}
The reference fixtures are what make AI-assisted translation trustworthy: expected `MeasureReport` results for the test patients are published alongside the measure content in the [dqm-content-qicore-2025](https://github.com/cqframework/dqm-content-qicore-2025) repository, so every translated measure is checkable against ground truth — not accepted on faith.
{% endhint %}

## Making it standards-conformant: SQLQuery + Measure/$evaluate-measure

Running SQL is fine internally, but the goal is a conformant FHIR service, not a database script. Two SQL-on-FHIR pieces close that gap:

- Each measure's SQL is stored in Aidbox as a **SQLQuery `Library`** resource (a SQL-on-FHIR profile on `Library`) and invoked with the `$sqlquery-run` operation. The calculation logic lives *in the FHIR server*, as first-class resources — not in application code.
- The service answers the standard FHIR R4 [`Measure/$evaluate-measure`](https://hl7.org/fhir/R4/operation-measure-evaluate-measure.html) operation and returns a proper `MeasureReport` — so any FHIR client consumes it the same way it would a CQL-engine result.

Each SQLQuery Library also declares `depends-on` lineage to the ViewDefinitions it reads, giving you a queryable graph: **measure → views → resources.** Everything a measure needs — the flat views, the terminology, the calculation — is now expressed as standard FHIR resources living in the FHIR server. Which sets up the real payoff.

```mermaid
flowchart LR
    Client -->|"POST /Measure/<br/>$evaluate-measure"| Aidbox
    Aidbox -->|routes to| App["evaluate-measure app"]
    App -->|"$sqlquery-run"| Lib["SQLQuery Library<br/>(the measure SQL)"]
    Lib -->|reads| SOF["sof.* views + concepts"]
    App -->|builds| MR["MeasureReport<br/>→ back to the client"]
```

The evaluate-measure app in the middle holds no measure logic: it resolves the right SQLQuery Library, invokes `$sqlquery-run`, and shapes the returned rows into a `MeasureReport`. The computation itself runs in the database.

## The payoff: the whole measure suite is a portable FHIR package

Because every part of a measure is now a standard FHIR resource, the entire suite packs into **one FHIR NPM package**: terminology (CodeSystems + ValueSets), the ViewDefinitions that flatten the data, and the SQLQuery Libraries that hold the calculation logic. In the example that package carries 170 resources across a dozen measures — 10 CodeSystems, 107 ValueSets, 10 ViewDefinitions, and 43 SQLQuery Libraries.

The package format is standard — the same FHIR NPM format Implementation Guides ship in — so any FHIR server can load it with its own package-install mechanism; in Aidbox that is a [`$fhir-package-install`](https://www.health-samurai.io/docs/aidbox/reference/package-registry-api#fhir-package-install) call at server boot, and the definitions are simply *there*. And here is the part that matters: **that package is not tied to Aidbox.** It is standard SQL-on-FHIR building blocks — ViewDefinitions and SQLQuery Libraries defined by the [SQL on FHIR](https://build.fhir.org/ig/HL7/sql-on-fhir/) specification. Move the package to any FHIR server that implements these building blocks, install it, and the same measures compute the same way. How each server executes them is an implementation detail — some run a separate SQL-on-FHIR engine, others execute in-database.

```mermaid
flowchart LR
    Pkg["FHIR package<br/>terminology + ViewDefinitions + SQLQuery Libraries"]
    Pkg -->|"package install"| S1["FHIR server A<br/>(SQL on FHIR)"]
    Pkg -->|"package install"| S2["FHIR server B<br/>(SQL on FHIR)"]
    Pkg -->|"package install"| S3["…any SQL-on-FHIR server"]
```

Aidbox takes the in-database path: the ViewDefinitions materialize into tables or views and the SQLQuery Libraries execute as native SQL, **directly in its PostgreSQL** — right where the data already lives. So the package installs and runs with nothing extra to stand up for the computation itself: no execution engine to deploy, scale, or keep in sync. The measure logic distributes the same way an Implementation Guide does — as standard, shareable FHIR artifacts.

## Investigating a result

Remember the "hard to explain" problem? It gets a direct answer here. Ask for a single patient, and the `MeasureReport` carries an `evaluatedResource` array: real FHIR resource references, each tagged with the population it satisfies via the standard `cqf-criteriaReference` extension.

```json
"evaluatedResource": [
  {
    "reference": "Encounter/abc",
    "extension": [
      { "url": ".../cqf-criteriaReference", "valueString": "initial-population" },
      { "url": ".../cqf-criteriaReference", "valueString": "denominator" }
    ]
  },
  {
    "reference": "Procedure/xyz",
    "extension": [
      { "url": ".../cqf-criteriaReference", "valueString": "numerator" }
    ]
  }
]
```

"Why is this patient in the numerator?" — *this* Procedure. "Why excluded?" — *this* hospice encounter. Every reference resolves to a resource that exists in the server, so the next step of the investigation is just a `GET`.

For population-scale investigation, each measure also ships an `-evidence` SQLQuery Library: one row per patient with the full decision chain — which pathway satisfied the numerator (colonoscopy vs. FOBT vs. none), the triggering resource with its code and date, and which exclusion fired. That is a care-gap worklist — *who is missing screening, and what exactly is missing* — as a single query. The example's demo app is these queries made clickable: a cross-measure worklist, a per-patient 360 view with evidence drill-down, and exportable outreach lists.

![Patient 360 view in the demo app: the CMS130 decision chain with a per-CTE verdict for every population step, and the qualifying encounter shown as evidence](image-1.png "Patient 360 in the demo app: an open colorectal-screening gap, the full CMS130 decision chain (one verdict per CTE), and the evidence resource behind the patient's Initial Population membership.")

And when a number still looks wrong, every population is a named CTE: `SELECT * FROM initial_population` and narrow it down step by step — no engine internals to trace.

## Run it yourself

Everything above is a working, open-source example in the Aidbox examples repository — a dozen CMS measures (CMS130, CMS165, CMS125, CMS131, and more) with sample patient data and an interactive demo app.

**→ [github.com/Aidbox/examples · aidbox-custom-operations/measure-evaluate](https://github.com/Aidbox/examples/tree/main/aidbox-custom-operations/measure-evaluate)**

Follow the instructions in the README to run the whole stack locally: start Aidbox with the measure package installed at boot, load the sample dataset, calculate the measures through the standard `Measure/$evaluate-measure` operation, and explore each patient's evidence in the demo app UI. What comes back is a plain FHIR `MeasureReport` with the population counts and score — computed by SQL, inside Aidbox, with no CQL engine anywhere in the stack.

![Demo app Overview: twelve CMS measure cards with scores, and a sidebar summarizing patients with gaps and open gaps](image-2.png "The demo app's Overview: a dozen CMS measures computed by SQL, with scores and open-gap counts across 530 sample patients.")

## Takeaway

CQL is a fine authoring language for quality measures. But it does not have to be your *execution* engine. When you flatten FHIR with ViewDefinitions, turn terminology into a join, and express each measure as a SQLQuery Library behind `Measure/$evaluate-measure`, the measure logic stops being locked inside an engine and becomes what SQL on FHIR promises: **standard FHIR artifacts you can package once and run anywhere.** Ship the whole suite as a FHIR package, install it on any SQL-on-FHIR server, and the same measures compute the same way — explainable down to the individual resource. On Aidbox, they run natively in PostgreSQL, so there is no separate execution engine to operate at all. And getting there is more approachable than it sounds: AI assistants translate the CQL to SQL, and CMS's own reference fixtures verify every measure against expected results.

> Interested in trying this measure-calculation approach on your own data? [Reach out to us](https://www.health-samurai.io/contacts?utm_source=article&utm_medium=blog&utm_campaign=cms-measures-sql) — we would be glad to walk you through it.


---

---
{
  "title": "Reducing Complexity in Patient Intake Forms with FHIR SDC",
  "description": "How conditional logic, carefully selected required fields, collapsible sections, response amendments, and data extraction turn large patient intake forms into clear clinical workflows.",
  "date": "2026-07-16",
  "author": "Aleksei Serednev",
  "reading-time": "8 min read",
  "tags": ["Forms", "FHIR SDC", "Patient Experience", "Data Extraction"],
  "utm-campaign": "feature",
  "utm-content": "patient-intake-sdc"
}
---

## The Problem with Traditional Patient Intake Forms

Many people know this situation: you visit a clinic for the first time and receive a five-page patient intake form. You have to answer dozens of questions, even though some of them clearly do not apply to you.

Digital medical forms based on FHIR SDC can help solve this problem. However, simply moving a form from paper to a screen does not automatically make it better. A digital form can still be long, poorly structured, and difficult to understand. In some cases, it may even feel more confusing than the paper version. Paper, at least, does not open another section when you click the wrong button.

Complex wording and irrelevant sections both make the experience worse. After the patient submits the form, a clinician may ask the same questions again because finding the answers inside a large and complicated form takes too much time.

This matters because an intake form is often one of the first points of contact between a patient and a clinic. Respect for patients is not limited to medical care or friendly service. It also includes the way information is collected.

A good intake form should therefore be treated as part of the overall patient experience. Even a simple and clear form can create a positive tone for the patient's next interactions with the clinic.

![Traditional patient intake form with a long and complex structure](traditional-intake-form.png)

## A Form Should Be Dynamic, Not Static

What can make the form-filling process more comfortable?

The first and most important step is to make the form dynamic. Patients should not have to answer questions that are not relevant to them. In fact, they do not even need to see those questions.

FHIR `Questionnaire` supports conditional logic through `enableWhen`. It controls whether a question or group is enabled based on an answer given elsewhere in the form. FHIR SDC also provides `enableWhenExpression` for more advanced conditions that cannot be easily represented by the standard `enableWhen` structure. Disabled items are normally hidden or made unavailable, and their required rules do not apply while they are disabled.

In Formbox, form authors can configure these conditions through a visual condition builder. Simple rules can be created without writing FHIRPath. For more complex logic, the builder can convert conditions into FHIRPath, while an expression editor remains available when direct control is needed.

This means that a form author can create both simple conditions and larger combinations of `AND` and `OR` rules without asking every team member to become a FHIRPath expert overnight.

### A Simple `enableWhen` Example

Consider a section about tobacco and nicotine use.

The form first asks:

> Do you currently use tobacco or nicotine-containing products?

When the patient selects **Yes**, the form can display additional questions, such as:

* How many cigarettes do you smoke per day?
* How many years have you used these products?
* Which tobacco or nicotine products do you use?

When the patient selects **No**, the entire follow-up section remains hidden.

A patient who does not smoke should not have to answer how many cigarettes they smoke per day. The correct number may be zero, but asking the question still adds unnecessary work and makes the form look longer than it really is.

With conditional display, the initial form looks cleaner and less intimidating. The patient sees only the questions that are relevant to the current situation.

The same approach can be applied to many other sections:

* Pregnancy-related questions
* Previous surgeries
* Allergies
* Current medications
* Family medical history
* Alcohol or substance use
* Details about a specific symptom

For example, there is no reason to display fields for the date and type of a previous surgery until the patient confirms that a previous surgery exists.

![Conditional display using enableWhen in a patient intake form](enable-when-example.png)

## Required Questions Should Be Used Carefully

Another important factor is the number of required questions.

A patient intake form should contain as few required questions as reasonably possible. A question should normally be required only when the clinic truly needs the answer to continue the workflow, provide safe care, identify the patient, or meet a legal or operational requirement.

Making every field required may appear to improve data completeness. In practice, it can produce the opposite result. Patients who do not understand a question may enter an approximate or incorrect answer simply because the form does not allow them to continue.

FHIR defines `required` at the question or group level. An enabled required item must be answered before the `QuestionnaireResponse` can be marked as completed. However, when an item is disabled by conditional logic, its required constraint is ignored. This combination is useful: a question can be required when it is relevant without being required for every patient.

For example, the name of a medication may be required after the patient confirms that they take medication. It should not block patients who have already selected **I do not currently take any medication**.

![Required questions configured only when clinically relevant](required-questions.png)

## Do Not Show the Whole Form at Once

Conditional logic is not the only way to reduce visual complexity. The form can also be divided into clear groups that patients open as they move through the process.

A practical initial state could look like this:

* The **Patient details** section is expanded.
* The remaining sections are collapsible.
* Those later sections are collapsed by default.

Formbox supports collapsible items and lets the form author choose whether their initial state is **Collapsed** or **Expanded**.

This does not reduce the actual number of questions, but it reduces the amount of information shown on the screen at one time. Instead of seeing one enormous questionnaire, the patient sees a small set of manageable steps.

### Why This Matters

Showing fewer questions at the right time provides several benefits.

First, the form becomes easier to understand. The patient can focus on one relevant topic instead of scanning a large page and trying to decide which fields can be ignored.

Second, it reduces the risk of errors. Irrelevant questions can lead to random values, contradictory answers, or unnecessary free-text explanations.

Finally, the form becomes easier to maintain. Most questions are hidden by default, but the form author can still see the full structure in the outline. This makes it easy to find any section, review its logic, and edit the form when needed.

## Completing the Form Can Be a Shared Process

Not every detail must be entered by the patient before the appointment.

Some information may be difficult for the patient to describe precisely. A practitioner may also discover additional details during the consultation. For example, the patient may remember the color and purpose of a tablet but not its name or dosage.

When Formbox generates a link for an existing `QuestionnaireResponse`, the `allow-amend` option can permit the response to be edited and submitted again.

With the appropriate access controls and workflow, a practitioner can add, clarify, or correct information during or after the appointment. The response can then be submitted with an `amended` status instead of forcing the patient to complete every detail alone. Formbox also updates linked resources when an amended response is extracted again in its observation-based extraction workflow.

This approach makes the form a shared information source rather than patient homework that must be perfect before anyone is allowed to look at it.

![Practitioner amending a patient-submitted QuestionnaireResponse](shared-process.png)

## What Happens After the Form Is Submitted?

A better user interface solves only part of the problem.

The answers should not remain locked inside a `QuestionnaireResponse`, where other clinical workflows may have difficulty finding or using them. A completed response is structured, but it is still primarily a record of the form and its answers.

Data Extraction allows users to use questionnaire answers to create or update other FHIR resources. Depending on the extraction approach and mappings, the result can be one resource or a `Bundle` containing several related resources.

Formbox supports several extraction approaches, including observation-based, definition-based, and template-based extraction. Definition-based extraction maps answers to paths in target resources, while template-based extraction can use resource templates and FHIRPath expressions to build more complex output.

For a patient intake form, extracted resources may include:

* `Patient` for demographic and contact information
* `AllergyIntolerance` for reported allergies and reactions
* `MedicationStatement` for medication that the patient reports taking
* `MedicationRequest` when the workflow represents an actual medication order or prescription
* `Observation` for values such as smoking status, weight, height, or other clinical measurements

Data extraction is not automatic simply because a questionnaire uses FHIR SDC. The questionnaire must contain the correct extraction configuration, and an extraction operation or supporting workflow must process the response. Once this configuration exists, however, one form can do more than collect answers. It can become a source of structured clinical information that other FHIR systems can search, compare, validate, and reuse.

This can also reduce repeated data entry. Instead of reading a response and manually copying every allergy or medication into another screen, the system can create the relevant resources according to defined mappings. A clinician still needs to review clinically important information, but the computer can handle the less exciting copying work.

![Questionnaire answers extracted into new FHIR resources](new-resources.png)

## Conclusion

By using several FHIR SDC features together, we transformed a large and overwhelming intake form into a clear, structured, and patient-friendly questionnaire that shows only what is necessary.

Conditional logic with `enableWhen` allowed us to remove roughly 70% of irrelevant questions from the initial view. These questions are now hidden and appear only when the patient's previous answers make them relevant.

Collapsible groups also help reduce the visual load. When the form is opened, the patient sees only a small number of questions — around ten — instead of the entire questionnaire at once. This makes it easier to start without feeling as though the first appointment has unexpectedly turned into an exam. The remaining sections can then be opened step by step.

Finally, the amend option gives patients room to make mistakes. Anyone may need help completing a medical form, especially when questions involve clinical terms, dates, medications, or past conditions. During the appointment, a practitioner can review the answers, clarify unclear details, and make the necessary updates. This helps improve the accuracy and completeness of the collected information.

Together, these features make the patient intake form easier for patients to complete and easier for clinicians to review and use. The result is not simply a shorter-looking form, but a more practical and reliable way to collect structured clinical information.

## Additional Resources

* [Learn more about `enableWhen` in Formbox](https://www.health-samurai.io/docs/formbox/aidbox-ui-builder-alpha/form-creation/widgets-deprecated#enablewhen-rule)
* [Learn more about data extraction in Formbox](https://www.health-samurai.io/docs/formbox/aidbox-ui-builder-alpha/form-creation/data-extraction)
* [Create a required consent checkbox in Formbox](https://www.health-samurai.io/docs/formbox/aidbox-ui-builder-alpha/form-creation/how-to-guides/how-to-add-consent-questions)

## Video

[Watch the patient intake form walkthrough on YouTube](https://youtu.be/Jj7zopCW3io)


---

---
{
  "title": "FHIR needs data quality profiles",
  "description": "A green validator tells you nothing about whether a dataset is usable. OMOP solved this a decade ago with the Data Quality Dashboard. FHIR now has every piece needed to build its own — on SQLQuery plus a few extensions.",
  "date": "2026-07-15",
  "author": "Nikolai Ryzhikov",
  "reading-time": "16 min read",
  "tags": [
    "SQL on FHIR",
    "Data Quality",
    "Analytics"
  ],
  "tldr": "You point a quality measure at a few million FHIR resources and get a number back. Should you trust it? Maybe 40% of the lab values are missing, weights arrived in pounds, and half the diabetes sits in resources your query never touches. None of it breaks a profile rule; all of it quietly moves the answer. Garbage in, garbage out — except the garbage is perfectly well-formed FHIR. A validator cannot help: it sees one resource at a time and has no concept of how many. Every other data stack solved this years ago — a check is just a query that returns the bad rows — and OMOP brought it to health data with the Data Quality Dashboard. FHIR needs the same, and already has the pieces: an SQLQuery plus three extensions carrying category, threshold and severity, which any SQL on FHIR engine can run.",
  "utm-campaign": "analytics",
  "utm-content": "fhir-data-quality"
}
---

## Garbage in, garbage out

Someone wants to run a clinical quality measure over your FHIR data — a HEDIS-style measure written in CQL, say, or a diabetes-control dashboard, or a cohort for a study. They pull a few million resources through a Bulk FHIR export, point their logic at it, and get a number back.

Should they trust that number?

Everything validated against US Core. Every reference resolved. The validator was green from top to bottom. And yet: maybe 40% of the lab observations carry no numeric value. Maybe half the patients have no encounters at all. Maybe a batch of body weights arrived in pounds where the profile expects kilograms — a perfectly conformant `Quantity` holding a perfectly wrong number. Maybe every patient on diabetes medication is missing a diabetes diagnosis, because the source system kept those in a table nobody mapped.

None of that breaks a single profile rule. All of it flows straight into the measure and quietly moves the answer. Garbage in, garbage out — except here the garbage is invisible, because every byte of it is well-formed FHIR. And the honest response to "how much of this data is wrong?" today is a shrug.

And that still assumes the data is even where you went looking for it. FHIR gives you more than one valid way to record the same clinical fact. A patient's diabetes might live in a `Condition`, or as an `Observation` with a diagnostic code, or be implied only by a `MedicationRequest` for metformin or a `Procedure`. A measure that queries `Condition` and nothing else isn't *wrong* — it just silently misses every patient whose diabetes was modeled the other way. The data is valid and conformant, just not where the logic looked, and no validator will tell you so.

This is not a hygiene problem to clean up later. It's the thing standing between FHIR data and every use case that motivated collecting it — analytics, quality measurement, research, a model.

## FHIR profiles the instance, not the dataset

The instinct is to reach for profiles, and profiles genuinely are part of the answer — just not the part people assume. A profile is where the community agrees on *representation*: that diabetes belongs in `Condition`, coded from this ValueSet, with these elements present. That's exactly the ambiguity from the opening, pinned down — without a profile, every dataset is a different shape and no shared check is even writable. Profiles are the foundation this whole approach stands on.

But a profile is an *agreement*, not an audit — and FHIR profiles are, by design, permissive. Most elements stay optional; must-support asks a system to be *capable* of a field without ever requiring a value; bindings are frequently extensible; escape valves like data-absent-reason are built in. That looseness is deliberate, so real-world data can flow. It's also why a profile describes the *shape* of good data without saying how much of your data fills it in — a contract, not a measurement.

And even the rules a profile *does* enforce, its engine enforces **one resource at a time**. The validator has no notion of a second resource, let alone ten million. So the questions that matter most here are exactly the ones it cannot ask:

- What fraction of `Observation.value` is null? *(a rate, not a rule)*
- Does any identifier repeat across two patients? *(uniqueness spans records)*
- Is our diabetes prevalence 0.1% where it should be near 10%? *(a distribution)*
- Do patients on metformin have a matching condition? *(a join)*

A validator, by construction, cannot count. No profile will ever express "nulls under 5% is acceptable", because a profile has no concept of *how many*.

![Left: an instance profile validates one Observation — status, code, value, subject all structurally valid. Right: a dataset profile, the checks that run across millions of records — null rate, unique keys, distribution, cross-resource joins.](dq-two-engines.svg "FHIR profiles the instance — one resource against a StructureDefinition. What it has no name for is the dataset profile: the rates, joins, and thresholds that decide whether the data as a whole is usable.")

That asymmetry is the whole argument in one picture. FHIR gives you a rich **instance profile** — a StructureDefinition that says what one well-formed resource looks like — and nothing for the **dataset profile**: no standard way to state, or check, what a good *collection* of those resources looks like. Everything below is about building that missing half.

You can watch this play out in the community. There's a [109-message thread on chat.fhir.org](https://chat.fhir.org/#narrow/stream/implementers/topic/Exchanging%20non-conformant%20data) — nineteen participants, including some of the most senior people in the ecosystem — working through what a system should do with a `Period` whose end precedes its start. Real data, out of a real EHR conversion. The debate covers whether to send it, drop it, move it into an extension, or tag it unreliable. It's a careful, thoughtful discussion.

And every word of it is about **one bad period**. Not once does anyone ask what share of periods in the dataset are inverted, because there is no way to ask.

Worse, the conclusion the community keeps arriving at makes the gap wider. If the accepted practice for garbage is to move it out of the computable element into an extension, or flag the resource with an [integrity security tag](https://terminology.hl7.org/ValueSet-v3-SecurityIntegrityObservationValue.html), then a fully conformant dataset can be stuffed with unusable data **by design**. Conformance doesn't reveal the problem. It absorbs it.

### Mandatory, present, and empty

The sharpest version of this is the [data-absent-reason extension](https://hl7.org/fhir/extensions/StructureDefinition-data-absent-reason.html). Mark an element `1..1` and you'd think you've guaranteed a value. You haven't. A minimum cardinality is satisfied by the element merely being *present* — and an element carrying nothing but an empty `data-absent-reason` is present. The validator counts it, the resource passes, and no value was ever supplied.

This isn't a loophole someone forgot to close; it's inherited from US Core, deliberately, as an escape valve for legacy, external, and redacted data. Implementers have [run into it in the wild](https://chat.fhir.org/#narrow/stream/Da.20Vinci.20CRD/topic/data-absent-reason%20on%20mandatory%20elements%20in%20CRD%20profiles) — a required field satisfied by an absent-reason and nothing else — and the community's own read is that it quietly defeats the point of marking the field required. The proposed remedy is another instance-level invariant, written and enforced per IG, forbidding the extension where a real value is expected.

Notice what that costs. To know whether your `1..1` fields actually hold data, you can't trust the green checkmark — you have to ask *what fraction of them are standing in an absent-reason instead of a value.* That is a rate across the dataset. A profile cannot compute it. A query can.

The gap even shows up where you'd most expect a fix. Da Vinci DEQM — the IG for exchanging quality measure data — has a section titled [Data Quality](https://hl7.org/fhir/us/davinci-deqm/guidance.html#data-quality). Its content, in full, is that measures should use defined profiles like US Core or QI-Core so exchanged data is standardized and suitable for evaluation. No thresholds. No rates. No aggregates. Not a single mechanism for assessing quality — just profiles again, the same tool that can't answer the question.

So this isn't an argument against profiles — it's an argument for a second kind. The instance profile stays the source of truth for what a *valid resource* is; a dataset profile measures how much of your data actually lives up to it. **One owns the instance, the other owns the dataset.**

## Every other data stack already tests its data

Step outside healthcare and this problem isn't just solved — it's table stakes. Testing your data is a standard stage in any serious analytics pipeline, and the mechanism is always the same, and always this simple: **a check is a query that returns the rows that break a rule. Zero rows, the data passes. Any rows, those rows are the problem.**

The same idea ships under a different name in every major tool:

| Tool | What a check is |
|---|---|
| [**dbt**](https://docs.getdbt.com/docs/build/data-tests) | a `SELECT` that returns failing rows — with generic templates like `not_null`, `unique`, `accepted_values`, `relationships` |
| [**SQLMesh**](https://sqlmesh.readthedocs.io/en/stable/concepts/audits/) | an *audit*: a query that must return zero rows, or the pipeline halts |
| [**Amazon Deequ**](https://github.com/awslabs/deequ) | "unit tests for data" on Spark — completeness, uniqueness, distribution, across billions of rows |
| [**Great Expectations**](https://greatexpectations.io/) · [**Soda**](https://www.soda.io/) | validation-as-code: human-readable *expectations* run in CI and in production |

Look at what they all check: values present, keys unique, numbers in an accepted range, references that resolve, distributions that look right. The same short list everywhere — because data goes wrong in the same handful of ways regardless of domain. This is a mature, load-bearing part of data engineering, not a fringe practice.

## OMOP brought it to health data a decade ago

Healthcare analytics already made this jump. OHDSI's [Data Quality Dashboard](https://ohdsi.github.io/DataQualityDashboard/) takes that same query-per-check pattern and aims it at clinical data: point it at an OMOP CDM database, it runs thousands of checks, and hands back a graded report. Nobody in that world would publish a dataset without one.

What OMOP adds is a taxonomy for the ways health data specifically goes wrong — the [Kahn framework](https://pmc.ncbi.nlm.nih.gov/articles/PMC5051581/), which sorts every check into three questions:

| Category | The question | Example |
|---|---|---|
| **Conformance** | Is the data in the right shape? | `status` holds a value outside the allowed set |
| **Completeness** | Is the data there at all? | 40% of observations have no value |
| **Plausibility** | Can the data be believed? | A body weight of 1000 kg |

Two mechanics make it work. First, a check type is a **template**, not a query — one `not_null` template expands across every required field of every table, which is how roughly two dozen templates become thousands of concrete checks. Second, every check carries a **threshold**: bad rows under 5% passes, over 5% fails. That's what makes these checks *fuzzy* in a way an invariant can never be. An invariant is binary. A data quality check is statistical, and reality is statistical.

This taxonomy isn't an OMOP quirk, either. The [NCQA Bulk FHIR Quality Coalition](https://www.ncqa.org/bulk-fhir-api-quality-coalition/) grades Bulk FHIR data with exactly these three categories. Germany's [Medical Informatics Initiative](https://doi.org/10.3233/SHTI230117) assesses FHIR data quality with Kahn. PhUSE has [evaluated FHIR API data for FDA submissions](https://www.lexjansen.com/phuse-us/2024/ic/PAP_IC12.pdf) on the same framework. The vocabulary is settled — FHIR just never picked it up.

## FHIR now has the pieces: SQLQuery + extensions

![FHIR resources flatten into a table via a ViewDefinition, then a SQL query with extensions turns that table into a data quality dashboard.](dq-pipeline.svg "The whole pipeline is standard artifacts: a ViewDefinition flattens FHIR, a SQL query plus extensions turns the result into a dashboard.")

Read the diagram left to right and you have the whole idea. A `ViewDefinition` flattens FHIR into a table. A SQL query over that table returns the rows that break a rule — the same dbt-style check every other stack runs. A few **extensions** on that query — Kahn category, threshold, severity — turn a plain query into a graded check you can put on a dashboard.

That's the entire proposal: **a data quality check is an `SQLQuery` plus three extensions.** No new resource, no new operation, no new engine — a check is structurally identical to any other query, and the extensions are the only thing that make it a check.

None of the parts is new — every piece a data quality dashboard needs already exists in the spec:

| A DQD needs… | FHIR already has |
|---|---|
| A flat table to check | **ViewDefinition** — flattens FHIR into columns |
| A check | **SQLQuery `Library`** — a query over that view |
| A way to run it | **`$sqlquery-run`** — the existing operation |
| Composition, roll-ups | **`relatedArtifact: depends-on`** — query-to-query dependencies |
| Schema rules | **profiles** — already the source of truth |

That's what changed. Building a DQD used to be an infrastructure project — OHDSI needed its own SQL engine, its own flat data model, years of work. [SQL on FHIR](/blog/aidbox-becomes-the-first-fhir-server-to-pass-all-sql-on-fhir-tests) standardizes that layer, so in FHIR it's no longer an infrastructure problem. It's just content: write the queries.

And because a check is nothing but SQL over a standard flat view, it's a *specification*, not an implementation. The same `SQLQuery` runs on Postgres, DuckDB, or Spark — or compiles down to the engines every analytics team already runs: a dbt test, a Deequ constraint, a Great Expectations suite. That's the whole reason to standardize it. Not to build another data quality engine — FHIR doesn't need one — but to give the ecosystem **one portable, vendor-neutral way to state what a good FHIR dataset looks like**, authored once and run anywhere.

This isn't a thought experiment. At the recent HL7 Vulcan connectathon we ran it: a FHIR-to-OMOP transformation built only from these primitives, plus **258 DQD checks — each one a `Library(type=sqlquery)` carrying the three extensions above**, not the mockup from the previous section. The transformation passed all 172 golden cases and the 23-case answer key with **zero conformance errors**. The checks flagged 5 failures on our own output and 20 on the working group's gold tables — every one a completeness or plausibility signal that the WG had deliberately seeded, matched to the row (our `plausibleGender` check caught exactly their 6 benign-prostatic-hyperplasia and 4 prostate-cancer conditions on female patients).

Two things stood out. Porting a decade of accumulated data quality checks cost **essentially nothing** — a DQD check is a SQL query returning bad rows, and SQL on FHIR runs exactly that. And the checks earned their keep immediately: `plausibleStartBeforeEnd` caught a visit ending three days before it began, sitting in the working group's *own gold tables* — not in the 130 source encounters, not in anyone's predictions, an artifact no human had spotted. The [community debates one inverted `Period` by hand](https://chat.fhir.org/#narrow/stream/implementers/topic/Exchanging%20non-conformant%20data); the check finds them across the whole dataset, automatically.

## What it looks like

Everything below shares one source table — a ViewDefinition flattening `Observation`:

```json
{ "resourceType": "ViewDefinition", "name": "obs_flat", "resource": "Observation",
  "select": [{ "column": [
    { "name": "id",         "path": "getResourceKey()" },
    { "name": "status",     "path": "status" },
    { "name": "loinc",      "path": "code.coding.where(system='http://loinc.org').code.first()" },
    { "name": "patient_id", "path": "subject.getReferenceKey(Patient)" },
    { "name": "value",      "path": "value.ofType(Quantity).value" },
    { "name": "unit",       "path": "value.ofType(Quantity).code" },
    { "name": "effective",  "path": "effective.ofType(dateTime)" }]}]}
```

A check is an SQLQuery over that view. The extensions carry the semantics — this one says *completeness, warn above 5% missing*:

```json
{ "resourceType": "Library", "id": "dqc-obs-value-complete",
  "type": { "coding": [{ "code": "sql-query" }] },
  "extension": [
    { "url": ".../dq-category",  "valueCode": "completeness" },
    { "url": ".../dq-threshold", "valueDecimal": 0.05 },
    { "url": ".../dq-severity",  "valueCode": "warning" }],
  "relatedArtifact": [
    { "type": "depends-on", "resource": "ViewDefinition/obs_flat", "label": "obs" }],
  "content": [{ "contentType": "application/sql", "data": "<base64>" }]}
```

The SQL inside is deliberately boring, and that's the feature:

```sql
-- completeness: rows where the measurement is missing
SELECT id FROM obs WHERE value IS NULL
```

**Referential integrity** just adds a second view and a second dependency, `patient_flat` labelled `pat`:

```sql
SELECT o.id, o.patient_id
FROM obs o LEFT JOIN pat ON o.patient_id = pat.id
WHERE o.patient_id IS NOT NULL AND pat.id IS NULL
```

**Plausibility** is where this earns its keep — no profile can express any of it. OMOP's DQD has a whole family of plausibility checks, and they port to LOINC-coded `Observation`s directly. Three of the most useful.

*Value outside the physiologic range for its code* — DQD's `plausibleValueLow` / `plausibleValueHigh`. The bounds live in a small reference table, one row per LOINC code, which is exactly the template pattern from earlier: one check, thousands of concrete bounds.

```sql
-- 29463-7 body weight (kg)  0–650   |  8480-6 systolic BP (mm[Hg])  0–300
-- 8867-4  heart rate (/min) 0–300   |  4548-4 HbA1c (%)             0–20
SELECT o.id, o.loinc, o.value, o.unit
FROM obs o JOIN obs_range r ON o.loinc = r.loinc
WHERE o.value < r.low OR o.value > r.high
```

*Wrong unit for the measurement* — DQD's `plausibleUnitConceptIds`. A body weight recorded in anything but a mass unit is suspect no matter how sane the number looks:

```sql
SELECT id, value, unit FROM obs
WHERE loinc = '29463-7' AND unit NOT IN ('kg', 'g', '[lb_av]')
```

*A test that contradicts the patient's sex* — DQD's `plausibleGender`. A prostate-specific antigen result on a female patient (`patient_flat` carries `gender`):

```sql
SELECT o.id, o.patient_id
FROM obs o JOIN pat ON o.patient_id = pat.id
WHERE o.loinc = '2857-1' AND pat.gender = 'female'
```

Cross-resource rules land here too. "A patient on diabetes medication should have a diabetes diagnosis" is a join — routine in SQL, awkward to impossible in FHIRPath.

**Profiling metrics** aren't pass/fail at all, just the numbers a dashboard needs:

```sql
SELECT count(*)                              AS "rowCount",
       count(*) FILTER (WHERE value IS NULL) AS "nullCount_value",
       count(DISTINCT patient_id)            AS "distinctCount_patient",
       min(value) AS "min_value", max(value) AS "max_value"
FROM obs
```

And roll-ups compose through the same dependency mechanism, pointing at other checks instead of views:

```sql
SELECT category, count(*) AS checks, sum(failed) AS failed
FROM ( SELECT 'conformance'  category, (SELECT count(*) FROM c1) > 0 failed
       UNION ALL SELECT 'conformance',  (SELECT count(*) FROM c2) > 0
       UNION ALL SELECT 'completeness', (SELECT count(*) FROM c3) > 0 ) t
GROUP BY category
```

The payoff lands where FHIR already does its work: the Implementation Guide. An IG author today ships an **instance profile** — the agreement on what goes where and how it's coded. With this, the same IG carries its other half, a **dataset profile**, in the same bundle:

- the **ViewDefinitions** that flatten data conforming to those profiles into tables, and
- the **quality checks** — SQLQuery checks over those tables, each tagged with its Kahn category and threshold.

Now an IG says more than *"here is the shape your data should take."* It says *"here is the shape, here is how to query it, and here is how to tell whether your data lives up to it."* A consumer points the package at a Bulk export and gets a data quality dashboard back — *this dataset passes 94 of 100 checks from this IG* — without writing a line of bespoke validation code. Profiles, views, and checks travel together, authored by the people who understand the domain.

## Where this goes

Put the pieces together and the picture is simple. Today an Implementation Guide ships an **instance profile**, and increasingly **ViewDefinitions**. With this, it ships the missing half — a **dataset profile**: a curated set of data-quality checks that spell out what a good dataset *for this IG* actually looks like. Publish both together, and any conformant SQL on FHIR engine runs the checks out of the box. The author writes them once; every server grades data against them the same way — no bespoke tooling, no per-vendor setup.

One honest limit: these checks can't be auto-derived from a profile's invariants, because ViewDefinition's FHIRPath subset is smaller than what those invariants use. The base catalogue gets written by hand — a one-time job the community shares.

And that's the invitation. This isn't hypothetical — it's live work in the [SQL on FHIR working group](https://github.com/HL7/sql-on-fhir), with the extension definitions and a starter set of checks in [issue #375](https://github.com/HL7/sql-on-fhir/issues/375), worked forward on the group's calls. The taxonomy is settled and the machinery exists; what's left is building the catalogue, in the open. If you've built data quality tooling over FHIR — or ever wished FHIR had it — come help design it: bring your checks to the thread and join a call.


---

---
{
  "title": "$batch-validate: Validate Every Stored Resource Against Its Profiles, at Scale",
  "description": "Aidbox 2607 replaces the old batch-validation API with the $batch-validate operation — validate a whole resource type already in your database, sync or async, and drill into exactly which resources are non-compliant and why.",
  "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 checks every resource of a type already stored in Aidbox against its FHIR schema and any profiles you name, synchronously or asynchronously. Results are aggregated into a compact, issue-indexed form, so validating 100 GB of non-conformant data doesn't add 100 GB to your database. Available from Aidbox 2607.",
  "seo-tags": ["FHIR validation", "batch validation", "FHIR profile validation", "Aidbox", "FHIR conformance", "healthcare data quality"]
}
---

A FHIR server accumulates complex, deeply structured medical records from every system that feeds it, and it's easy to assume they're all correct.
Trusting the data is one thing; being able to check it — robustly, at scale — is another.
With millions of resources, doing manual validation of each is tedious, and probably unreasonable.
And you rarely validate just once, because profiles are not static.
Implementation guides like US Core and the HL7 Da Vinci guides publish new versions regularly, each one adding elements, tightening cardinalities, or changing the value sets they bind to.
Every time you adopt a new profile version — or publish one of your own — the same concern returns: how much of what you already hold still conforms.
So how do you check all of it, repeatedly, without it becoming a project?

FHIR's `$validate` operation, in theory, could be automated, but you'll have to fetch each resource and POST it back, collecting and filtering results.
This approach has a lot of problems, and is hard to scale.
Ideally, you'd want to ask the server to validate a specific resource type, and tell you what's wrong in one call, with proper ability to scale both horizontally and vertically.

That's precisely what `$batch-validate` does.
It ships in Aidbox 2607, and it replaces the previous batch-validation API entirely.

## Why we rebuilt batch validation

Aidbox has had asynchronous batch validation for years, exposed through a set of RPCs (`aidbox.validation/batch-validation` and friends).
It worked, but it had several problems.
For one, **every validation error was stored as its own `BatchValidationError` resource.**
It was also quite slow, making validation of lots of resources an unnecessarily long task.

Finally, validating a large dataset could produce so many results that it could rival the data itself in size.
A hundred gigabytes of non-conformant resources could produce something close to a hundred gigabytes of error resources.
The mechanism you reached for to *understand* a data-quality problem made your storage problem worse.

It was also async-only, RPC-shaped rather than a FHIR operation, and it handed you a pile of error resources to query rather than an answer.

`$batch-validate` keeps the good part — validate what's already stored, in parallel — and fixes the rest.

|                | Old batch validation                          | `$batch-validate`                                                   |
|----------------|-----------------------------------------------|---------------------------------------------------------------------|
| Interface      | Proprietary RPCs                              | FHIR operation (`Parameters` in and out)                            |
| Modes          | Asynchronous only                             | Synchronous **or** asynchronous                                     |
| Result storage | One `BatchValidationError` resource per error | One row per **distinct** issue plus a tiny table of resource IDs    |
| Storage cost   | Grows with the error count                    | Bounded — resource bodies are never copied                          |
| Output         | A pile of error resources to query            | Worst-first issue summary with on-demand drill-down                 |
| Scaling        | Fixed                                         | Hash-partitioned chunks, streamed, parallel across nodes, indexable |

The old `aidbox.validation/*` RPCs and the `BatchValidationRun` / `BatchValidationError` resources no longer exist. This is a breaking change; if you used them, migrate to the operation below.

## How to use

`$batch-validate` runs against a single resource type.
The only required parameter is `_since`, a lower bound on `meta.lastUpdated`.
It forces every run to declare a window instead of scanning the whole dataset by accident — to validate everything, pass the epoch.

So to validate every Observation updated in April 2026, we can call `$batch-validate` like so:

```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"}
```

By default the call is **synchronous**: it blocks and returns a `Parameters` summary with the headline counts and one entry per distinct issue, worst first.

```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` is the number of distinct resources that hit that exact issue — the fastest way to see whether a problem touches six resources or six hundred thousand.

Synchronous calls are a good fit for a small set of resources, where you want the answer right away.
The work still runs in parallel: `number-of-chunks` (set per call) splits the resources into that many chunks, and the [`scheduler-executors`](https://www.health-samurai.io/docs/aidbox/reference/all-settings#scheduler-executors) setting controls how many run at once on the node.
Together they trade chunk granularity against how hard one machine works — that's your vertical scaling knob.

However, when you want to validate a substantially larger dataset, you may want to do it in an asynchronous way.

## Async for big datasets

To make any `$batch-validate` call asynchronous, all you need is to add a `Prefer: respond-async` header to the call.
Aidbox then schedules the work on its task engine, spreading it across nodes, allowing for both horizontal and vertical scaling.

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

You get a `202` with a `Content-Location` header, which you can poll to see the progress:

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

While it runs you get `202` with an `X-Progress: 45%` header.
When it finishes, you get the same `Parameters` summary a synchronous call returns.
Both synchronous and asynchronous calls persist their results under a `task-id`, so there's no difference in how you analyze results.

## Explore invalid resources

The summary tells you which issues exist and how many resources each one hits.
To see the actual resources, follow the `invalid-resources` link — filter it to a single issue with `_issue`, and page with `_count` / `_page`:

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

Each invalid resource comes back with a **version-specific** `fullUrl` pointing at the exact version that was validated, the resource body, and an `OperationOutcome` listing every issue that resource has:

```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: "..."}
```

The outcome lists a resource's **full** issue set, even when `_issue` narrows which resources come back — so you never fix one problem only to discover a second one on the next pass.

## Test a profile before you enforce it

The most common reason to run this is a new profile: you want to know what breaks before you make it a requirement.
Pass one or more `profile` canonicals, and every resource is validated against them on top of its base schema.

```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"}
```

Multiple profiles are conjunctive (AND): a resource is compliant only if it conforms to every one, and the issues are the union across them.
So you can turn US Core on knowing exactly what would fail, instead of finding out in production.

## Tune the validator per run

Sometimes you want a fast structural sweep and don't care about terminology yet; sometimes you want to be stricter than the box defaults.
Each `disable-*` / `strict-*` parameter overrides one validator setting for that run only — omit it to keep the box's configured behavior.

| Parameter                        | Effect                                                         |
|----------------------------------|----------------------------------------------------------------|
| `disable-terminology-validation` | Skip coded-binding / terminology checks                        |
| `disable-primitive-validation`   | Skip primitive type and format checks                          |
| `disable-slicing-validation`     | Skip slice validation                                          |
| `disable-constraint-validation`  | Skip **all** FHIRPath invariants (or check all when `false`)   |
| `disable-constraint`             | Skip specific invariants by key (e.g. `us-core-8`)             |
| `strict-profile-resolution`      | Treat an unresolved profile as an error instead of skipping it |
| `strict-extension-resolution`    | Treat an unresolved extension as an error                      |

`strict-profile-resolution` is worth calling out.
Without it, a profile URL that doesn't resolve is skipped, so a typo turns into a "compliant" report that is quietly wrong.
Turn it on when you want the run to fail loudly instead.

## Built to scale

Under the hood, `$batch-validate` hash-partitions the resources into a fixed number of chunks (`number-of-chunks`, default 12).
Each chunk validates its `mod(hash(id), N)` slice, and the chunks aggregate into one result:

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

Each chunk is streamed, so heap stays bounded no matter how large the dataset is.
A synchronous run executes its chunks on a dedicated worker pool — a fixed thread pool Aidbox spins up for that run and tears down when it finishes, sized by the [`scheduler-executors`](https://www.health-samurai.io/docs/aidbox/reference/all-settings#scheduler-executors) setting and separate from the threads that serve your regular API traffic.
At most that many chunks run at once, so there's no upper limit on `number-of-chunks` — a larger count simply queues instead of growing the heap.
An async run doesn't use this local pool; instead it runs on the task engine's own pool, writing one scheduler row per chunk for any node to claim.
Since it neither borrows request threads nor connection-pool slots, it's safe against a live box; CPU is what you spend, so favor async for a big first sweep.

The async path is also what lets a run scale across machines.
Each chunk is a job on a scheduler that lives in the shared Postgres, so you can run several Aidbox instances on different machines against one database and they split the chunks between them — a run gets faster as you add nodes.
A chunk is claimed by exactly one instance, so it never runs twice; and if an instance dies mid-run, the scheduler reclaims its chunk and retries it on another, with idempotent writes so a reclaimed chunk never double-counts.

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

Because `N` is fixed for a run, the partition predicate is a constant expression you can index.
For a very large dataset validated with a high chunk count, a matching expression index turns each chunk from a full scan into a selective index scan:

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

Then run with `number-of-chunks: 10000`.
The index modulus must match the chunk count, or PostgreSQL won't use it — confirm with `EXPLAIN`.

## Compact by design

Here's why validating 100 GB of bad data no longer costs you another 100 GB.
Aidbox stores results in an aggregated, issue-indexed form in a dedicated `aidbox_batch_validation` schema:

| Table              | Holds                                                              |
|--------------------|--------------------------------------------------------------------|
| `issue`            | one row per **distinct** error                                     |
| `invalid_resource` | a tiny `(issue, resource_id, version)` row per resource — ids only |
| `chunk_stat`       | one row per chunk with its metrics                                 |

All occurrences that share a profile, resource type, normalized path, code, and constraint collapse into a single issue whose count is the number of distinct resources that hit it.
Aidbox never copies the invalid resource bodies or their `OperationOutcome`s: the drill-down re-reads each body from history at the validated version and reconstructs the outcome from the stored fields.
Distinct problems, not raw volume, drive the storage cost.

## Try it

`$batch-validate` is available in Aidbox 2607.
Point it at a resource type, pass an epoch `_since`, and you get a worst-first map of your data-quality problems in one call — then drill into the exact resources behind each one.

Want to see it work without writing anything?
We published an interactive notebook that runs the whole flow end to end: it loads a set of sample Patients, creates a profile modeled on US Core Patient, validates them in one call, and charts the results — the valid/invalid split, the invalid Patients by issue, and where the problems concentrate.
Read the [**Batch validation** notebook](/docs/aidbox/notebooks/64c853e6-f13a-4246-bb0b-044020b3b01a) here in the docs, then open it in your own Aidbox and run it top to bottom.

Full reference: [Batch resource validation](https://www.health-samurai.io/docs/aidbox/modules/profiling-and-validation/batch-resource-validation).


---

---
{
  "title": "Aidbox, Formbox & Payerbox 2606: Analytics, Bulk Export, and API Control",
  "description": "Release 2606 adds SQLView to the Analytics UI, chart visualization, _elements for bulk export, per-resource API configuration, newer Da Vinci prior auth versions, and Formbox reliability fixes.",
  "date": "2026-07-06",
  "author": "Health Samurai Team",
  "reading-time": "6 min read",
  "tags": ["Aidbox", "Analytics", "SQL on FHIR"],
  "utm-campaign": "release",
  "utm-content": "2606-release"
}
---

Release 2606 focuses on the workflows closest to FHIR data operations: analytics, bulk export, API behavior, prior authorization, and form reliability. The common thread is control over how FHIR data is queried, exported, exposed through APIs, and handled in payer and form workflows.

In [Aidbox 2606](https://www.health-samurai.io/docs/aidbox/overview/release-notes), SQLView moves into the Analytics UI, query results can be rendered as charts, [`$export`](https://www.health-samurai.io/docs/aidbox/api/bulk-api/export) output can be narrowed with `_elements`, and REST API/history behavior can be configured per resource type. The same release also publishes a [performance benchmark](https://www.health-samurai.io/articles/performance-at-scale-baseline) and documents supply chain artifacts through [cosign signatures and SBOMs](https://www.health-samurai.io/docs/aidbox/overview/supply-chain-security).

[Payerbox 2606](https://www.health-samurai.io/docs/payerbox/releases) moves forward on payer interoperability and prior authorization, with newer Da Vinci CRD, DTR, and PAS support, Payer-to-Payer export updates, and a [payerbox umbrella Helm chart](https://www.health-samurai.io/docs/payerbox/run-payerbox/deploy) for full-stack Kubernetes deployment.

[Formbox 2606](https://www.health-samurai.io/docs/formbox/release-notes), formerly Aidbox Forms, focuses on everyday reliability: auto-save, more reliable active-field submission, locale-aware signing timestamps, and better SDCConfig support for shared and embedded forms.

## Analytics Workflows Get More Connected in Aidbox

The Analytics UI now includes SQLView, a new SQL on FHIR Library profile. It appears in the unified `/analytics` list together with ViewDefinition and SQLQuery, so related analytics artifacts are easier to find and manage in one place.

Teams can create and edit SQLViews, resolve dependencies, inspect lineage, and use the new `sql-view` notebook cell. The useful change is not only another artifact type, but a more connected workflow for shaping FHIR data with SQL on FHIR.

Chart visualization also comes to the SQL Console, Notebooks, and SQL Query views. Instead of exporting results just to check a pattern, users can run a query and review the shape of the output inside Aidbox.

## Bulk Export Gets More Precise

Export consumers often need a smaller, predictable subset rather than the full FHIR resource. This is common in analytics, payer exchange, and other data-sharing workflows where unnecessary fields increase volume without adding value.

The [`_elements` parameter for `$export`](https://www.health-samurai.io/docs/aidbox/api/bulk-api/export) makes that possible in 2606. Export output can be limited to selected elements, such as `name` or `Patient.name`, while the required `resourceType`, `id`, and `meta` fields remain in the resource.

Filtered resources are tagged as `SUBSETTED`, making the partial nature of the output explicit for downstream systems. The parameter is available through GET query parameters and through the POST `Parameters` body.

Nested paths, for example `Patient.name.family`, are available through the opt-in [`fhir.bulk-data.export.nested-elements`](https://www.health-samurai.io/docs/aidbox/reference/all-settings) setting. 2606 also supports [system-level bulk export](https://www.health-samurai.io/docs/aidbox/api/bulk-api/export), extending the export options available for larger data workflows.

## API Behavior and Runtime Predictability

[Resource-level Storage and API configuration](https://www.health-samurai.io/docs/aidbox/configuration/storage-and-api-configuration) gives teams control over REST API and history behavior per resource type. Instead of treating the whole API surface as one global switch, an instance can expose or retain history differently for different resource types.

Caching also gets a production-focused update. The version-based cache layer improves performance and memory usage, and it fixes cases where cached data could stay stale after underlying resources changed.

Search behavior is clearer in failure cases. Unsupported search parameters return a clear error instead of an internal error, and the `SearchParameter` metadata cache is invalidated on update and delete so custom search parameter changes take effect immediately.

Access changes have a shorter waiting period as well. When a new role is created for a user, the JWT cache is cleared for that specific user, allowing newly granted access to apply without waiting for normal cache expiry.

For teams evaluating deployment characteristics, the public [performance benchmark](https://www.health-samurai.io/articles/performance-at-scale-baseline) gives a shared baseline for scale discussions. The [supply chain security overview](https://www.health-samurai.io/docs/aidbox/overview/supply-chain-security) documents signed Docker images and SBOMs, which helps platform and security teams review the artifacts they deploy.

## Payerbox Updates for Prior Auth and Payer-to-Payer Exchange

For Payer-to-Payer workflows, [`$bulk-member-match`](https://www.health-samurai.io/docs/payerbox/api-reference/operations/bulk-member-match) authenticates the calling payer through UDAP B2B, with details in [Authentication](https://www.health-samurai.io/docs/payerbox/api-reference/authentication). [`$davinci-data-export`](https://www.health-samurai.io/docs/payerbox/api-reference/operations/davinci-data-export) also gains the `payertopayer` export type for Payer-to-Payer exchange.

Provider Directory work also changes in the CMS Medicare Plan Finder pipeline. Scope filters now run inside the `$export` query, export output is gzip-compressed, and a runnable reference implementation is available in the [Aidbox examples](https://github.com/Aidbox/examples/tree/main/aidbox-features/medicare-plan-finder); the [MPF Pipeline](https://www.health-samurai.io/docs/payerbox/run-payerbox/provider-directory-pipeline) docs explain the pipeline setup.

Prior Auth APIs move to newer Da Vinci versions. [CRD](https://www.health-samurai.io/docs/payerbox/prior-auth/crd) is upgraded to Da Vinci CRD 2.1.0 and relays decision-service errors with the original HTTP status and `OperationOutcome`, while [DTR](https://www.health-samurai.io/docs/payerbox/prior-auth/dtr) moves to Da Vinci DTR 2.1.0.

[PAS](https://www.health-samurai.io/docs/payerbox/prior-auth/pas) is upgraded to Da Vinci PAS STU 2.1.0, which is now the default, with STU 2.0.1 still selectable through `PAS_IG_VERSION`. This keeps the prior authorization APIs aligned with the newer implementation guide version while preserving a configuration path for the previous one.

[`Claim/$submit`](https://www.health-samurai.io/docs/payerbox/api-reference/operations/claim-submit) now writes a ClaimResponse reference extension on the submitted Claim, linking the Claim to the resulting ClaimResponse. The operation is also idempotent on `Claim.identifier`, so resubmitting an existing Claim returns the existing ClaimResponse instead of creating a duplicate prior authorization.

Under PAS 2.1.0, an updated prior authorization keeps a single ClaimResponse on the original Claim. [`Claim/$submit`](https://www.health-samurai.io/docs/payerbox/api-reference/operations/claim-submit) and [`Claim/$inquire`](https://www.health-samurai.io/docs/payerbox/api-reference/operations/claim-inquire) return that ClaimResponse for any Claim in the update chain.

The FHIR App Portal picks up two smaller product updates. The Developer Portal can register a backend bulk data service with a client secret for client-credentials flows, in addition to a JWKS URI, and [Backend Services](https://www.health-samurai.io/docs/payerbox/fhir-app-portal/backend-services) covers that setup; the Admin Portal has a redesigned app review card.

Kubernetes deployment gets a simpler entry point through the new [payerbox umbrella Helm chart](https://www.health-samurai.io/docs/payerbox/run-payerbox/deploy). The chart deploys the full stack, including portals, Interop APIs, Prior Auth, and Aidbox.

## Formbox Improves Reliability During Form Filling and Sharing

Formbox 2606 focuses on improving reliability and everyday usability.

Auto-save now preserves form data if a form is reloaded while being completed. Form submission is also more reliable for text and date/time fields, ensuring the latest entered values are submitted even if the active field has not lost focus.

Several quality-of-life improvements are included as well. The signature item now displays the signing date and time using the browser locale, and the Calculated Expression section opens the Advanced Editor by default to reduce the risk of accidental data loss.

Form sharing has also been improved with better SDCConfig support. Shared forms can now include SDCConfig, apply themes defined in it, and the embedded form builder provides more reliable SDCConfig loading through `onFetch`.

## Legacy Cleanup and Other Aidbox Updates

MPI `$match` and related functionality have been removed from Aidbox. Patient matching now lives in [MDMbox](https://www.health-samurai.io/mdmbox), which is the product path for that capability.

The legacy engine cleanup is broader than MPI. Zen validation, the Entity/Attribute framework, the legacy FHIR Terminology Repository, and the Zen-based repository, indexes, and search implementation have all been removed.

[FHIR Schema validation](https://www.health-samurai.io/docs/aidbox/modules/profiling-and-validation/fhir-schema-validator) is now the only validation mode. The `BOX_FHIR_SCHEMA_VALIDATION` setting has been removed and has no effect, and validation can no longer be disabled.

Metadata output is also cleaner. `/fhir/metadata` no longer emits non-absolute canonical URLs in `CapabilityStatement.implementationGuide`, and packages without a canonical URL are omitted.

One additional protocol update is included in Aidbox 2606. The [Streamable HTTP](https://www.health-samurai.io/docs/aidbox/modules/other-modules/mcp) MCP transport supports direct connections from modern MCP clients such as ChatGPT and Claude, but it stays here as a secondary update rather than the main release story.

## Read the Full Release Notes

This post highlights the release themes most relevant for data operations, payer workflows, and form reliability. For the complete changelog, read the release notes for [Aidbox 2606](https://www.health-samurai.io/docs/aidbox/overview/release-notes), [Payerbox 2606](https://www.health-samurai.io/docs/payerbox/releases), and [Formbox 2606](https://www.health-samurai.io/docs/formbox/release-notes).

The linked docs include the details needed for upgrade planning and feature configuration. They are the best place to verify exact settings, operation behavior, and product-specific release notes before implementation.


---

---
{
  "title": "Building FHIR Provider Directory for Medicare Plan Finder",
  "description": "CMS requires Medicare Advantage plans to publish a PDex Plan-Net FHIR provider directory. With bulk $export and _typeFilter, getting the right data out takes one call, and the rest is ordinary code.",
  "date": "2026-07-06",
  "author": "Akim Khalitov, Gleb Markin",
  "tags": [
    "Compliance",
    "Integrations",
    "Payerbox"
  ],
  "reading-time": "7 min read",
  "tldr": "CMS now requires every Medicare Advantage plan to publish a provider directory for Medicare Plan Finder, which crawls it daily as published files, not a live API. CMS allows two formats; this takes the FHIR one: a bulk $export pulls the in-network resources, a streaming pass keeps only the records they reference, and an atomic publish serves the resulting FHIR Bundles, plus an index manifest, through a crawler-friendly endpoint.",
  "utm-campaign": "feature",
  "utm-content": "provider-directory",
  "hide-comments": true
}
---

If you run a Medicare Advantage plan, CMS now requires you to publish a
[provider directory](https://www.health-samurai.io/articles/mpf-provider-directory-medicare-advantage-cms-4208-f2)
listing which providers and facilities are in-network for each plan, so
[Medicare Plan Finder](https://www.medicare.gov/plan-compare/) (MPF) can answer the question
shoppers actually ask: is my provider covered? That question is MA-specific: an MA plan covers a
contracted network, and since MA is federally funded Medicare run by private insurers, CMS gets
to dictate how the directory is published. The directory has to
be machine-readable, refreshed within 30 days of any change,
and attested to once a year. It's part of
[CMS's broader push toward FHIR-based interoperability](https://www.health-samurai.io/articles/understanding-the-cms-0057-f-interoperability-and-prior-authorization-final-rule).

CMS accepts two formats, a flat JSON file or FHIR; we build the FHIR one. It follows
[PDex Plan-Net](https://hl7.org/fhir/us/davinci-pdex-plan-net/), the Da Vinci IG that
standardizes a plan's network across FHIR profiles on
[InsurancePlan](https://hl7.org/fhir/us/davinci-pdex-plan-net/StructureDefinition-plannet-InsurancePlan.html),
[Organization](https://hl7.org/fhir/us/davinci-pdex-plan-net/StructureDefinition-plannet-Organization.html)
(as both network and facility),
[Practitioner](https://hl7.org/fhir/us/davinci-pdex-plan-net/StructureDefinition-plannet-Practitioner.html),
[PractitionerRole](https://hl7.org/fhir/us/davinci-pdex-plan-net/StructureDefinition-plannet-PractitionerRole.html),
[Location](https://hl7.org/fhir/us/davinci-pdex-plan-net/StructureDefinition-plannet-Location.html), and
[OrganizationAffiliation](https://hl7.org/fhir/us/davinci-pdex-plan-net/StructureDefinition-plannet-OrganizationAffiliation.html),
and the references between them. CMS doesn't query your server per shopper: its crawler ingests
your published files once a day and validates them, following its
[technical implementation guide](https://www.cms.gov/files/zip/mpf-ma-provider-directory-technical-guide-02182026-final-zip.zip).
Fail validation, miss the annual attestation, or trip CMS's data-quality threshold, and CMS
suppresses your directory from MPF, generally restoring it the day after you resolve the issue.
While it's suppressed, shoppers comparing plans can't see your network, so keeping the directory
valid on every run matters.

The rest of this article walks that pipeline, stage by stage.

## How it works

Your data already lives in Payerbox. The pipeline turns it into the files CMS crawls, on a
schedule. Four stages:

```mermaid
flowchart LR
    A["1 · Extract<br/>$export + _typeFilter<br/>(narrows parents)"] --> B["2 · Filter<br/>resolve refs,<br/>keep children"]
    B --> C["3 · Bundle<br/>FHIR + index.json"]
    C --> D["4 · Publish<br/>cloud storage"]
    D --> E["CMS crawls<br/>daily"]
```

*The daily run, end to end: extract from Payerbox, filter to the in-network subset, bundle as
FHIR, then publish the files CMS crawls.*

A scheduled job runs the whole thing each morning, ahead of CMS's daily crawl.

Everything after extraction is plain code over NDJSON, so it runs and tests against a sample
export, no live FHIR server needed.

## Getting the data out

There are two ways to get the resources out of Payerbox. The pipeline reads through a common
source interface, so either could feed it; we use bulk `$export`.

- **The FHIR REST API.** Page through with ordinary search queries. That's a fine fit for a
  small, targeted slice, but pulling whole resource types at scale means a lot of round-trips,
  and the data can shift between pages.
- **The FHIR bulk `$export` operation.** This operation
  runs [asynchronously](https://www.health-samurai.io/articles/unified-fhir-async-operations-pattern)
  and streams everything out as NDJSON in one pass, without the between-pages drift of paging
  the live API. It's built for this, so that's what we use.

`$export` is the standard [FHIR Bulk Data Access](https://hl7.org/fhir/uv/bulkdata/) operation,
with `_typeFilter` to push selective filters into the export and `+gzip` on `_outputFormat` to
keep the dump small. With `_typeFilter`, the server sends only the resources that matter:

```
GET /fhir/$export
  ?_outputFormat=application/fhir+ndjson+gzip
  &_type=InsurancePlan,Organization,Practitioner,PractitionerRole,Location,OrganizationAffiliation
  &_typeFilter=InsurancePlan?status=active&_profile=.../plannet-InsurancePlan&_id=...
  &_typeFilter=PractitionerRole?active=true&_profile=.../plannet-PractitionerRole&network=...
  &_typeFilter=OrganizationAffiliation?active=true&_profile=.../plannet-OrganizationAffiliation&network=...
```

Kicked off with the `Prefer: respond-async` request header, `$export` returns `202 Accepted`
with a `Content-Location` response header: the status URL you poll until the NDJSON files are
ready. Plans, practitioner roles, and affiliations come back already
narrowed to active, in-network, Plan-Net resources. The children they reference come in full,
so a kept record never points at something dropped.

## Filtering to the in-network subset

That `$export` was the first filter: `_typeFilter` narrows the parents on the FHIR server. A
code-side pass then resolves the children: it walks the graph (plan → network → affiliated
organizations and practitioners → their locations), keeps a child only when a kept parent
references it, and drops the rest. That graph is the references PDex
Plan-Net defines between the six resources:

<div class="narrow" style="display: flex; justify-content: center">

```mermaid
flowchart TD
    IP["InsurancePlan"] -->|network| NET["Organization<br/>(Network, type=ntwk)"]
    PR["PractitionerRole"] -->|network| NET
    PR -->|practitioner| PRAC["Practitioner"]
    PR -->|location| LOC["Location"]
    OA["OrganizationAffiliation"] -->|network| NET
    OA -->|organization| FAC["Organization<br/>(Facility, type=fac)"]
    OA -->|location| LOC
```

</div>

*The references between Plan-Net resources. `_typeFilter` narrows the parents (InsurancePlan,
PractitionerRole, OrganizationAffiliation) on the FHIR server; the code pass then keeps the children
they reference.*

## Bundling and publishing

Kept resources go into FHIR [Bundles](https://hl7.org/fhir/R4/bundle.html)
(`type: collection`), split per resource type into numbered files (`PractitionerRole-001.json`,
`-002.json`, and so on); each resource keeps its source `meta.lastUpdated`, which the guide
maps to each record's last-updated date. CMS expects the split: it requires a per-contract
index file, a
manifest of every file's URL the crawler reads first:

```json
{
  "provider_urls": [
    "https://.../H9999/2026/InsurancePlan-001.json",
    "https://.../H9999/2026/PractitionerRole-001.json",
    "https://.../H9999/2026/PractitionerRole-002.json",
    "https://.../H9999/2026/Organization-001.json"
  ]
}
```

The split keeps each file a manageable size for the crawler; the pipeline rolls to the next
numbered file once a bundle grows too large or passes an entry cap.

Publishing comes with one hard rule: a crawl must never land on a half-written directory. A
file like `Organization-042.json` holds different resources from one run to the next, so
overwriting it in place while the old manifest still points at it is a torn read. The publish
is therefore an atomic swap: wipe the old files, upload the new bundles, then
upload `index.json` last. That final upload is the single step that makes the new version live,
so a crawler mid-publish sees yesterday's directory, today's, or a brief 404 during the swap,
never a half-written blend. A run that fails partway raises an alert. There's no partial-resume:
the next run just rebuilds the whole directory from scratch.

## Serving the crawler

CMS recommends conditional requests, and the endpoint serves them. Each file carries an `ETag`
(a fingerprint of its contents that changes when the file does) and a `Last-Modified`
timestamp; the crawler stores them and, on its next visit, re-fetches a file only when its
`ETag` has changed. Unchanged files come back as `304 Not Modified` with no body:

```http
GET /mpf-provider-directory/H9999/2026/Organization-042.json
If-None-Match: "a1b2c3"        # the ETag the crawler received on its last fetch
→ 304 Not Modified             # unchanged since then, so no body
```

On a directory of thousands of bundle files that barely change day to day, that's the gap
between a heavy re-crawl and a cheap one. It also explains the atomic publish: the `ETag` must
flip exactly when the contents change.

That endpoint proxies a private bucket rather than exposing the bucket itself, and needs no
auth: the Plan-Net directory is meant to be openly accessible, with no client registration.

## Building one yourself?

Building it is relatively modest engineering for anyone who works with FHIR servers regularly: a
daily export-filter-bundle-publish job. But the care is in getting it right on every run, and the
whole thing assumes correct Plan-Net data already lives in the FHIR server. Getting it there is a
project of its own. Its data quality should be monitored continuously, validating the PDex Plan-Net
resource set against CMS's MPF criteria.

This directory ships as an optional module of
[Payerbox](https://www.health-samurai.io/cms-0057-f), Health Samurai's platform for CMS payer
requirements, which also delivers the CMS-0057-F APIs due in 2027.

> Building a provider directory for Medicare Plan Finder?
> [Talk to our team](https://www.health-samurai.io/company#contact-form) to get started.

## References

- [CMS-4208-F2 final rule](https://www.federalregister.gov/documents/2025/09/19/2025-18236/medicare-and-medicaid-programs-contract-year-2026-policy-and-technical-changes-to-the-medicare)
- [The MPF provider directory requirement, explained](https://www.health-samurai.io/articles/mpf-provider-directory-medicare-advantage-cms-4208-f2)
- [CMS MA Provider Directory Technical Implementation Guide](https://www.cms.gov/files/zip/mpf-ma-provider-directory-technical-guide-02182026-final-zip.zip)
- [Da Vinci PDex Plan-Net IG](https://hl7.org/fhir/us/davinci-pdex-plan-net/)
- [FHIR Bulk Data Access](https://hl7.org/fhir/uv/bulkdata/)


---

---
{
  "title": "@atomic-ehr/codegen: US Core Profiles in Python",
  "description": "Generate typed US Core profile classes in Python from the FHIR IG with @atomic-ehr/codegen — typed factories, extensions, slices, and profile-aware validation.",
  "date": "2026-07-03",
  "author": "Mikhail Artemyev, Aleksandr Penskoi",
  "reading-time": "12 minutes",
  "tags": [
    "FHIR Tools",
    "FHIR Standard",
    "Code Generation",
    "Python",
    "Pydantic",
    "Aidbox"
  ]
}
---
Building US Core resources by hand is tedious. You stamp `meta.profile`, look up LOINC codes, hand-roll the `us-core-race` nested extension — every field is a typo waiting to happen, every profile is its own version of the same ceremony.

[`@atomic-ehr/codegen`](https://github.com/atomic-ehr/codegen) makes that boilerplate disappear. Point it at the [US Core IG](https://www.hl7.org/fhir/us/core/) and you get one Pydantic model per base type plus a plain-Python wrapper class per profile, with typed accessors for fixed values, extensions, and slices, and a `validate()` that knows what the profile requires.

This tutorial walks through that end-to-end on two US Core profiles: [US Core Patient](https://www.hl7.org/fhir/us/core/StructureDefinition-us-core-patient.html) and [US Core Blood Pressure](https://www.hl7.org/fhir/us/core/StructureDefinition-us-core-blood-pressure.html).

## What You'll Build

A CSV-to-FHIR converter, built step by step:

1. generate profile classes for [US Core Patient](https://www.hl7.org/fhir/us/core/StructureDefinition-us-core-patient.html) and [US Core Blood Pressure](https://www.hl7.org/fhir/us/core/StructureDefinition-us-core-blood-pressure.html) from `hl7.fhir.us.core@8.0.1`,
2. turn each row into a US Core Patient — typed extension setters and `apply()`,
3. turn each row into a US Core Blood Pressure — typed slices, fixed LOINC, and `validate()`,
4. package them as a Bundle,
5. read the bundle back with typed getters to compute an average BP,
6. post the bundle to a local Aidbox server via [fhirpy client](https://pypi.org/project/fhirpy/).

## Prerequisites

- **Node.js 20+** (or Bun) — the generator *itself* is the `@atomic-ehr/codegen` Node package. You run it once to emit Python; after that you don't need Node again. The generation script is a few lines of TypeScript (shown below).
- **Python 3.12+** — the generated code targets modern Python ([PEP 604](https://peps.python.org/pep-0604/) `X | None` unions, generic models via `typing_extensions`).
- **Pydantic v2** (`pydantic>=2.11`) and **fhirpy** — generated models are Pydantic v2; with the default fhirpy client they also drop into fhirpy's async client (Step 6). Both are pinned in the generated `requirements.txt` (see Step 1); pass `client: "none"` for plain Pydantic with no client code.
- Basic familiarity with FHIR and US Core (knowing what "profile" and "slice" mean is enough).

## Step 1 — Generate Profile Classes

Code generation runs through the Node tool, so set up a small generator project alongside your Python app:

```bash
mkdir py-us-core-tutorial && cd py-us-core-tutorial
npm init -y
npm install --save-dev @atomic-ehr/codegen tsx typescript
```

Create `generate.ts`:

```typescript
import { APIBuilder, mkCodegenLogger, prettyReport } from "@atomic-ehr/codegen";

const main = async () => {
  const logger = mkCodegenLogger({
    suppressTags: ["#fieldTypeNotFound", "#duplicateSchema", "#duplicateCanonical", "#largeValueSet"],
  });

  const builder = new APIBuilder({ logger })
    .fromPackage("hl7.fhir.us.core", "8.0.1")
    .typeSchema({
      treeShake: {
        "hl7.fhir.us.core": {
          "http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient": {},
          "http://hl7.org/fhir/us/core/StructureDefinition/us-core-blood-pressure": {},
        },
        "hl7.fhir.r4.core": {
          "http://hl7.org/fhir/StructureDefinition/Bundle": {},
        },
      },
    })
    .python({
      generateProfile: true,
      allowExtraFields: false,
      primitiveTypeExtension: true,
    })
    .outputTo("./fhir_types")
    .cleanOutput(true);

  const report = await builder.generate();
  console.log(prettyReport(report));
  if (!report.success) process.exit(1);
};

main();
```

The knobs that matter here:

- **`generateProfile: true`** — emit a wrapper class per profile with typed accessors for extensions, slices, and fixed values. Without it you get only the base R4 Pydantic models.
- **`allowExtraFields: false`** — generated models use Pydantic's `extra="forbid"`, so an unknown field raises at parse time instead of being silently dropped.
- **`primitiveTypeExtension: true`** — also generate the FHIR primitive-extension siblings (the `_field` companions, e.g. `birthDateExtension`) so you can attach extensions and `id`s to primitive values.
- **`treeShake: { ... }`** — only the listed canonicals and their transitive deps are generated (~20 files instead of hundreds).

Run it. `prettyReport(report)` prints a grouped summary so you see what got emitted without crawling the output dir:

```bash
$ npx tsx generate.ts
# generation logs omitted; this is the prettyReport summary
Generated files (24 files, 12 kloc):
  python (23 files, 3.6 kloc):
    - fhir_types/ (4 files, 622 loc)
    - fhir_types/hl7_fhir_r4_core/ (8 files, 1.1 kloc)
    - fhir_types/hl7_fhir_r4_core/profiles/ (2 files, 164 loc)
    - fhir_types/hl7_fhir_us_core/profiles/ (9 files, 1.8 kloc)
  ir-report (1 files, 8.2 kloc):
    - fhir_types/README.md (8223 loc)
Duration: 8097ms
Status: 🟩 Success
```

The on-disk layout looks like this:

```
fhir_types/
├── hl7_fhir_r4_core/                  # Base R4 Pydantic models
│   ├── base.py                        # Element, Coding, CodeableConcept, Quantity, ...
│   ├── resource.py                    # Resource, DomainResource, Meta
│   ├── patient.py
│   ├── observation.py
│   ├── bundle.py
│   ├── profiles/                      # base R4 profiles US Core builds on
│   │   └── observation_observation_vitalsigns.py   # vital-signs base (BP derives from it)
│   └── ...
├── hl7_fhir_us_core/
│   └── profiles/
│       ├── __init__.py                # re-exports the profile classes
│       ├── patient_uscore_patient_profile.py
│       ├── observation_uscore_blood_pressure_profile.py
│       ├── extension_uscore_race_extension.py
│       └── ...
├── fhirpy_base_model.py               # fhirpy client base model (default fhirpy client)
├── profile_helpers.py                 # Runtime helpers shared by all profile classes
├── README.md                          # IR report — human-readable dump of the generated types
└── requirements.txt                   # pydantic, fhirpy (+ pytest, requests for tests/Step 6)
```

<details><summary>Set up Python virtual environment</summary>

```bash
python3.14 -m venv venv
source venv/bin/activate
```

</details>

Point your Python app at the emitted `fhir_types/` and install the dependencies:

```bash
pip install -r fhir_types/requirements.txt
```

The generated `requirements.txt` pins Pydantic and fhirpy plus pytest and requests for the tests and examples.

The full tutorial code lives in [`Aidbox/examples`](https://github.com/Aidbox/examples/tree/main/developer-experience/atomic-ehr-codegen-python-us-core-profiles) — `generate.ts`, `load.py`, `avg.py`, `post.py`, the CSV, and the committed `fhir_types/` so you can browse the generated code without running the generator. For broader profile-API exploration, the codegen repo also has a [`python-r4-us-core` test example](https://github.com/atomic-ehr/codegen/tree/main/examples/python-r4-us-core). Both use the default `camelCase` attribute names, just like the snippets here. (Pass `fieldFormat: "snake_case"` if you'd rather spell attributes `birth_date`, `effective_date_time`; serialization always emits FHIR-correct camelCase JSON either way.)

## Step 2 — Row to a US Core Patient

The input is `patients.csv` — basic demographics plus one BP reading per patient. Race uses the [OMB-category codes](https://www.hl7.org/fhir/us/core/ValueSet-omb-race-category.html) US Core expects:

```csv
mrn,family,given,birthDate,gender,raceCode,raceDisplay,effectiveDateTime,systolic,diastolic
MRN-001,Lovelace,Ada,1815-12-10,female,2106-3,White,2026-04-15,120,80
MRN-002,Turing,Alan,1912-06-23,male,2106-3,White,2026-04-15,118,76
MRN-003,Curie,Marie,1867-11-07,female,2106-3,White,2026-04-16,125,82
MRN-004,Carver,George,1864-01-01,male,2054-5,Black or African American,2026-04-16,135,88
MRN-005,Ochoa,Ellen,1958-05-10,female,2054-5,Black or African American,2026-04-17,128,84
```

`csv.DictReader` hands each row over as a plain `dict[str, str]`; numeric parsing happens later, when we pass values to typed profile setters.

The US Core Patient profile adds a few extensions and makes `identifier` and `name` required. The generated class has a typed setter for each:

```python
from fhir_types.hl7_fhir_r4_core.base import Identifier, HumanName, Coding
from fhir_types.hl7_fhir_r4_core.patient import Patient
from fhir_types.hl7_fhir_us_core.profiles import UscorePatientProfile

def row_to_patient(row: dict[str, str]) -> UscorePatientProfile:
    base_patient = Patient(
        resourceType="Patient",
        identifier=[Identifier(system="http://hospital.example.org/mrn", value=row["mrn"])],
        name=[HumanName(family=row["family"], given=[row["given"]])],
        gender=row["gender"],          # gender is a Literal type — Pydantic validates the value
        birthDate=row["birthDate"],    # default camelCase attrs match the FHIR wire names
    )

    patient = UscorePatientProfile.apply(base_patient)

    patient.set_race({
        "ombCategory": {"system": "urn:oid:2.16.840.1.113883.6.238", "code": row["raceCode"], "display": row["raceDisplay"]},
        "text": row["raceDisplay"],
    })

    return patient
```

Two phases:

1. **Build the plain `Patient`** — profile-required (`identifier`, `name`) and must-support (`gender`, `birthDate`) fields as a typed R4 Pydantic model. Construct with the default camelCase attribute names (`birthDate`, the FHIR wire names); values are validated immediately (e.g. `gender` is a `Literal["male", "female", "other", "unknown"]`).
2. **Then `UscorePatientProfile.apply(base_patient)`** stamps `meta.profile` and returns a profile instance with typed accessors for the US Core extensions. `apply()` wraps the resource in place — the profile mutates the same `Patient` object.

Three notes on what the profile API does for you:

- **Three extension setter forms.** `set_race({ "ombCategory": ..., "text": ... })` takes flat input — note the sub-extension keys (`ombCategory`, `detailed`, `text`) are the camelCase slice names — and generates the nested `extension[]` plumbing. The same setter also accepts a typed extension-profile instance (`UscoreRaceExtension`) or a raw `Extension`, and raises if a raw extension's `url` doesn't match.
- **Single-value extensions take the value directly.** `us-core-individual-sex` carries one `valueCoding`, so `set_sex(Coding(code="female"))` takes a `Coding` (or a raw `Extension`).
- **No setters for must-support base fields.** `gender`, `birthDate`, and `address` aren't profiled further by US Core, so the profile class emits no `.set_gender()`-style wrappers — populate them as normal `Patient` fields. `validate()` still warns if a must-support field is missing.

> Pydantic emits a `UserWarning` when an `extension[]` list holds plain dicts rather than `Extension` instances — expected with the current flat-dict plumbing. Silence it with `warnings.filterwarnings("ignore", category=UserWarning, module="pydantic")`.

## Step 3 — Row to a US Core Blood Pressure

The BP profile is where codegen really earns its keep. The US Core Blood Pressure profile:

- fixes `code` to LOINC 85354-9 ("Blood pressure panel"),
- fixes a `vital-signs` category slice,
- defines `component[systolic]` and `component[diastolic]` slices with specific LOINC discriminators (8480-6 and 8462-4),
- requires an `effectiveDateTime` or `effectivePeriod`,
- requires `valueQuantity` inside each slice.

Hand-rolling that per row is the kind of thing codegen eliminates. The generated class collapses it to three setters:

```python
from fhir_types.hl7_fhir_r4_core.base import Reference
from fhir_types.hl7_fhir_us_core.profiles import UscoreBloodPressureProfile

def row_to_bp(row: dict[str, str], patient_urn: str) -> UscoreBloodPressureProfile:
    bp = UscoreBloodPressureProfile.create(
        status="final",
        subject=Reference(reference=patient_urn),
    )

    (
        bp.set_effective_date_time(row["effectiveDateTime"])
          .set_systolic({"value": float(row["systolic"]), "unit": "mmHg", "system": "http://unitsofmeasure.org", "code": "mm[Hg]"})
          .set_diastolic({"value": float(row["diastolic"]), "unit": "mmHg", "system": "http://unitsofmeasure.org", "code": "mm[Hg]"})
    )

    errors = bp.validate()["errors"]
    if errors:
        raise ValueError(f"{row['mrn']}: {'; '.join(errors)}")

    return bp
```

What happens behind the scenes:

- **`create()` does the ceremony.** It stamps `meta.profile`, fills the fixed `code` (LOINC 85354-9), appends the vital-signs category slice, and adds empty `component[systolic]` / `component[diastolic]` stubs with discriminator codes already set. `create()` takes keyword-only args; `create_resource()` is the same but returns a plain `Observation` instead of a profile wrapper.
- **`set_systolic({ "value": ..., "unit": ... })` fills the `valueQuantity`** inside the systolic slice. The discriminator `code` on that component is already there from `create()` — you only supply the reading.
- **`validate()` returns `{"errors": [...], "warnings": [...]}`.** Errors block (required fields, excluded fields, disallowed choice variants, slice cardinality). Warnings surface must-support concerns. A malformed row fails fast with the MRN — you don't discover it at POST time.

You didn't type the discriminator codes. You didn't remember `85354-9`. The setters chain fluently (each returns the profile), just like the TypeScript API.

## Step 4 — Assemble the Bundle

Each row produces a Patient and a BP Observation linked by the Patient's `urn:uuid` placeholder. Package them as transaction entries. The generated `Bundle` and `BundleEntry` are generic over the contained resource, so a `Bundle[Patient | Observation]` keeps `entry[].resource` typed to that union. (`row_to_patient` and `row_to_bp` are the Step 2–3 functions; in the example they all live in one `load.py`, so they're already in scope here.)

```python
import json
import csv
import uuid

from fhir_types.hl7_fhir_r4_core.patient import Patient
from fhir_types.hl7_fhir_r4_core.observation import Observation
from fhir_types.hl7_fhir_r4_core.bundle import Bundle, BundleEntry, BundleEntryRequest

def row_to_entries(row: dict[str, str]) -> list[BundleEntry[Patient | Observation]]:
    patient_urn = f"urn:uuid:{uuid.uuid4()}"
    patient = row_to_patient(row)
    bp = row_to_bp(row, patient_urn)

    return [
        BundleEntry(fullUrl=patient_urn, resource=patient.to_resource(),
                    request=BundleEntryRequest(method="POST", url="Patient")),
        BundleEntry(fullUrl=f"urn:uuid:{uuid.uuid4()}", resource=bp.to_resource(),
                    request=BundleEntryRequest(method="POST", url="Observation")),
    ]

rows = list(csv.DictReader(open("patients.csv")))
print(f"Loaded {len(rows)} rows")

entries = [entry for row in rows for entry in row_to_entries(row)]

bundle = Bundle[Patient | Observation](
    resourceType="Bundle",
    type="transaction",
    entry=entries,
)

with open("bundle.json", "w") as f:
    json.dump(bundle.model_dump(by_alias=True, exclude_none=True), f, indent=2)
print(f"Wrote bundle with {len(entries)} entries")
```

```bash
$ python load.py
Loaded 5 rows
Wrote bundle with 10 entries
```

Worth noticing:

- **`to_resource()` gives you the plain model** — the underlying Pydantic resource, no wrapper, ready to drop into a `BundleEntry`.
- **`model_dump(by_alias=True, exclude_none=True)` produces FHIR JSON** — `by_alias` serializes through the FHIR-wire aliases (so a snake_case build still emits `effectiveDateTime`) and `exclude_none` drops `None`-valued fields. The one serialization call you'll use everywhere.
- **`urn:uuid` references.** The patient's `fullUrl` and the observation's `subject.reference` share one UUID; the server resolves it to a real id on commit.

## Step 5 — Read Back: Average BP from the Bundle

Now read it back. Parse `bundle.json` and compute the average systolic/diastolic to exercise the read-side API:

```python
import json
from typing import Any

from fhir_types.hl7_fhir_r4_core.observation import Observation
from fhir_types.hl7_fhir_us_core.profiles import UscoreBloodPressureProfile

bundle = json.load(open("bundle.json"))

def is_us_core_bp(resource: dict[str, Any]) -> bool:
    return (
        resource.get("resourceType") == "Observation"
        and UscoreBloodPressureProfile.canonical_url in (resource.get("meta", {}).get("profile") or [])
    )

bps = [
    UscoreBloodPressureProfile.from_resource(Observation.model_validate(entry["resource"]))
    for entry in bundle.get("entry", [])
    if is_us_core_bp(entry["resource"])
]

def avg(xs: list[float]) -> float:
    return sum(xs) / len(xs)

# get_systolic()/get_diastolic() are Optional, so guard with a walrus before indexing.
systolic = [s["value"] for bp in bps if (s := bp.get_systolic()) is not None]
diastolic = [d["value"] for bp in bps if (d := bp.get_diastolic()) is not None]

print(f"Avg BP: {avg(systolic):.1f}/{avg(diastolic):.1f} mmHg (n={len(bps)})")
```

```bash
$ python avg.py
Avg BP: 125.2/82.0 mmHg (n=5)
```

Three things the profile does here:

- **`from_resource(obs)` validates as it wraps.** It checks that `meta.profile` includes the canonical URL and returns a profile instance, raising if a resource that *claims* the profile is malformed — so a broken bundle fails at read time, not on the next field access.
- **No built-in type guard.** Unlike the TypeScript API's `is()` predicate, the Python classes don't ship a `.filter()`-style guard. You select candidates yourself — check `resourceType` and `canonical_url in meta.profile` (above), or wrap `from_resource()` in `try/except ValueError`. Either way `canonical_url` is exposed as a class attribute for exactly this.
- **`get_systolic()` / `get_diastolic()` return the flat slice value.** No walking `component[].code.coding[].code` to match LOINC codes — the profile already knows which slice is which, and hands you back the `Quantity` data as a plain dict.

That's the round-trip: CSV → typed profiles → validated Bundle → typed read-back with profile-aware getters. The same handful of lines would process BPs fetched from a FHIR server, loaded from a file, or received on a Subscription — the typed profile is the common shape, no matter the source.

## Step 6 — Land Your Bundle on a FHIR Server

The typed pipeline is only half the story. To actually see the transaction commit — patient IDs assigned, `urn:uuid` references rewritten, resources stored and searchable — you need a FHIR server. Spin up and run [Aidbox](https://www.health-samurai.io/aidbox):

```bash
curl -JO https://aidbox.app/runme && docker compose up -d
```

Open <http://localhost:8080> in your browser to grab a free developer license, then pull the root client secret out of `docker-compose.yaml` into an env var — reused by the curls and the Python script below:

```bash
export BOX_ROOT_CLIENT_SECRET=$(awk '/BOX_ROOT_CLIENT_SECRET:/{print $2}' docker-compose.yaml)
```

Verify the FHIR endpoint is up:

```bash
curl -u "root:$BOX_ROOT_CLIENT_SECRET" http://localhost:8080/fhir/metadata
```

You should see a JSON `CapabilityStatement`.

Send the `bundle.json` you just wrote with fhirpy's async client:

```python
import asyncio
import base64
import json
import os

from fhirpy import AsyncFHIRClient
from fhir_types.hl7_fhir_r4_core import Bundle

secret = os.environ["BOX_ROOT_CLIENT_SECRET"]  # exported above
auth = base64.b64encode(f"root:{secret}".encode()).decode()


async def main() -> None:
    client = AsyncFHIRClient("http://localhost:8080/fhir", authorization=f"Basic {auth}")
    bundle = json.load(open("bundle.json"))
    resp: Bundle = await client.execute("/", method="post", data=bundle)
    if resp.entry is None:
        return

    for entry in resp.entry:
        if entry.response is None:
            continue
        print(entry.response.status, entry.response.location)


asyncio.run(main())
```

Aidbox returns a `transaction-response` bundle — one entry per input, each with a `201 Created` and a `location` pointing at the stored resource:

```bash
$ python post.py
201 Created Patient/<id>/_history/1
201 Created Observation/<id>/_history/1
...
```

Query an observation back and look at its `subject`:

```bash
$ curl -u "root:$BOX_ROOT_CLIENT_SECRET" \
  "http://localhost:8080/fhir/Observation?code=http://loinc.org|85354-9" \
  | jq '.entry[].resource.subject.reference'
"Patient/01J..."
"Patient/01J..."
```

No `urn:uuid` — Aidbox rewrote the placeholders atomically on commit.

## Type-Check the Pipeline

The generated models are Pydantic v2, so the converter type-checks with [mypy](https://mypy-lang.org/) — already pinned in the generated `requirements.txt`. One requirement: enable the **Pydantic mypy plugin** (it ships with Pydantic, no extra install), or mypy can't tell that a `Field(None, ...)` default makes a field optional and floods you with false "missing argument" errors on every model you construct.

Drop a `mypy.ini` next to your code:

```ini
[mypy]
strict = True
plugins = pydantic.mypy
```

Then run it:

```bash
$ mypy .
Success: no issues found in 35 source files
```

Both your converter modules — `load.py`, `avg.py`, `post.py` — and the generated `fhir_types/` package come back clean. The typed factories, `to_resource()`, and the `Bundle[Patient | Observation]` generic all check out, so a wrong field type or a missing required argument is caught before you ever reach the server. The generated profile layer type-checks under full `--strict` too — the whole point of `mypy.ini` being just those two lines: no `disable_error_code`, no `strict_optional = False`, nothing hand-edited inside `fhir_types/`. The only thing your own code supplies is an ordinary None-guard when you read an optional field — the walrus in `avg.py` above, or `if entries is None: ...` before indexing a list. That's plain strict-mode Python, not a generator wart.

## Where To Go Next

- **More of the profile API.** Other factories, getters, and slice/extension forms are exercised in the codegen example tests: [`test_profile_patient.py`](https://github.com/atomic-ehr/codegen/blob/main/examples/python-r4-us-core/test_profile_patient.py), [`test_profile_bp.py`](https://github.com/atomic-ehr/codegen/blob/main/examples/python-r4-us-core/test_profile_bp.py), [`test_profile_bodyweight.py`](https://github.com/atomic-ehr/codegen/blob/main/examples/python-r4-us-core/test_profile_bodyweight.py), and [`test_profile_typed_bundle.py`](https://github.com/atomic-ehr/codegen/blob/main/examples/python-r4-us-core/test_profile_typed_bundle.py).
- **Typed bundles with named entry slices.** A profiled Bundle generates per-slice setters/getters (`set_patient_entry`, `get_organization_entry`) with single vs. unbounded (`max: *`) cardinality handled for you — see the typed-bundle test above.
- **Tune the output for your codebase.** `fieldFormat` (`snake_case`/`camelCase`), `client` (`"fhirpy"`/`"none"`), `allowExtraFields`, and `primitiveTypeExtension` are all toggles on `.python({ ... })`.
- **Mix profiles from multiple packages.** `APIBuilder.fromPackage()` chains — US Core alongside your custom IG or a regional base. `localStructureDefinitions()` pulls in profiles straight from a folder of `StructureDefinition` JSON.

## Wrap Up

The generator emits both the base R4 Pydantic models and a thin profile-class layer on top — no runtime DSL, no ORM, no framework. `to_resource()` always gives you a plain Pydantic resource, and `model_dump(by_alias=True, exclude_none=True)` always gives you plain FHIR JSON you can send to any server.

`@atomic-ehr/codegen` is MIT-licensed; issues and PRs welcome.

[GitHub](https://github.com/atomic-ehr/codegen) | [NPM](https://www.npmjs.com/package/@atomic-ehr/codegen) | [US Core IG](https://www.hl7.org/fhir/us/core/)


---

---
{
  "title": "Performance at Scale: Baseline",
  "description": "A baseline note for our performance at scale series: what we measure first, why the starting point matters, and how we keep the benchmark honest.",
  "date": "2026-06-29",
  "author": "Marat Surmashev",
  "reading-time": "6 min read",
  "tags": ["Database", "Infrastructure", "Aidbox", "Performance"]
}
---

## Why the baseline matters

Before we compare FHIR servers under load, we need a clean starting point. The baseline tells us how the systems behave on an empty (or nearly empty) database and gives us a reference for everything that comes later — when data volume and traffic start to matter.

As we said in our [previous post](/articles/performance-at-scale), we focus on the core FHIR workloads: CRUD, Bundle processing, and Search. The whole benchmark is **open source** — the source code, the test harness, and the results all live in [github.com/HealthSamurai/fhir-server-performance-benchmark](https://github.com/HealthSamurai/fhir-server-performance-benchmark), and the run is re-executed daily, so the [interactive report](https://healthsamurai.github.io/fhir-server-performance-benchmark/) (every chart plus the raw data) always reflects the latest numbers. The figures quoted in this post come from the run of June 28, 2026; the live report is the source of truth.

We test four FHIR servers: Aidbox, HAPI FHIR, Medplum, and the Microsoft FHIR Server. They span three different runtimes and two different databases. Aidbox and HAPI both run on the JVM over PostgreSQL; Medplum runs on Node.js, also over PostgreSQL; and the Microsoft FHIR Server runs on .NET and is the odd one out on storage — it doesn't support PostgreSQL, so we run the latest Microsoft FHIR Server on SQL Server.

| Server | Code base     | Runtime        | Database     |
|--------|---------------|----------------|-------------|
| [Aidbox](https://www.health-samurai.io/fhir-server)    | closed source | JVM (Clojure) | PostgreSQL  |
| [HAPI FHIR](https://github.com/hapifhir/hapi-fhir)  | open source | JVM (Java)     | PostgreSQL  |
| [Medplum](https://github.com/medplum/medplum)    | open source | Node.js        | PostgreSQL  |
| [Microsoft FHIR Server](https://github.com/microsoft/fhir-server)    | open source | .NET (C#)      | SQL Server  |

The Microsoft FHIR Server is the most recent addition to the benchmark, and the results below now include it alongside the three PostgreSQL-backed servers.


## Test environment

Our testing runs on a single bare-metal machine with 64 CPU cores and 500 GB of RAM. The whole stack is orchestrated with Docker Compose, and resources are pinned per container so the comparison stays fair:

| Server | Image | CPU / RAM | Topology |
|--------|-------|-----------|----------|
| Aidbox | `healthsamurai/aidboxone:edge` | 8 vCPU / 24 GB | single instance (JVM) |
| HAPI FHIR | `hapiproject/hapi:latest` | 8 vCPU / 24 GB | single instance (JVM) |
| Medplum | `medplum/medplum-server:latest` | 1 vCPU / 3 GB each | 8 replicas (Node.js) |
| Microsoft FHIR Server | `mcr.microsoft.com/healthcareapis/r4-fhir-server:latest` | 8 vCPU / 24 GB | single instance (.NET) |

Every application server gets the same budget: 8 vCPU and 24 GB of RAM. Medplum reaches it differently — its Node.js runtime is single-threaded, so a single process cannot use 8 cores; it is scaled out as 8 replicas of 1 vCPU / 3 GB each (8 vCPU and 24 GB in total) to compete on equal footing. Medplum additionally relies on Redis for sessions and caching, which the others do not need.

For the three PostgreSQL-based servers the database layer is **PostgreSQL 18** (8 vCPU / 30 GB), shared across the stack but isolated with a database-per-server model so no server's storage or query load can affect another's. The Microsoft FHIR Server cannot run on PostgreSQL, so it gets its own dedicated **SQL Server 2022** (Developer edition) on the same 8 vCPU / 30 GB budget — the SQL Server counterpart to the shared Postgres. Because the suites run sequentially, only one application server and its database are active at a time. Everything runs on local NVMe SSD storage to remove network latency from the results and keep the dataset close to the server.

For load generation we use [Grafana k6](https://k6.io/), which runs the scenarios in sequence (prewarm → CRUD → import → search). For synthetic data we use [Synthea](https://github.com/synthetichealth/synthea) because it produces realistic healthcare data patterns. Per-container CPU, memory, and I/O are collected with cAdvisor, while database internals come from postgres-exporter (for the PostgreSQL servers) and mssql-exporter (for the Microsoft FHIR Server's SQL Server). All of it is aggregated by Prometheus and visualized in Grafana — the same metrics that back the charts below. Full details are on the [infrastructure page](https://healthsamurai.github.io/fhir-server-performance-benchmark/infrastructure/) of the report.

> **A note on fairness.** We build Aidbox, so naturally we know it best. We are *not* experts in HAPI FHIR, Medplum, or the Microsoft FHIR Server, and it is entirely possible that our configuration for them is not optimal. Our goal was the opposite of cherry-picking: maximize hardware utilization and give every server the most equal resources we reasonably could. The whole setup is open source for exactly this reason — if you know how to tune any of these servers better, we genuinely want to know. Open an issue or a pull request against [the benchmark repo](https://github.com/HealthSamurai/fhir-server-performance-benchmark) and the daily run will pick up your changes.

## Test suites and scenarios

Performance testing is crucial for understanding system behavior under load. For this baseline we run three suites, in order:

1. **CRUD on an empty database**
   - Measure Create, Read, Update, and Delete performance on a fresh installation
2. **Batch import (1K patients)**
   - Perform a batch import of 1,000 synthetic (Synthea) patient records and measure throughput and storage
3. **Search**
   - Evaluate search operations performance against the imported dataset

This gives us a clean reference point. Larger datasets and incremental load — where write degradation and complex queries start to matter — are the subject of the next posts in the series.

## Baseline CRUD performance

The CRUD suite contains sequential Create, Read, Update, and Delete operations over nine different FHIR resources: Patient, Location, Practitioner, Organization, Encounter, MedicationRequest, Observation, Claim, and ExplanationOfBenefit. The suite runs with a constant 300 concurrent threads for up to 5 minutes.

We establish baseline metrics with the average RPS over all iterations.

__Average throughput (RPS) across all CRUD operations (higher is better)__
![CRUD operations RPS on empty database](total_rps.svg)

| Server | Total CRUD throughput (RPS) |
|--------|-----------------------------|
| Aidbox | 5,212 |
| HAPI   | 3,058 |
| Medplum | 1,420 |
| Microsoft FHIR Server | 440 |

__Average latency (99th percentile) by operation (lower is better)__
![CRUD operations p99 latency by operation](describe_rps.svg)

| Operation | Aidbox | HAPI | Medplum | Microsoft |
|-----------|--------|------|---------|-----------|
| Create | 106 ms | 276 ms | 758 ms | 1,180 ms |
| Read   | 91 ms | 225 ms | 404 ms | 379 ms |
| Update | 110 ms | 271 ms | 626 ms | 1,233 ms |
| Delete | 93 ms | 239 ms | 647 ms | 1,064 ms |

Based on these results, Aidbox delivers about 70% more throughput than HAPI and roughly 3.7x more than Medplum. The Microsoft FHIR Server is far behind on this suite, at 440 RPS — more than an order of magnitude below Aidbox. On latency, Aidbox is around 2.5x faster than HAPI and 6x faster than Medplum at the 99th percentile. The Microsoft FHIR Server is an interesting case: its reads are quick (379 ms p99, even ahead of Medplum), but its writes are the slowest of the group (~1.2 s p99 on create and update), which drags its overall throughput down.

One thing to keep in mind when reading these numbers: the throughput gap (≈70% over HAPI) is smaller than the latency gap (≈2.5x). Under high concurrency a server can hold up aggregate throughput by processing many requests in parallel even when each individual request is slower, so tail latency and total RPS don't necessarily move together — a single-number comparison can hide which one a given workload actually cares about.

__Average latency over all operations (99th percentile) by resource size (lower is better)__
![CRUD p99 latency by resource size](avg_latency_by_resource_size.svg)

The chart shows that Aidbox has a slight correlation between latency and resource size, most visible when processing Encounter, Patient, and ExplanationOfBenefit resources. HAPI shows more constant latency across resource sizes, while Medplum and the Microsoft FHIR Server are uniformly high on writes. We also see an interesting spike on Patient processing by Medplum (its create p99 jumps to ~1.16 s, versus ~560–830 ms for other resources), which suggests some extra work happens specifically for the Patient resource.

## Batch processing

To test batch processing capabilities, we used a dataset of 1,000 synthetic patient records generated with Synthea (around 2 million resources in total). The generated FHIR Bundles vary in size, ranging from small bundles around 150 KB to large ones up to 120 MB and up to 50,000 resources in a single bundle. Each Bundle has type `transaction`. To make the test more realistic and closer to real-world usage, we ran 20 concurrent threads processing these bundles.

__Average throughput (resources per second) by server (higher is better)__
![1,000 synthetic patient records import throughput](1k_import_throughput.svg)

| Server | Import throughput (resources/sec) |
|--------|-----------------------------------|
| Aidbox | 2,678 |
| HAPI   | 2,214 |
| Medplum | 764 |
| Microsoft FHIR Server | 448 |

On batch processing, Aidbox and HAPI lead and trade the top spot between runs — in this one Aidbox is ahead, ingesting 2,678 resources per second to HAPI's 2,214 (about 21% faster). Medplum lags well behind both at 764 resources per second, and the Microsoft FHIR Server is the slowest at 448 resources per second — roughly 5–6x behind the two JVM servers.

__Database size by server (lower is better)__
![1,000 synthetic patient records import database size](1k_import_db_size.svg)

Storage is where the four servers spread out the most, into two rough camps. The Microsoft FHIR Server (4.24 GB) and Aidbox (6.83 GB) stay compact; Medplum (11.8 GB) and HAPI (22.6 GB) are markedly larger — HAPI's footprint is about 3.3x Aidbox's. The two compact servers get there differently: Aidbox by not pre-building indexes, the Microsoft FHIR Server on its SQL Server datastore — yet, as we saw above, the Microsoft server pays for it elsewhere, in slow writes and searches. We return to the index tradeoff in the conclusion.

## Search suite

Benchmarking search functionality in a FHIR server comprehensively is a complex challenge for several reasons:

1. FHIR has an extensive set of search parameters, making it time-consuming to benchmark them all.
2. There are many combinations of search parameter type and value type.
3. Various modifiers and prefixes can be applied to searches.
4. There are complex operations like joins (`_include`, `_revinclude`, `_has`, chained) and sorting.
5. The number of possible search parameter combinations is vast.

Since testing every possible search parameter combination is impractical, we focus on the most commonly used search parameters. This gives us a solid baseline understanding of how efficiently each server implements its search logic. We concentrate on standard FHIR R4 search parameters that are frequently used in practice and have corresponding data in our synthetic dataset, covering six families: string, date, reference, quantity, token, and FHIR composite parameters.

We run the suite with 30 concurrent threads (k6 VUs) for 2 minutes, using a fixed page size of 20 results (`_count=20`). Since search operations can return large result sets, we exclude response transfer and parsing time from the measurements by enabling the `discardResponseBodies: true` k6 setting — we focus solely on server response time, not on FHIR conformance. All search families run mixed in a single iteration rather than sequentially, which is closer to real-world usage. Each family also fires a deliberately non-matching value (a non-existent id, code, or name) to exercise the empty-result path, and reference values are sampled live from each server (real resource ids) right before the run.

One caveat follows from discarding response bodies: we measure response time, not correctness. A server that mishandles a query type — returning an error, or an unfiltered result, quickly — can look artificially fast. We flag the one case we found (Medplum's composite search, below), but the same skepticism applies across the board: treat any unusually low latency for a given server-and-type as something to verify against the live results rather than take at face value.

The parameters we exercise:

| Search type | Resource | Parameters |
|-------------|----------|------------|
| String | Patient | `name`, `address` (with `:contains`) |
| String | Organization | `name` (with `:contains`) |
| Date | Patient | `birthdate` |
| Date | Observation | `date` |
| Date | Encounter | `date` |
| Reference | Observation | `subject`, `encounter`, `performer` |
| Reference | Encounter | `subject`, `participant` |
| Reference | MedicationRequest | `subject`, `encounter`, `requester` |
| Quantity | Observation | `value-quantity`, `component-value-quantity`, `combo-value-quantity` |
| Token | Observation | `category`, `code` |
| Token | Encounter | `status`, `class` |
| Composite | Observation | `code-value-quantity`, `component-code-value-quantity`, `combo-code-value-quantity` |

Date and quantity searches rotate through prefixes (`eq`, `lt`, `gt`, `ge`, `le`; date also adds `sa` and `eb`). String searches use the `:contains` modifier. Token searches cover plain `[code]`, fully-qualified `[system]|[code]`, and comma-separated OR lists. Composite searches use FHIR composite parameters with the `$` separator (for example `code-value-quantity=8867-4$gt100`).

### Search results

__Total search throughput (RPS, higher is better)__
![Search throughput by server](search_throughput.svg)

| Server | Search throughput (RPS) |
|--------|-------------------------|
| Aidbox | 3,404 |
| Medplum | 1,796 |
| HAPI   | 1,005 |
| Microsoft FHIR Server | 261 |

__P99 latency by search type (ms, lower is better)__
![Search p99 latency by type](search_latency.svg)

| Search type | Aidbox | Medplum | HAPI | Microsoft |
|-------------|--------|---------|------|-----------|
| String    | 24 | 81 | 77  | 188 |
| Date      | 26 | 82 | 121 | 271 |
| Reference | 24 | 83 | 96  | 167 |
| Token     | 32 | 82 | 97  | 197 |
| Quantity  | 55 | 91 | 101 | 1,191 |
| Composite | 47 | — | 125 | 1,897 |

*The chart shows all four servers. The Microsoft FHIR Server's quantity (~1.2 s) and composite (~1.9 s) p99 run off the scale and squash the three PostgreSQL servers into short bars near the baseline — their exact values are in the table above. Medplum is absent from the composite group because it does not support that search type (see below).*

The ordering changes compared to CRUD. On search, Aidbox leads throughput at 3,404 RPS — about 90% more than Medplum (1,796 RPS) and roughly 3.4x more than HAPI (1,005 RPS). The Microsoft FHIR Server is far behind at 261 RPS. Notably, HAPI — which was second on CRUD — is the slowest of the three PostgreSQL servers on search, both on throughput and on tail latency.

On latency, Aidbox returns the lowest p99 on every search family. Medplum is consistently in the middle — except on composite, which it does not support (see the note below). Among the Postgres servers, HAPI's latency is the highest, hovering around 95–125 ms across families. The Microsoft FHIR Server is in a different regime entirely: tolerable on string, date, reference, and token searches (~170–270 ms) but extremely slow on quantity (~1.2 s) and composite (~1.9 s) searches — which is exactly what sinks its search throughput.

> **Medplum does not support composite search.** Medplum's [search architecture documentation](https://www.medplum.com/docs/contributing/search-architecture) lists the FHIR `composite` parameter type as unsupported. In the benchmark, these requests return without performing the search, so any latency or throughput they report is an artifact rather than a real measurement — which is why Medplum appeared to "beat" everyone on composite in earlier drafts. We therefore leave Medplum's composite cell blank and drop it from the chart. Quantity search, by contrast, *is* supported by Medplum, so those numbers stand.

A note on scope: this baseline does not include sorting (`_sort`) or join operations (`_include`, `_revinclude`, `_has`, chained). They are most interesting once data volume grows — on a 1K-patient dataset almost everything fits in memory — so we add them on larger datasets in a later post.

## Conclusion

On a clean, mostly in-memory baseline the standings line up like this — they shift somewhat between daily runs, so read them as a snapshot, not a verdict:

- **CRUD** — Aidbox leads throughput (5,212 RPS) over HAPI (3,058) and Medplum (1,420), with the best p99 latency on every operation (~100 ms vs ~250 ms for HAPI and ~610 ms for Medplum). The Microsoft FHIR Server trails at 440 RPS, with fast reads but ~1.2 s write latency.
- **Batch import** — Aidbox leads ingestion this run (2,678 vs HAPI's 2,214 resources/sec); Medplum is well behind (764), and the Microsoft FHIR Server is the slowest (448).
- **Search** — Aidbox leads both throughput (3,404 RPS) and per-query latency; HAPI drops to last among the Postgres servers, and the Microsoft FHIR Server is far behind on throughput (261 RPS), sunk by very slow quantity and composite queries.
- **Storage** — two camps: the Microsoft FHIR Server (4.24 GB) and Aidbox (6.83 GB) are compact, while Medplum (11.8 GB) and HAPI (22.6 GB) are larger — HAPI's footprint is about 3.3x Aidbox's.

### Indexing strategy: a tradeoff this baseline only half-measures

The storage and import numbers follow directly from how each server stores and indexes data — and that choice is a tradeoff, not a verdict.

HAPI, Medplum, and the Microsoft FHIR Server pre-build indexes on searchable fields as data is written. That costs write throughput, and for HAPI and Medplum it costs storage (the 12–23 GB above). The Microsoft FHIR Server shows the storage cost isn't inevitable — it pre-builds indexes and still stays at 4.24 GB — but it pays heavily in write latency.

Aidbox takes the opposite default, and it cuts both ways. It ships with **no search indexes at all** — which is what makes its imports fast and its footprint small, but it also means an unindexed query falls back to a sequential scan, and the responsibility for indexing moves onto you. That is not a trivial job: you have to know which search parameters your workload actually hits, and over-indexing brings back exactly the write and storage costs we just described. What Aidbox offers in place of pre-built indexes is the tooling to do it deliberately — query analytics, statistics on which search parameters are actually used, and index recommendations derived from real traffic. Used well, that lets you index precisely for your workload and get both fast operations and a very compact, efficiently stored dataset; used carelessly, you get a server with no indexes. It is a more powerful default in expert hands and a sharper edge in inexperienced ones.

The important caveat — and the reason to read the search numbers above carefully — is that pre-building indexes buys something: predictable query performance as data grows. A 1K-patient dataset that fits comfortably in memory is precisely the case where that payoff doesn't show up, so this baseline structurally flatters the no-index default on search. Whether that advantage holds as data grows — and whether write-time degradation, not just raw speed, starts to dominate — is exactly what the next post is designed to measure.

## Next in the series

This is the starting point. In the next posts we move from the baseline to heavier workloads — larger datasets and incremental load — to measure how import speed degrades as data accumulates, how CRUD and complex search (sorting and joins) hold up on a full database, and how the storage gap projects at realistic data volumes.

*Follow us on [LinkedIn](https://www.linkedin.com/company/health-samurai) for the next benchmark update.*


---
