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,containsandxor,orimplies
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])
Converted and distributed by TomeVault — claim your Tome and manage your conversions.