← Blog
Healthcare

Bulk FHIR Export for Analytics and AI: Limits, Cost, De-ID

Thien Nguyen · Nov 11, 2026

Bulk FHIR ($export) is the right tool for periodic, cohort-based extracts for analytics, quality reporting and AI evaluation sets. It is the wrong tool for near-real-time warehouse sync. Expect asynchronous jobs measured in hours, NDJSON files that expire (14 days at Epic), Group-only support at major EHRs, and a de-identification step before any AI use.

Population data is suddenly on every roadmap. AI teams want training and evaluation sets, payers have CMS-0057-F Provider Access and Payer-to-Payer APIs due from January 1, 2027, and quality and value-based care programs need cohort extracts. The HL7 Bulk Data Access IG, also called Flat FHIR, is now at v3.0.0 (STU 3). But what the spec allows and what each EHR implements are different things. This guide covers the protocol, the vendor limits, the performance and cost drivers, and how to de-identify before data reaches a model.

What is Bulk FHIR export?

It is an asynchronous FHIR operation ($export) that returns large amounts of data for a system, a group or a set of patients as newline-delimited JSON (NDJSON) files. You start a job, poll a status URL, then download files listed in a manifest.

The flow from the export specification:

  1. Kick-off at [base]/$export, [base]/Patient/$export or [base]/Group/[id]/$export with Prefer: respond-async. Success is 202 Accepted with a Content-Location header pointing to the status endpoint.
  2. Poll status. While running, the server returns 202 with optional X-Progress and Retry-After. Respect Retry-After.
  3. Read the manifest when complete: transactionTime, requiresAccessToken, and arrays of output, deleted and error files.
  4. Download NDJSON files (servers "SHALL support Newline Delimited JSON"). If requiresAccessToken is true, file requests need the same access token protection as the kick-off.
  5. Clean up with a DELETE to the status URL, which servers may treat as a signal that files can be removed.

Authorization should follow SMART Backend Services, with scopes that cover every exported resource type.

Four steps of a Bulk FHIR export: 1 kick off a Group export with Prefer respond-async and type filters, receiving 202 and a Content-Location status URL; 2 poll the status URL respecting Retry-After, at Epic every 10 minutes for groups under 100 patients and every 30 minutes above; 3 read the manifest's output, error and deleted arrays and download NDJSON files; 4 send DELETE to clean up, noting Epic deletes results after 14 days.
Asynchronous by design: plan for hours, not seconds.

What is new in Bulk Data v3.0.0?

STU 3 adds partial manifests (allowPartialManifests) so servers can release files before the whole job finishes, output organized by resource instance (organizeOutputBy), an _until parameter to pair with _since, a Group profile for characteristic-based cohorts, and promotes _typeFilter from experimental.

According to the IG's change log, STU 2 had already added POST kick-off with a Parameters resource, _elements, a patient filter and includeAssociatedData. The Argonaut accelerator is specifying $bulk-submit and $bulk-publish for a future release. Do not build on v3 features until your source EHR advertises them; most production EHRs implement earlier versions, as the next section shows.

What do Epic and Oracle Health actually support?

Less than the spec. Epic supports only Group export (based on v1.0.1 plus some later features) and does not support _since. Oracle Health supports Group export (v1.0.1 plus some v2 parameters) and Patient export (v2.0.0) that requires explicit patient IDs. Both run exports on the provider's operational systems.

EpicOracle Health (Millennium)
Export levelsGroup onlyGroup and Patient (Patient needs a list of IDs, no "all patients")
Spec version1.0.1 plus some later featuresGroup: 1.0.1 plus some v2.0.0 experimental parameters; Patient: 2.0.0
Incremental (_since)Not supportedNot documented on the bulk page
Filters_type, _typeFilter, includeAssociatedDataUnsupported parameters return 422 unless Prefer: handling=lenient
Result retention14 days, then deletedNot documented on the bulk page
Polling guidanceEvery 10 min for groups under 100 patients, every 30 min above429 when too many requests
Who defines the groupThe health system (you get the Group FHIR ID from them)The health system, in Ignite Management Tooling

Sources: Epic's bulk data tutorial and Oracle's Bulk Data Access documentation. Epic is explicit about fit: it lists "data synchronization with data warehouses," "periodic loads of large amounts of clinical data" and "incremental data loads" as poor use cases, and says results are not incremental and cannot be retrieved until the whole job is ready.

Is Bulk FHIR good for AI training and evaluation data?

For evaluation sets and cohort-scoped fine-tuning data, yes, after de-identification and with a clear legal basis. For continuously refreshed training corpora across a whole health system, usually no: the extract model, vendor limits and load on operational systems make it a poor fit.

Where bulk works well for AI:

  • Evaluation sets: a fixed, versioned cohort (for example, 2,000 encounters with known outcomes) exported once, de-identified, frozen and reused for every model and prompt change. See LLM testing for regulated industries.
  • Retrieval indexes for a defined population: a care-management program's patients, refreshed on a schedule the health system agrees to.
  • Feature tables for risk models where weekly or monthly freshness is acceptable.

