FHIRPath
FHIRPath is a path-based navigation and extraction language for hierarchical data models, used extensively in FHIR and CQL. All operations return collections.
Path Navigation
Patient.name.given // Navigate to given names Patient.name[0].given // First name's given (0-based indexing) Patient.name.first().given // Equivalent using first() Observation.value.ofType(Quantity).unit // Filter by type then navigate
Prefix with type name to restrict context: Patient.name.given only works on Patient resources.
Literals
| Type | Examples |
|---|---|
| Boolean | true, false |
| Integer | 42, -5 |
| Decimal | 3.14159 |
| String | 'hello', 'urn:oid:1.2.3' (single quotes, use \' to escape) |
| Date | @2024-01-15, @2024-01, @2024 (partial dates allowed) |
| DateTime | @2024-01-15T14:30:00Z, @2024-01-15T14:30:00+10:00, @2024T |
| Time | @T14:30:00, @T14:30 |
| Quantity | 10 'mg', 4 days, 100 '[degF]' (UCUM units in quotes) |
Collections
- •All values are collections (even single items)
- •Empty collection:
{ } - •Collections are ordered, non-unique, 0-indexed
- •Empty propagates through most operations
Key Functions
Existence
name.exists() // true if any names name.exists(use = 'official') // true if any official names name.all(given.exists()) // true if all names have given telecom.where(system='phone').empty() // true if no phone telecoms name.count() // number of names
Filtering and Projection
name.where(use = 'official') // Filter by criteria entry.select(resource as Patient) // Project/transform name.select(given.first() + ' ' + family) // Combine fields telecom.distinct() // Remove duplicates item.repeat(item) // Recursive traversal (unique) expansion.repeat(contains) // Navigate tree structures
Subsetting
name[0] // First item (0-based) name.first() // First item name.last() // Last item name.tail() // All but first name.skip(2) // Skip first 2 name.take(3) // Take first 3 name.single() // Error if not exactly one
Combining
name.given | name.family // Union (removes duplicates) a.union(b) // Same as | a.combine(b) // Merge without removing duplicates a.intersect(b) // Items in both a.exclude(b) // Items in a but not b
Type Operations
value is Quantity // Type check (returns Boolean) value as Quantity // Cast (empty if wrong type) entry.resource.ofType(Patient) // Filter collection by type
String Functions
'hello'.length() // 5
'hello'.substring(1, 3) // 'ell'
'hello'.startsWith('he') // true
'hello'.endsWith('lo') // true
'hello'.contains('ell') // true (different from contains operator)
'hello'.indexOf('l') // 2
'HELLO'.lower() // 'hello'
'hello'.upper() // 'HELLO'
'a-b-c'.replace('-', '_') // 'a_b_c'
'abc'.matches('[a-z]+') // true (partial match)
'abc'.matchesFull('[a-z]+') // true (full match)
'a,b,c'.split(',') // {'a', 'b', 'c'}
('a' | 'b' | 'c').join(',') // 'a,b,c'
' hello '.trim() // 'hello'
Math Functions (STU)
(-5).abs() // 5 1.5.ceiling() // 2 1.5.floor() // 1 1.5.round() // 2 1.5.truncate() // 1 2.power(3) // 8.0 9.sqrt() // 3.0 2.718.ln() // ~1.0 100.log(10) // 2.0 0.exp() // 1.0
Conversion
'42'.toInteger() // 42 42.toString() // '42' '3.14'.toDecimal() // 3.14 'true'.toBoolean() // true (also 't', 'yes', 'y', '1') '2024-01-15'.toDate() // @2024-01-15 '10:30:00'.toTime() // @T10:30:00 '5 mg'.toQuantity() // 5 'mg' value.convertsToInteger() // true if conversion would succeed
Date/Time Component Extraction (STU)
@2024-03-15.yearOf() // 2024 @2024-03-15.monthOf() // 3 @2024-03-15.dayOf() // 15 @2024-03-15T10:30:45.123.hourOf() // 10 @2024-03-15T10:30:45.123.minuteOf() // 30 @2024-03-15T10:30:45.123.secondOf() // 45 @2024-03-15T10:30:45.123.millisecondOf() // 123 @2024-03-15T10:30:00+10:00.timezoneOffsetOf() // 10.0 @2024-03-15T10:30:00.dateOf() // @2024-03-15 @2024-03-15T10:30:00.timeOf() // @T10:30:00
Utility
now() // Current DateTime
today() // Current Date
timeOfDay() // Current Time
iif(condition, true-result, else) // Conditional (short-circuit)
coalesce(a, b, c) // First non-empty (STU)
trace('label', projection) // Debug logging
defineVariable('name', expr) // Define variable for later use (STU)
Aggregates (STU)
(1 | 2 | 3).sum() // 6 (1 | 2 | 3).min() // 1 (1 | 2 | 3).max() // 3 (1 | 2 | 3).avg() // 2.0 values.aggregate($this + $total, 0) // Custom aggregation
Operators
Equality
a = b // Equal (empty if either empty) a ~ b // Equivalent (false if precisions differ, case-insensitive for strings) a != b // Not equal a !~ b // Not equivalent
Comparison
a > b // Greater than a < b // Less than a >= b // Greater or equal a <= b // Less or equal
Comparison with different date/time precisions returns empty: @2024-01 > @2024-01-15 → { }
Boolean
a and b // Three-valued AND a or b // Three-valued OR a xor b // Exclusive OR a implies b // Implication not() // Negation (function, not operator)
Three-valued logic: true and { } → { }, false and { } → false
Math
5 + 3 // Addition (also string concatenation) 5 - 3 // Subtraction 5 * 3 // Multiplication 5 / 3 // Division (always Decimal) 5 div 3 // Integer division 5 mod 3 // Modulo 'a' & 'b' // String concat (empty → empty string)
Collections
item in collection // Membership collection contains item // Containership (converse of in) a | b // Union
Precedence (high to low)
- •
.(path),[](indexer) - •Unary
+,- - •
*,/,div,mod - •
+,-,& - •
is,as - •
| - •
>,<,>=,<= - •
=,~,!=,!~ - •
in,contains - •
and - •
xor,or - •
implies
Date/Time Arithmetic
Add/subtract time-valued quantities from dates/times:
@2024-01-15 + 1 month // @2024-02-15 @2024-01-31 + 1 month // @2024-02-29 (last day of month) @2024-03-15 - 10 days // @2024-03-05 now() - 1 year // One year ago
Calendar duration keywords: year(s), month(s), week(s), day(s), hour(s), minute(s), second(s), millisecond(s)
UCUM definite durations ('a', 'mo', 'd') are NOT equivalent for arithmetic above seconds.
Environment Variables
%ucum // UCUM URL (http://unitsofmeasure.org) %context // Original evaluation context %`custom` // Custom variables (backtick-delimited)
Singleton Evaluation
When a function expects a single item but receives a collection:
- •Single item → use it
- •Single item convertible to Boolean → use as Boolean
- •Empty → return empty
- •Multiple items → ERROR
Common Patterns
Safe navigation with existence checks
name.where(use = 'official').exists() implies name.where(use = 'official').given.exists()
Conditional logic
iif(gender = 'male', 'M', iif(gender = 'female', 'F', 'U'))
Fallback values
coalesce(name.where(use='official'), name.where(use='usual'), name.first())
Reference resolution (FHIR-specific)
subject.resolve().ofType(Patient).birthDate generalPractitioner.all(resolve() is Practitioner)
Working with CodeableConcept
code.coding.where(system = 'http://snomed.info/sct').code code.coding.exists(system = 'http://loinc.org' and code = '12345-6')
Recursive navigation
Questionnaire.repeat(item) // All nested items ValueSet.expansion.repeat(contains) // All expansion concepts
Type-safe polymorphic access
Observation.value.ofType(Quantity).value > 100 (Observation.value as Quantity).value > 100
FHIR Invariants
FHIRPath is used to define constraints in FHIR profiles:
// Name must have family or given name.family.exists() or name.given.exists() // If dosage exists, it must have timing or asNeeded dosage.exists() implies (dosage.timing.exists() or dosage.asNeeded.exists()) // All references must be resolvable as Practitioner performer.all(resolve() is Practitioner)
Pitfalls
- •Multiple items in singleton context:
Patient.name.given + ' ' + Patient.name.familyfails if multiple names - •Equality with empty:
{ } = { }returns{ }, nottrue - •Date precision:
@2024 = @2024-01returns{ }(unknown) - •String contains vs collection contains:
.contains('x')is string function;contains xis operator - •Case sensitivity: FHIRPath keywords are case-sensitive; type names depend on model
- •Indexing: Collections are 0-based (first element is
[0])