A FHIR Observation is the R4 resource that carries one measurement or simple assertion about a patient: a heart rate, a potassium result, an AUDIT-C score, a smoking status. If your product ingests vitals, labs, remote monitoring readings or screening scores from an EHR, almost all of it arrives as Observation resources, each pairing a code (usually LOINC) with a value, a time and a subject.
This field guide checks each element against the FHIR R4 (4.0.1) specification and US Core, uses JSON captured from public test servers and the spec, and shows what Epic and Oracle Health document for reading and writing Observations.
Key takeaways
- Base R4 Observation requires only
statusandcode. Profiles such as US Core make subject, category and effective time mandatory. - Vital signs use fixed LOINC codes and UCUM units. Blood pressure is one Observation (85354-9) with systolic and diastolic components.
- Lab results are Observations referenced from
DiagnosticReport.result; the Observation carries value, reference range and interpretation. - Screenings use
hasMemberto link a panel to item Observations.componentis for values that cannot stand alone. - The
codesearch parameter matchesObservation.codeonly, so systolic 8480-6 will not find a blood pressure panel. - EHRs differ on value types, reference ranges, local codes and write support. Read each vendor's docs before designing ingestion.
What is the FHIR Observation resource?
Observation is the FHIR resource for measurements and simple assertions about a patient, device or other subject. It holds vital signs, lab results, imaging findings, device readings, screening scores, social history and exam findings. Each instance records what was measured (code), the result (value), when it was true (effective) and whose record it belongs to (subject).
The R4 Observation specification also implies what Observation is not. A diagnosis belongs in Condition (see our FHIR Condition resource guide), the order-level lab report belongs in DiagnosticReport, and raw questionnaire answers belong in QuestionnaireResponse. Observation sits between them and points to them. New to how resources fit together? Start with what FHIR is and how it models healthcare data.
Observation anatomy: elements and cardinality
Observation has two dozen top-level elements; ingestion logic depends on about sixteen. Only status and code are mandatory in base R4, which is why the same clinical fact looks different on every server. Here is each element with its most common pitfall.
| Element | Card. | Type | What it holds | Common pitfall |
|---|---|---|---|---|
| status | 1..1 | code | registered, preliminary, final, amended, corrected, cancelled, entered-in-error, unknown | A modifier. Filtering on final alone drops corrected results. |
| category | 0..* | CodeableConcept | Broad class, such as vital-signs | Optional in base, required by US Core. Can repeat. |
| code | 1..1 | CodeableConcept | What was measured, usually LOINC | Local codes sit beside LOINC. Match system plus code. |
| subject | 0..1 | Reference(Patient | Group | Device | Location) | Whose record it is | US Core requires a Patient. |
| encounter | 0..1 | Reference(Encounter) | Visit context | Often absent for outside labs and home readings. |
| effective[x] | 0..1 | dateTime | Period | Timing | instant | Collection or measurement time | Chart on this, not on issued. |
| issued | 0..1 | instant | When this version was released | Good for new-result alerts only. |
| performer | 0..* | Reference | Who is responsible for the value | The Patient for home readings. |
| value[x] | 0..1 | 11 types: Quantity, CodeableConcept, string, boolean, integer, Range, Ratio and more | The result | Never assume valueQuantity. |
| dataAbsentReason | 0..1 | CodeableConcept | Why the value is missing | obs-6: only when value[x] is absent. |
| interpretation | 0..* | CodeableConcept | H, L, N, A, HH, LL | Extensible, so local flags appear. |
| device | 0..1 | Reference(Device | DeviceMetric) | The measuring device | The cuff, not the relaying phone. |
| referenceRange | 0..* | BackboneElement | low, high, type, appliesTo, age, text | obs-3: needs low, high or text. |
| hasMember | 0..* | Reference(Observation | QuestionnaireResponse | MolecularSequence) | Panel members | The panel itself has no value. |
| derivedFrom | 0..* | Reference | Source data | Links a score to its answers. |
| component | 0..* | BackboneElement | Values measured together | code search ignores component codes. |
Three base invariants catch most malformed data. obs-3: a reference range needs a low, high or text. obs-6: dataAbsentReason appears only when there is no value. obs-7: if code repeats a component code, the top-level value must be empty. Validate these on ingest. Our guide to FHIR profiles, extensions and Must Support explains how profiles tighten the optional elements.
Which Observation category codes will you see?
R4 defines nine codes in the observation-category code system: vital-signs, laboratory, social-history, imaging, procedure, survey, exam, therapy and activity. The binding is Preferred, so servers can add their own. Category is the first filter you apply, and in some EHR APIs it changes which other search parameters are honored.
| Category | Spec scope | Typical integration source |
|---|---|---|
| vital-signs | BP, heart and respiratory rate, height, weight, BMI, SpO2, temperature | Nursing flowsheets, RPM devices |
| laboratory | Specimen-based chemistry, hematology, microbiology, pathology | LIS feeds, reference labs |
| social-history | Lifestyle, occupational and environmental history | Smoking status, alcohol use |
| survey | Assessment tools and survey instruments | PHQ-9, AUDIT-C, Apgar |
| exam | Physical exam findings | Structured exam documentation |
| imaging | Findings from x-ray, ultrasound, CT, MRI | Measurements from imaging reports |
| procedure | Results of non-lab, non-imaging procedures | Endoscopy, cardiac cath |
| therapy | Non-interventional treatment protocols | Physical, nutrition therapy |
| activity | Activity outside clinical supervision | Steps, sleep from wearables |
Definitions are in the R4 category value set. Real APIs support a subset and add codes: Oracle Health lists laboratory, vital-signs, social-history, survey and sdoh.
FHIR vital signs: LOINC codes and UCUM units
The R4 vital signs profile fixes a LOINC code and UCUM unit for each common vital sign. Each vital sign Observation needs a status, the vital-signs category, one of these codes, a patient, an effective time, and either a numeric value with a UCUM unit or a dataAbsentReason. The bindings are extensible, so additional codings are allowed.
| Vital sign | LOINC | UCUM unit | Spec note |
|---|---|---|---|
| Vital signs panel | 85353-1 | none | Members via hasMember. Replaces 8716-3. |
| Respiratory rate | 9279-1 | /min | |
| Heart rate | 8867-4 | /min | |
| Oxygen saturation | 2708-6 | % | 59408-5 (pulse oximetry) may be added. |
| Body temperature | 8310-5 | Cel, [degF] | |
| Body height | 8302-2 | cm, [in_i] | |
| Body length (lying) | 8306-3 | cm, [in_i] | Typically infants. |
| Head circumference | 9843-4 | cm, [in_i] | |
| Body weight | 29463-7 | g, kg, [lb_av] | |
| Body mass index | 39156-5 | kg/m2 | |
| Blood pressure panel | 85354-9 | none | Components only. |
| Systolic BP | 8480-6 | mm[Hg] | Component code. |
| Diastolic BP | 8462-4 | mm[Hg] | Component code. |
| Mean BP | 8478-0 | mm[Hg] |
Source: the R4 vital signs profile. Two details break parsers. valueQuantity.unit is display text and valueQuantity.code is the UCUM code, so "mmHg" and "mm[Hg]" appear together; convert on system plus code. And when a local code is recorded, the spec expects the standard LOINC too, so pick the LOINC coding from code.coding. For mapping at scale, see terminology services for SNOMED, LOINC and RxNorm.
US Core 9.0.0 adds a profile for each vital sign plus pediatric percentiles, listed on the US Core profiles page. Which version an EHR certifies against is covered in USCDI vs US Core version mapping.
How is blood pressure represented in FHIR?
Blood pressure is one Observation coded 85354-9 with no top-level value and two components, systolic (8480-6) and diastolic (8462-4), each a valueQuantity in mm[Hg]. Both components are required, though either may carry a dataAbsentReason. This response from the public HAPI FHIR R4 server is trimmed, with identifiers replaced:
{
"resourceType": "Observation",
"status": "final",
"category": [{ "coding": [{ "system": "http://terminology.hl7.org/CodeSystem/observation-category", "code": "vital-signs" }] }],
"code": { "coding": [{ "system": "http://loinc.org", "code": "85354-9", "display": "Blood pressure panel" }] },
"subject": { "reference": "Patient/example" },
"effectiveDateTime": "2026-07-30T10:27:00",
"component": [
{
"code": { "coding": [{ "system": "http://loinc.org", "code": "8480-6", "display": "Systolic blood pressure" }] },
"valueQuantity": { "value": 120, "unit": "mmHg", "system": "http://unitsofmeasure.org", "code": "mm[Hg]" }
},
{
"code": { "coding": [{ "system": "http://loinc.org", "code": "8462-4", "display": "Diastolic blood pressure" }] },
"valueQuantity": { "value": 80, "unit": "mmHg", "system": "http://unitsofmeasure.org", "code": "mm[Hg]" }
}
]
} The captured timestamp has no timezone offset, although R4 says one SHALL be present when a time is given. Decide which zone to assume and flag it. The search trap: code=8480-6 will not return this resource. Search on 85354-9, or use combo-code, which matches code and component.code where supported.
FHIR lab results: DiagnosticReport plus Observation
A lab result has two layers. DiagnosticReport represents the ordered test or panel, its status and conclusion. Each analyte is a separate Observation referenced from DiagnosticReport.result, holding the value, units, reference range and interpretation. Result Observations can also be searched on their own, without the report.
In the SMART Health IT R4 sandbox, a Basic Metabolic Panel report (51990-0) references result Observations coded 2339-0 (glucose), 6299-2 (urea nitrogen), 38483-4 (creatinine), 2947-0 (sodium) and 6298-4 (potassium). That synthetic data omits reference ranges, so here is the glucose example from the R4 spec, trimmed:
{
"resourceType": "Observation",
"status": "final",
"code": { "coding": [{ "system": "http://loinc.org", "code": "15074-8", "display": "Glucose [Moles/volume] in Blood" }] },
"subject": { "reference": "Patient/example" },
"effectivePeriod": { "start": "2013-04-02T09:30:10+01:00" },
"issued": "2013-04-03T15:30:10+01:00",
"valueQuantity": { "value": 6.3, "unit": "mmol/l", "system": "http://unitsofmeasure.org", "code": "mmol/L" },
"interpretation": [{ "coding": [{ "system": "http://terminology.hl7.org/CodeSystem/v3-ObservationInterpretation", "code": "H", "display": "High" }] }],
"referenceRange": [{
"low": { "value": 3.1, "unit": "mmol/l", "system": "http://unitsofmeasure.org", "code": "mmol/L" },
"high": { "value": 6.2, "unit": "mmol/l", "system": "http://unitsofmeasure.org", "code": "mmol/L" }
}]
} The US Core Laboratory Result Observation Profile requires the laboratory category and a LOINC code when available, and marks as Must Support the value, UCUM units, SNOMED CT for coded values, interpretation, reference range and specimen type. Interpretation codes come from the interpretation value set: H, L, N, A, HH (critical high), LL (critical low), POS and NEG. Three rules keep lab data honest:
- Keep the lab's flag. Do not recompute H or L from a generic range; ranges vary by lab, method, age and sex.
- Respect range semantics. Multiple
referenceRangeentries mean OR, multipleappliesTocodes mean AND, and a range can be text only, such as "Negative". - Handle status transitions. Upsert on resource id so a corrected potassium replaces the preliminary one.
If results start as HL7 v2 ORU messages, our LIS integration guide for ORU to DiagnosticReport mapping covers the OBX conversion.
Screening assessments and panels: hasMember vs component
Use component when values only make sense together and share every other attribute, like systolic and diastolic pressure. Use hasMember when each part is a standalone Observation, like the questions and total score of a screening instrument or the members of a vital signs panel.
The US Core Screening Assessment Profile models instruments as a survey-category panel with an overarching code, an empty value and hasMember references to one Observation per question. This is the AUDIT-C panel example published with US Core 9.0.0, trimmed:
{
"resourceType": "Observation",
"status": "final",
"category": [{ "coding": [{ "system": "http://terminology.hl7.org/CodeSystem/observation-category", "code": "survey" }] }],
"code": { "coding": [{ "system": "http://loinc.org", "code": "72109-2", "display": "Alcohol Use Disorder Identification Test - Consumption [AUDIT-C]" }] },
"subject": { "reference": "Patient/example" },
"effectiveDateTime": "2023-08-14T20:40:49.675000+00:00",
"hasMember": [
{ "reference": "Observation/AUDIT-C-item-example-68517-2" },
{ "reference": "Observation/AUDIT-C-item-example-68519-8" },
{ "reference": "Observation/AUDIT-C-item-example-68520-6" },
{ "reference": "Observation/AUDIT-C-item-example-75626-2" }
],
"derivedFrom": [{ "reference": "QuestionnaireResponse/AUDIT-C" }]
} The last member, 75626-2 (total score), carries valueQuantity with UCUM code "1". A search for patient=X&category=survey returns the panel and every item as separate entries, so ingestion must assemble panels by following hasMember or users see four unrelated scores instead of one AUDIT-C result.
How EHRs populate Observation differently
Two conformant servers can return the same potassium or blood pressure in different shapes. Differences cluster around value types, reference range formats, component versus separate resources, local codes and flowsheet-backed storage. Where a vendor documents a behavior we cite it; treat the rest as patterns to test on every target.
| Pattern | What you receive | How to handle it |
|---|---|---|
| Value type | valueString for qualified or free-text results, valueQuantity elsewhere | Branch on type, keep the original string |
| Reference ranges | Structured low and high, or text only | Store and show text; never invent bounds |
| Blood pressure | Components, or values stored separately upstream | Normalize to 85354-9; Oracle Health pairs components only when paired in Millennium |
| Codes | LOINC plus local codes, or local only | Map through a terminology service |
| Flowsheet data | Vitals filed as flowsheet rows mapped to LOINC | Confirm row mappings with each site |
| API surface | Observation split into category-specific APIs | Request every relevant API and scope |
Epic. The open.epic FHIR interface list publishes Observation as separate Read and Search APIs by data type, including Labs, Vital Signs, Social History, Core Characteristics, Assessments, SDOH Assessments, Lines Drains and Airways, SmartData Elements and Genomics. Each specification sets its own search rules:
- Vital Signs search:
patientorsubject, pluscategory=vital-signsor acode. A code sent without a system is treated as LOINC, and Epic flowsheet IDs also work. Beyond the 17 LOINC codes Epic supports by default, it returns other non-duplicable flowsheet data. - Labs search:
patientorsubject, pluscategoryorcodebut not both. Category acceptslaboratory, SNOMED CT lab section codes the site has mapped, or site-specific order types. Values arrive asvalueQuantity,valueString(also used for narrative and impression text),valueRangeorvalueRatio, andreferenceRange.textcarries the display range. For whole panels such as a CBC, Epic points to DiagnosticReport.Search. - Assessments search (August 2024 and later):
category=surveyfor all assessments, orfunctional-status,disability-status,cognitive-status,sdoh,activityorsocial-history. Panels such as PHQ-9 reference their items throughhasMember. From August 2024 the Vital Signs API stopped returning these assessments, which Epic marks as a breaking change.
Epic's search parameter guide splits parameters into native and post-filter types. Post-filters (R4 only, from May 2024) such as combo-code or value-string run after the native search, so their cost grows with how many resources the native parameters match. Epic's error reference on fhir.epic.com lists error 59111 for an R4 Observation search where "the required category parameter is not a valid category option".
Oracle Health. The Millennium Observation search docs state that when category is included, as a parameter or granular scope, the code parameter is ignored. _count defaults to 50 with a maximum of 200, results sort by effective date descending, and a vital-signs search by code with a proprietary system returns an empty response. So category=laboratory&code=2339-0 will not narrow to glucose there.
More vendor behavior in why FHIR compliant does not mean interoperable.
Ingesting vitals and labs from more than one EHR? We build and run Observation pipelines across Epic, Oracle Health, athenahealth, eClinicalWorks and NextGen for product teams. Talk to our team and we will map what each of your target EHRs actually returns for the codes you care about.
Writing Observations: RPM and device data
Reading Observations is broadly supported; writing them is not. Where EHRs accept create, it is limited to specific categories and codes and often depends on site configuration. For remote patient monitoring products, this decides whether readings reach the chart.
US Core's informative Writing Vital Signs page, which carries no certification requirement yet, sets the intended contract. Servers advertise create in the CapabilityStatement and use category-scoped SMART scopes such as patient/Observation.c?category=...|vital-signs. Single create SHALL be supported, batch MAY be. Apps writing patient-supplied data tag it patient-supplied, and device references the measuring device. Servers MAY discard near-duplicate readings but SHALL document it. An illustrative request following that guidance (not captured from a server):
POST [base]/Observation
Content-Type: application/fhir+json
{
"resourceType": "Observation",
"meta": { "tag": [{ "system": "http://hl7.org/fhir/us/core/CodeSystem/us-core-tags", "code": "patient-supplied" }] },
"status": "final",
"category": [{ "coding": [{ "system": "http://terminology.hl7.org/CodeSystem/observation-category", "code": "vital-signs" }] }],
"code": { "coding": [{ "system": "http://loinc.org", "code": "29463-7", "display": "Body weight" }] },
"subject": { "reference": "Patient/example" },
"performer": [{ "reference": "Patient/example" }],
"device": { "reference": "Device/example-scale" },
"effectiveDateTime": "2026-07-30T07:12:00-05:00",
"valueQuantity": { "value": 81.4, "unit": "kg", "system": "http://unitsofmeasure.org", "code": "kg" }
} Epic lists Observation.Create (Vital Signs) (R4) and Observation.Create (Lines, Drains, Airways) (R4). The Vital Signs create specification files one reading per request to a flowsheet row, with specific rules:
categorymust bevital-signs.subject,effectiveDateTimeand onevalue[x]are required, andstatusaccepts onlyfinal.codeis a LOINC code or an Epic flowsheet ID: an encoded ID that the site's IT team generates, or the internal ID, which works only for rows treated as vital signs.- Blood pressure goes in
componentwith 8480-6 and 8462-4. - Clinician and backend apps must reference an existing Encounter by FHIR ID, and cannot write to closed encounters or future appointments. Patient-facing apps write to patient-entered flowsheets, which need an order placed in Epic first, and Epic's remote patient monitoring guide adds that those patients need active MyChart accounts.
- A request fails if a reading already exists at the same timestamp or if it includes an
id.
To file several readings at once, or overwrite a value at the same timestamp, Epic points to its HL7v2 flowsheet data interfaces. Its error reference shows how create fails: 59187 when a patient-facing app writes for a patient with no Patient-Entered Flowsheets assigned, 59188 when "no single flowsheet row can be found by the codes provided", and 59189 when the reading fails to file.
Oracle Health's Observation create docs support VitalSigns, Laboratory and Imaging, one category per request. Vital signs accept only valueQuantity and require a performer, components are limited to blood pressure and pulse oximetry, and a create takes one interpretation and one reference range.
When the target does not accept writes, keep readings in your platform and send clinicians summaries or HL7 v2 results. Our RPM data pipeline from wearable to FHIR Observation covers that architecture.
FHIR Observation search patterns that perform
Always scope Observation searches to a patient and narrow by category, code or date. US Core requires servers to support patient plus category, patient plus code, and patient plus category plus date with gt, lt, ge and le. Unscoped or code-only queries are slow at best and rejected at worst.
# US Core SHALL: vital signs for a patient
GET [base]/Observation?patient=123&category=http://terminology.hl7.org/CodeSystem/observation-category|vital-signs
# US Core SHALL: specific codes (OR list SHOULD be supported)
GET [base]/Observation?patient=123&code=http://loinc.org|8867-4,http://loinc.org|85354-9
# US Core SHALL: category plus a date window
GET [base]/Observation?patient=123&category=laboratory&date=ge2026-06-01&date=lt2026-07-01
# Base R4, where supported: latest reading
GET [base]/Observation?patient=123&code=http://loinc.org|29463-7&_sort=-date&_count=1 The SHOULD set on the US Core lab and vital signs profiles adds patient plus category plus status, patient plus category plus _lastUpdated, and patient plus code plus date. Beyond that it is server-specific: value-quantity, combo-code and _sort are valid R4 but not universal, and while R4 defines $lastn, open.epic lists it only for STU3 APIs. Check the server first with our guide to reading a FHIR CapabilityStatement.
Use system|code tokens so a local code matching a LOINC string is not returned, and follow the Bundle next link instead of building page URLs, as the FHIR Bundle resource guide explains. More mistakes in 10 FHIR search patterns developers get wrong.
Where teams get stuck with FHIR Observation data
Observation failures rarely show up in a sandbox. They appear once real sites send real data, when they cost the most to fix. These consume the most engineering time on integrations we build:
- Assuming every value is a Quantity. A lab arrives as
valueStringand is dropped without an error. Finding silently missing results after launch means auditing every code path. - Searching component codes. A dashboard queries systolic 8480-6 and shows nothing. Teams chase permissions for days before finding the code versus component.code rule.
- Ignoring status transitions. Corrected results never replace originals and entered-in-error readings stay visible. That is a safety issue, not a cosmetic one.
- Charting on the wrong timestamp. Using
issuedorlastUpdatedinstead ofeffective[x]shifts trend lines, especially for backfilled RPM data. - Trusting that LOINC is present. Rows with only local codes vanish from analytics and nobody notices.
- Duplicate device readings. Device cloud retries create duplicates, or the EHR discards them under its own rules. Idempotency keys and a dedupe window are needed before go-live.
- Assuming write works because read works. Create needs its own scopes, code mappings and often site flowsheet build, which means another round of coordination with every customer's analysts.
Many Observations carry no encounter, which breaks visit-based joins; the FHIR Encounter resource guide covers when that link is reliable. For profile expectations, read our US Core implementation guide.
FHIR Observation implementation checklist
Run through this before calling an Observation integration production-ready. Every item is cheaper to test in week one of EHR integration work than to fix after launch.
- Parse every value[x] type the target can send; log unknown types instead of dropping them.
- Select codings by system plus code and store all codings, including local ones.
- Normalize units on the UCUM code and keep the original value.
- Model blood pressure as 85354-9 with components; search the panel code or combo-code.
- Handle every status, upserting by resource id.
- Use effective[x] for clinical time and issued or _lastUpdated for sync.
- Store reference range text and interpretation flags exactly as sent.
- Assemble panels by following hasMember and derivedFrom.
- Build queries from US Core mandatory combinations, then apply each vendor's search rules.
- Validate against obs-3, obs-6, obs-7 and the relevant US Core profile.
- For writes, confirm create in the CapabilityStatement, request category scopes, tag patient-supplied data and document your dedupe window.
Need Observation data from Epic, Oracle Health or athenahealth flowing into your product without per-site surprises? Our Healthcare Interoperability Solutions team builds and runs these pipelines, and our open-source FHIR server passes the ONC (g)(10) Inferno SMART App Launch suite, 47 of 47. Building the RPM or monitoring product itself? Our Custom Healthcare Software Development team builds that side too. Talk to our team to scope your integration.