Where it does not: real-time decision support (use EHR-native APIs or CDS Hooks), whole-enterprise data lakes (ask about the EHR vendor's warehouse options), and anything that needs updates within minutes.

How do you de-identify FHIR data before it reaches a model?

Use one of HIPAA's two methods. Safe Harbor removes 18 identifier types, including all date elements except year, and all but the first three ZIP digits (those too, where that area has 20,000 people or fewer). Expert Determination has a qualified expert document that re-identification risk is "very small." Free text needs extra work in both cases.

The rules are in 45 CFR 164.514. FHIR-specific traps:

  • Identifiers are everywhere, not just in Patient: Encounter.identifier, DocumentReference metadata, Coverage member IDs, device identifiers, and URLs in Attachment.url.
  • Dates drive re-identification. Safe Harbor drops all date elements except year. If your model needs intervals, consider date shifting per patient under Expert Determination instead.
  • Ages over 89 must be grouped as 90 or older under Safe Harbor.
  • Clinical notes (DocumentReference, Binary) contain names, addresses and dates in free text. Rule-based scrubbing misses some; measure your residual rate on a labeled sample.
  • References keep links. Replace resource IDs consistently (keyed hashing with a secret held by the covered entity) so the graph stays intact without exposing source IDs. Under 164.514(c), any re-identification code must not be derived from the individual's information.
  • "Actual knowledge" still applies: Safe Harbor fails if you know the remaining data could identify someone.

If the use is research, public health or operations and you need ZIP codes or dates, a limited data set under a data use agreement may fit better than full de-identification.

Four HIPAA Safe Harbor rules for FHIR exports: remove 18 identifier types across all resources; keep only the year of dates; keep at most the first 3 ZIP digits and only if that area has more than 20,000 people; group ages over 89 into a single 90 or older category.
Free-text notes need separate scrubbing and measurement.

How do you turn NDJSON into analytics tables?

Land raw NDJSON unchanged in object storage, then flatten into columnar tables with declared, versioned view definitions. SQL on FHIR v2 ViewDefinitions are the emerging standard for this; the SQL on FHIR specification's continuous build is at 2.1.0-pre.

A pipeline that holds up

  1. Raw zone: NDJSON as delivered, partitioned by export transactionTime, encrypted, with access limited to the pipeline role.
  2. Validation: profile checks (US Core where applicable), counts per resource type versus the manifest, and the error files reviewed, not ignored.
  3. De-identified zone: a separate account or project, written only by the de-identification job.
  4. Flat tables: ViewDefinitions for Patient, Encounter, Condition, Observation and so on, stored as Parquet, with the definition version in the table metadata.
  5. Lineage: every row traceable to export job, file and resource ID (or its pseudonym).

What drives Bulk FHIR performance and cost?

Population size, number of resource types, date range and the source system's load set the run time. Your costs come from storage, compute for flattening and de-identification, and engineering time per health system. Narrow filters reduce all of them.

  • Filter at the source: Epic recommends _type and _typeFilter "whenever possible" to cut response time and storage. Binary is excluded by default at Epic and only retrievable via _type.
  • Poll politely: follow Retry-After and vendor guidance; aggressive polling risks 429s and goodwill.
  • Schedule with the customer: exports run on operational databases. Agree windows.
  • No _since? Design for it: where incremental export is missing, keep cohorts small and stable, and diff full extracts on your side instead of re-processing everything downstream.
  • Check lenient handling: on Oracle Health, Prefer: handling=lenient silently drops unsupported parameters. Verify your filters actually applied.
  • Download before expiry: at Epic you have 14 days from kick-off to finish and download.

The spec also lets a server reply 429 to prevent a new export while one is still running, so do not launch parallel jobs for the same client without checking the server's policy. For running cost of the AI layer on top, see the cost to run an AI product.

What are the most common Bulk FHIR mistakes?

Treating bulk as a sync mechanism, assuming every server supports _since and Patient-level export, ignoring the error and deleted arrays, keeping PHI files past their purpose, and sending raw exports to a model provider without a BAA or de-identification.

Also common: forgetting that Group membership is controlled by the health system and can change between runs, and not recording transactionTime, which you need for any later incremental logic. For governance of the AI that consumes this data, see HIPAA-compliant AI agents and the HIPAA Security Rule and AI in 2026.

How BeevR builds this

We build bulk export clients on SMART Backend Services, land NDJSON in encrypted, access-controlled storage, validate against the manifest, de-identify in a separate zone with measured residual risk on notes, and flatten with versioned view definitions so every number in a dashboard or evaluation set traces back to a source file. We agree cohorts and schedules with each health system and tell you when bulk is the wrong tool. BeevR signs a BAA as a business associate, models can run inside your own AWS account under the AWS BAA, and you own the source code and IP from day one.

Our senior, founder-led team in Hanoi runs a multi-tenant EHR live across four US clinics at about 300 patients a day, with a working demo every Friday and fixed-scope, fixed-price phases from $10K per phase. See our EHR integration services and healthcare AI agent development.

Building an analytics or AI pipeline on EHR data? Book a 30-minute call and we will check your use case against what your source EHRs actually support.