Capturing Vital Signs
Vital signs are a group of important medical signs that measure the body's most vital (life-sustaining) functions, including blood pressure, pulse rate, respiration rate, and body temperature. Capturing these signs is a common part of many procedures, so it is important to ensure that they are being recorded and stored accurately.
The Observation Resource
Vital signs are stored as Observation resources. To track the specific vital that is being observed, use the Observation.code element. This describes what was observed, and is sometimes called the observation "name". This should be coded using LOINC codes per US core guidelines.
| Element | Description | Code System | Example |
|---|---|---|---|
status | The status of the result value. Indicates if the observation is preliminary or final, if it has been amended/corrected, or if it has an error or been cancelled. | Observation status types | registered |
code | The type of observation. | LOINC | 8867-4 – Heart Rate |
subject | A reference to who/what the observation is about. | Patient/homer-simpson | |
encounter | A reference to the Encounter or visit during which the observation was made. | Encounter/example-appointment | |
basedOn | A plan or order that that prompted this observation to be made. | CarePlan/example-plan | |
performer | Who performed the observation and is responsible for its accuracy. | Practitioner/dr-alice-smith | |
value[x] | The actual result(s) of the observation. | See below | |
dataAbsentReason | The reason why the result is missing. | Observation data absent reason types | Asked But Unknown |
interpretation | A categorical assessment of the result. For example, high, low, or normal. | Observation interpretation types | Normal |
device | The device used to generate the measurement. This can be medical or non-medical and can range from a simple tongue depressor to an MRI machine. Also includes personal and wearable devices such as smart phones or watches | Device/my-apple-watch | |
specimen | The specimen from which the observation was derived. A sample of material taken from the patient on which laboratory testing or analysis was performed. | Specimen/finger-prick-blood-sample | |
component | Some observations have multiple component observations. These component observations are expressed as separate code value pairs that share the same attributes. An example is systolic and diastolic component observations for blood pressure measurements. | See below |
To record the actual value of a measurement, you can use one of the various value fields on the Observation resource. There are fields for valueQuantity, valueString, valueBoolean and more.
Example: basic reading of body temperature
{
"resourceType": "Observation",
"id": "example-observation-1",
"code": {
"system": "http://loinc.org",
"code": "8310-5",
"display": "Body temperature",
},
"valueQuantity": {
"value": 98.2,
"unit": "degrees Fahrenheit",
"system": "http://unitsofmeasure.org/",
"code": "[degF]",
},
"status": "final",
}
Example: an observation generated from a survey response
{
"resourceType": "Observation",
"id": "example-observation-2",
"code": {
"system": "http://loinc.org",
"code": "29463-7",
"display": "Body weight",
},
"valueQuantity": {
"value": 165,
"unit": "pounds",
"system": "http://unitsofmeasure.org/",
"code": "[lb_av]",
},
"status": "preliminary",
// A representation of how the measurement was made, in this case through a survey
"method": {
"system": "http://example-practice.org/",
"code": "entry-survey",
"display": "entry survey",
},
}
Example: an observation generated by a device
{
"resourceType": "Observation",
"id": "example-observation-3",
"code": {
"system": "http://loinc.org",
"code": "8480-6",
"display": "Systolic blood pressure",
},
"valueQuantity": {
"value": 100,
"unit": "mmhG",
"system": "http://unitsomeasure.org",
"code": "mm[Hg]",
},
"status": "preliminary",
// The device that measured the observation, in this case
"device": {
"resource": {
"resourceType": "Device",
"id": "example-device",
},
},
}
Example: an observation performed by the patient
{
"resourceType": "Observation",
"id": "example-observation-4",
"code": {
"system": "http://loinc.org",
"code": "8867-4",
"display": "Heart rate",
},
"valueQuantity": {
"value": 70,
"unit": "beats per minute",
"system": "http://unitsomeasure.org",
"code": "{Beats}/min",
},
"status": "preliminary",
// This was recorded by the patient, as we can see both the subject and the performer are the patient with the same id
"subject": {
"resource": {
"resourceType": "Patient",
"id": "example-patient",
},
},
"performer": {
"resource": {
"resourceType": "Patient",
"id": "example-patient"
},
},
}
The valueQuantity field is stored as a Quantity type, which contains a value field to represent the numerical value and a unit field, which is a human readable unit that defines what is measured. Whenever possible, the unit should be coded using Unified Codes for Units of Measure (UCUM).
Observation Datatypes
Observation resources can be measured in many different ways. To account for this, the value[x] fields provide multiple ways to account for different datatypes.
value[x] | Description | Datatype | Application | Example |
|---|---|---|---|---|
valueQuantity | Used for numeric measurements with a value and unit. | Quantity | Height | 177 cm |
valueCodeableConcept | Used when the value is represented by a coded concept (e.g. LOINC or SNOMED). | CodeableConcept | HIV test interpretation | 260385009 – Negative |
valueString | Used for text values that do not require a specific coding. | string | Pain level | mild pain |
valueBoolean | Used for binary observations, where the result is either true or false. | boolean | Vaccination status | true |
valueInteger | Used for simple integer values with no units. | number | Age | 28 |
valueRange | Used for observations that have a range as a result. | Range | Body temperature | 98.0 – 98.7 |
valueRatio | Used to represent ratios between two values as a result. | Ratio | Height over body weight | 177 cm / 75 kg |
valueSampledData | Used to represent data that is sampled over a period of time. | SampledData | Glucose measurement | 6 mmol/l |
valueTime | Used to represent the exact time an observation was made, without a date. | string | Time heart rate returned to normal | 15:30:00 |
valueDateTime | Used to represent the exact time and date an observation was made. | string | Birth date and time | 2023-07-24T13:22:00Z |
valuePeriod | Used for observations that have a specific duration or period. | Period | Menstrual cycle duration | 2023-05-12 – 2023-6-09 |
Using valueSampledData for Timeseries Data
The valueSampledData field is recommended for storing dense timeseries observation data from devices such as wearables (e.g., smartwatches, fitness trackers, continuous glucose monitors). This datatype efficiently represents a series of measurements taken at regular intervals within a single Observation resource.
When to Use valueSampledData
Use valueSampledData when you have:
- Multiple measurements taken at regular intervals (e.g., heart rate readings every minute)
- Data from wearable devices or continuous monitoring equipment
- Timeseries data that would otherwise require creating many individual
Observationresources
Medplum is not designed for large-scale wearables data ingestion. While valueSampledData is suitable for moderate-frequency timeseries data (e.g., hourly or daily summaries), applications that need to store high-frequency data from many devices simultaneously (e.g., second-by-second readings from thousands of wearables) should consider using a dedicated data warehouse solution.
For large-scale wearables data:
- Use Medplum for clinical summaries and aggregated observations
- Stream raw high-frequency data to a time-series database or data warehouse
Structure of SampledData
The SampledData datatype includes:
origin: The base value and unit (as aQuantity)period: The number of milliseconds between samplesdimensions: The number of values per sample periodfactor: A scaling factor applied to the datadata: A space-separated string of compressed sample values
Example: Heart rate data from a wearable device
{
"resourceType": "Observation",
"status": "final",
"code": {
"system": "http://loinc.org",
"code": "8867-4",
"display": "Heart rate"
},
"subject": {
"reference": "Patient/example-patient"
},
"device": {
"reference": "Device/apple-watch-123"
},
"effectivePeriod": {
"start": "2024-01-15T08:00:00Z",
"end": "2024-01-15T09:00:00Z"
},
"valueSampledData": {
"origin": {
"value": 60,
"unit": "beats/min",
"system": "http://unitsofmeasure.org",
"code": "{Beats}/min"
},
"period": 60000, // 1 minute in milliseconds
"dimensions": 1,
"factor": 1,
"data": "5 3 2 4 1 2 3 2 1 0 2 1 3 2 4 3 2 1 0 1 2 3 4 5 4 3 2 1 0 1 2 3 4 5 6 5 4 3 2 1 0 1 2 3 4 5 4 3 2 1 0 1 2 3 4 5 4 3 2 1 0"
// Each value represents the difference from the origin (60 bpm)
// So "5" = 65 bpm, "3" = 63 bpm, etc.
}
}
Helper Functions
Medplum provides helper functions in @medplum/core for working with valueSampledData:
expandSampledData()
Expands a SampledData object into an array of numeric values:
import { expandSampledData } from '@medplum/core';
const sampledData = {
origin: { value: 60, unit: 'beats/min' },
period: 60000,
dimensions: 1,
factor: 1,
data: '5 3 2 4 1'
};
const values = expandSampledData(sampledData);
// Returns: [65, 63, 62, 64, 61]
expandSampledObservation()
Converts an Observation with valueSampledData into multiple individual Observation resources, one for each data point:
import { expandSampledObservation } from '@medplum/core';
const observation = {
resourceType: 'Observation',
code: { /* ... */ },
valueSampledData: { /* ... */ },
effectiveDateTime: '2024-01-15T08:00:00Z'
};
const expandedObservations = expandSampledObservation(observation);
// Returns an array of Observation resources, each with a valueQuantity
// and effectiveDateTime calculated from the sampled data
summarizeObservations() and DataSampler
Summarize multiple Observation resources into a single summary Observation with the original data points preserved in component.valueSampledData:
import { summarizeObservations, DataSampler } from '@medplum/core';
// Option 1: Using summarizeObservations helper
const observations = [/* array of Observation resources */];
const summary = summarizeObservations(
observations,
{ text: 'Average Heart Rate' },
(data) => data.reduce((a, b) => a + b, 0) / data.length
);
// Option 2: Using DataSampler class for more control
const sampler = new DataSampler({
code: { text: 'Heart Rate' },
unit: { unit: 'beats/min', code: '{Beats}/min' }
});
sampler.addObservation(obs1);
sampler.addObservation(obs2);
// ... add more observations
const summary = sampler.summarize(
{ text: 'Average Heart Rate' },
(data) => data.reduce((a, b) => a + b, 0) / data.length
);
Multi-component Observations
In some cases it is possible for an Observation to have multiple "sub-observations". The Observation.component field can be used in these cases when there are multiple results that cannot be reasonably interpreted individually.
The component element should only be used when there is one method, one observation, one performer, one device, and one time.
While creating sub-observations can provide powerful functionality, it can be complex to maintain and operationalize. It is recommended to only use the component field when absolutely necessary.
A classic example of a multi-component Observation is systolic and diastolic blood pressure.
Example
{
"resourceType": "Observation",
"id": "example-component-observation",
"code": {
"system": "http://loinc.org",
"code": "85354-9",
"display": "Blood pressure panel with all children optional",
},
"component": [
{
"code": {
"system": "http://loinc.org",
"code": "8480-6",
"display": "Systolic blood pressure",
},
"valueQuantity": {
"value": 100,
"unit": "mmHg",
"system": "http://unitsofmeasure.org/",
"code": "mm[Hg]",
},
},
{
"code": {
"system": "http://loinc.org",
"code": "8462-4",
"display": "Diastolic blood pressure",
},
"valueQuantity": {
"value": 80,
"unit": "mmHg",
"system": "http://unitsofmeasure.org/",
"code": "mm[Hg]",
},
},
],
}
Reference Ranges
The Observation.referenceRange element is a range of values that corresponds to what is considered normal for a specific patient given their age, gender, race, etc.
The set of all possible values for a referenceRange is defined in the ObservationDefinition resource and is usually set up by clinical administrators. For more details, see the Observation Reference Ranges docs.