AgentSkillsCN

fhirpath

为编写FHIRPath表达式——一种基于路径的FHIR数据导航与提取语言——提供专业指导。在编写FHIRPath表达式、导航FHIR资源树、筛选集合、进行日期/时间运算、使用FHIRPath函数、编写FHIR不变量,或理解FHIRPath运算符时使用此技能。触发关键词包括“FHIRPath”“fhirpath”“路径表达式”“FHIR导航”“where()”“select()”“exists()”“ofType()”“resolve()”“FHIR不变量”“集合筛选”“FHIRPath函数”。

SKILL.md
--- frontmatter
name: fhirpath
description: Expert guidance for writing FHIRPath expressions - a path-based navigation and extraction language for FHIR data. Use this skill when writing FHIRPath expressions, navigating FHIR resource trees, filtering collections, performing date/time arithmetic, using FHIRPath functions, writing FHIR invariants, or understanding FHIRPath operators. Trigger keywords include "FHIRPath", "fhirpath", "path expression", "FHIR navigation", "where()", "select()", "exists()", "ofType()", "resolve()", "FHIR invariant", "collection filtering", "FHIRPath function".

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

code
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

TypeExamples
Booleantrue, false
Integer42, -5
Decimal3.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
Quantity10 '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

code
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

code
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

code
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

code
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

code
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

code
'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)

code
(-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

code
'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)

code
@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

code
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)

code
(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

code
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

code
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

code
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

code
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

code
item in collection       // Membership
collection contains item // Containership (converse of in)
a | b                    // Union

Precedence (high to low)

  1. . (path), [] (indexer)
  2. Unary +, -
  3. *, /, div, mod
  4. +, -, &
  5. is, as
  6. |
  7. >, <, >=, <=
  8. =, ~, !=, !~
  9. in, contains
  10. and
  11. xor, or
  12. implies

Date/Time Arithmetic

Add/subtract time-valued quantities from dates/times:

code
@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

code
%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

code
name.where(use = 'official').exists() implies name.where(use = 'official').given.exists()

Conditional logic

code
iif(gender = 'male', 'M', iif(gender = 'female', 'F', 'U'))

Fallback values

code
coalesce(name.where(use='official'), name.where(use='usual'), name.first())

Reference resolution (FHIR-specific)

code
subject.resolve().ofType(Patient).birthDate
generalPractitioner.all(resolve() is Practitioner)

Working with CodeableConcept

code
code.coding.where(system = 'http://snomed.info/sct').code
code.coding.exists(system = 'http://loinc.org' and code = '12345-6')

Recursive navigation

code
Questionnaire.repeat(item)            // All nested items
ValueSet.expansion.repeat(contains)   // All expansion concepts

Type-safe polymorphic access

code
Observation.value.ofType(Quantity).value > 100
(Observation.value as Quantity).value > 100

FHIR Invariants

FHIRPath is used to define constraints in FHIR profiles:

code
// 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

  1. Multiple items in singleton context: Patient.name.given + ' ' + Patient.name.family fails if multiple names
  2. Equality with empty: { } = { } returns { }, not true
  3. Date precision: @2024 = @2024-01 returns { } (unknown)
  4. String contains vs collection contains: .contains('x') is string function; contains x is operator
  5. Case sensitivity: FHIRPath keywords are case-sensitive; type names depend on model
  6. Indexing: Collections are 0-based (first element is [0])