Skip to content

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.

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

athenahealth, Practice Fusion, Nextech and Compulink send subject: { reference: "Patient/123" } without type. Inject it from the reference prefix before linking resources to patients.

  • 25–35% of completed encounters arriving as in-progress (athenahealth).
  • status absent or unknown (some bundle exports).
  • Encounter.type that 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.

  • 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. A coding[0] parser finds zero readings.
  • Drug codes may exist only on a referenced Medication resource, not inline on the MedicationRequest (athenahealth).
  • Screenings may be stored as Condition resources 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).

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.

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