Extraction patterns across EHRs
The same handful of problems show up on almost every EHR. Each EHR page lists its own variants; this page collects the patterns.
Bulk export is rarely usable as advertised
Section titled “Bulk export is rarely usable as advertised”| What happens | Seen on |
|---|---|
| Works, but one resource type per job, sequentially, and slowly | eClinicalWorks |
| Latency from 3 minutes to 4+ hours, unrelated to data volume | athenahealth |
Manifest returns 200 while its output list is still filling |
ModMed |
| Accepts every kickoff, then crawls (1% after 34 minutes) | Nextech |
Only Group/[id]/$export; without _since, only the last 24 hours are exported |
Greenway |
_type ignored — every type comes back every time |
Practice Fusion |
| Requires a patient Group the customer must build | Epic, NextGen |
Design for it: detect stalls by watching progress stop changing (for example the X-Progress
header) rather than using a wall-clock timeout; require the manifest to be stable across several
polls before accepting it; and keep a search-based fallback.
Population search is often forbidden
Section titled “Population search is often forbidden”Many EHRs refuse searches that are not scoped to a patient — athenahealth returns 403, others return 400 “patient or _id must be supplied”. Your cohort then has to come from somewhere else: a bulk export of encounters, encounters searched by practitioner NPI, a patient list you already hold, or (as a last resort) enumerating patients by name.
_since and _lastUpdated select by edit time
Section titled “_since and _lastUpdated select by edit time”They filter on meta.lastUpdated. A January visit edited in June appears in an export with _since
in June; an unedited one does not. Some servers ignore _lastUpdated entirely. Always decide
measurement-year membership yourself from period.start or effective[x].
Paging is the biggest source of silent data loss
Section titled “Paging is the biggest source of silent data loss”| Pitfall | Seen on |
|---|---|
| Default page of 10 | Oracle Health (Cerner) |
Default page of 20 with no next link unless you send _count |
athenahealth |
Per-resource page caps (50, 90, 100) whatever _count you send |
athenahealth |
Next-page offset computed from the _count you asked for, not what was served — the next page 404s |
athenahealth |
Bundle total reports the page size, not the number of matches |
ModMed |
_count rejected with 400; only link.next works |
eClinicalWorks |
page=N only, no link.next |
Compulink |
Vendor parameters duplicated onto next URLs cause 403 |
athenahealth |
Rules that cover all of them: always send an explicit _count; always walk link.next to the end;
cap the number of pages; and when you hit the cap or a page fails, mark the patient incomplete
rather than treating the missing data as clinically absent.
HTTP 200 with zero results is not proof of absence
Section titled “HTTP 200 with zero results is not proof of absence”- A bare year in a date search (
date=2026) returns 200 with nothing on some servers; the prefixed form (date=ge2026-01-01) returns data. (ModMed) category=vital-signsreturns 200 with nothing; searching by the blood-pressure panel code works. (eClinicalWorks)- A practice that has not enabled your app returns 200 with empty searchsets. (Greenway)
- The reverse also exists: 404
{"error":"[no-data]"}meaning an empty result. (Compulink) - Scopes granted, but the clinical module that feeds FHIR is not populated.
References often lack type
Section titled “References often lack type”athenahealth, Practice Fusion, Nextech and Compulink send subject: { reference: "Patient/123" }
without type. Inject it from the reference prefix before linking resources to patients.
Encounter status and type are unreliable
Section titled “Encounter status and type are unreliable”- 25–35% of completed encounters arriving as
in-progress(athenahealth). statusabsent orunknown(some bundle exports).Encounter.typethat is a data-absent reason (eClinicalWorks), a generic “summary clinical document” code (Practice Fusion), or a proprietary code that matches no measure value set (Oracle Health).
A practical rule: only enforce the measure’s qualifying-encounter value sets when they match a meaningful share of the year’s encounters (we use 10%); otherwise fall back to counting any completed visit, and flag the result.
Coded data quirks
Section titled “Coded data quirks”- Scan every coding, never just
coding[0]. On Oracle Health the first coding of a blood pressure component is proprietary; the LOINC is further down. Acoding[0]parser finds zero readings. - Drug codes may exist only on a referenced
Medicationresource, not inline on theMedicationRequest(athenahealth). - Screenings may be stored as
Conditionresources rather than reports or procedures (Practice Fusion). - Depression screening scores may exist only inside note attachments (HTML or PDF), or only as
QuestionnaireResponse(Elation). - Duplicate lab results with different coding and units for the same order (eClinicalWorks A1c — see that page).
Attribution is not in FHIR
Section titled “Attribution is not in FHIR”TIN is never in the data. NPI is often missing or published as a data-absent reason. Plan for a
practice roster or mapping you maintain, and for attributing each encounter to its primary
performer (participant type PPRF) who belongs to the practice.
Resilience
Section titled “Resilience”- Request level: a few retries on 0, 429 and 5xx with exponential backoff, honouring
Retry-After. - Job level: pause the whole cascade on sustained 429s (double the pause, cap it), and mark an endpoint dead after repeated consecutive failures.
- Daily quotas matter more than burst limits: a per-patient cascade on a 70,000-patient practice used a whole day’s API allowance while peaking at only 20% of the per-second limit.
- Prevent overlapping runs with a single-instance lock; make runs resumable.