Overview
Alongside the aggregate
data covered by get_analytics(), DHIS2’s
Tracker stores case-based, person-level data — the kind
of record you’d use for individual patients or clients rather than
facility-level totals. khisr provides functions for reading
it:
| khisr function | DHIS2 API Endpoint | Retrieves |
|---|---|---|
get_tracked_entities() |
tracker/trackedEntities |
The people/subjects being tracked |
get_enrollments() |
tracker/enrollments |
Their enrollment in a program |
get_events() |
tracker/events |
What happened at each program stage visit |
get_relationships() |
tracker/relationships |
Links between two tracker objects (e.g. an index case and a contact) |
get_event_analytics() |
analytics/events/query |
Aggregated/dimensional analytics over events |
get_enrollment_analytics() |
analytics/enrollments/query |
Aggregated/dimensional analytics over enrollments |
These map onto five core Tracker concepts:
| Concept | Description |
|---|---|
| Tracked Entity | The subject being followed over time (e.g. a patient or
client). Identified by a trackedEntityType such as
“Person”. |
| Program | The case-based program a tracked entity is enrolled in (e.g. a child health programme). |
| Enrollment | A tracked entity’s participation in a specific program, with its own enrollment and incident dates. |
| Event | A single visit/data-collection point within an
enrollment, tied to one programStage (e.g. a birth visit)
and carrying dataValues. |
| Relationship | A typed link between two tracker objects, e.g. a TB index case and a household contact. |
All of these functions are and, except for the two analytics-query functions, share the same scoping, pagination, and error-handling behaviour.
Every query below needs real program, program stage, and org unit ids, which are metadata rather than data — look them up with the matching [metadata helpers][metadata-helpers]:
get_programs()
#> # A tibble: 29 × 2
#> name id
#> <chr> <chr>
#> 1 AFI - Acute Febrile Illness w0qPtIW0JYu
#> 2 Adverse events following immunization (AEFI) EZkN8vYZwjR
#> 3 Animal Health PREnRHSp3be
#> 4 Anti-Tuberculosis Drug Resistance Survey (DRS) KYzHf1Ta6C4
#> 5 Cancer Registry AbPGPdlFWqc
#> # ℹ 24 more rows
get_program_stages()
#> # A tibble: 103 × 2
#> name id
#> <chr> <chr>
#> 1 AEFI so8YZ9J3MeO
#> 2 Acceptance INAofuHURWX
#> 3 Admission pigybbQRpbs
#> 4 Animal exposure JsiWrweuQh5
#> 5 Availability, trainings and medical clearance - RRT Manager HSngsKadeW4
#> # ℹ 98 more rows
get_tracked_entity_types()
#> # A tibble: 6 × 2
#> name id
#> <chr> <chr>
#> 1 ENTO- Larval habitat qIKI1C8qgbn
#> 2 Focus area We9I19a3vO1
#> 3 Health Event DnxQe1mgmlp
#> 4 Health Facility xzck98PlU28
#> 5 Malaria Case IjGPYyk56jz
#> # ℹ 1 more row
get_tracked_entity_attributes()
#> # A tibble: 197 × 2
#> name id
#> <chr> <chr>
#> 1 AEFI Case ID h5FuguPFF2j
#> 2 AFI - Alternate phone number yIUuFclti50
#> 3 AFI - Highest education received HysEcGdXB1w
#> 4 AFI - Marital status bDm1t4BanCm
#> 5 AFI - Occupation dMyRrx6lb7H
#> # ℹ 192 more rowsScoping a query
Every Tracker query must be scoped — DHIS2 rejects an unscoped
request. Scope by program, and/or by an org unit together
with org_unit_mode, which controls how the org unit is
interpreted. get_tracked_entities() and
get_enrollments() take org_units — one or more
org unit ids — while get_events() takes a single
org_unit — confirmed live against a DHIS2 instance that the
Tracker API’s events endpoint only accepts one org unit,
unlike the other two:
org_unit_mode |
Meaning |
|---|---|
SELECTED |
Only the given org unit(s) (DHIS2’s default when one is set) |
CHILDREN |
The given org unit(s) and their immediate children |
DESCENDANTS |
The given org unit(s) and all units below them in the hierarchy |
ACCESSIBLE |
Everything the authenticated user has access to (default when no org unit is given) |
CAPTURE |
Everything the authenticated user can capture data into |
ALL |
Every org unit in the instance (typically requires elevated authority) |
# All tracked entities enrolled in a program, at an org unit and everything below it
get_tracked_entities(
program = 'PREnRHSp3be',
org_units = 'IWp9dQGM0bS',
org_unit_mode = 'DESCENDANTS'
)
#> # A tibble: 270 × 6
#> trackedEntity trackedEntityType createdAt updatedAt orgUnit inactive
#> <chr> <chr> <chr> <chr> <chr> <lgl>
#> 1 qkU5JI6SQcd DnxQe1mgmlp 2024-06-06T10:40:2… 2024-06-… NRcrkS… FALSE
#> 2 ytRUQrTYLFz DnxQe1mgmlp 2024-06-06T10:40:2… 2024-06-… QoGegg… FALSE
#> 3 xIOlpNNRcNY DnxQe1mgmlp 2024-06-06T10:40:2… 2024-06-… o0Q54F… FALSE
#> 4 MNUPROje7ZP DnxQe1mgmlp 2024-06-06T10:40:2… 2024-06-… wQUe9H… FALSE
#> 5 rlTI0qJF8fl DnxQe1mgmlp 2024-06-06T10:40:2… 2024-06-… mNaSC8… FALSE
#> # ℹ 265 more rowsRetrieving tracked entities
get_tracked_entities() returns one row per tracked
entity. Repeating structures such as attributes and
enrollments come back as list-columns — use
[tidyr::unnest_wider()] or [tidyr::unnest_longer()] to flatten the parts
you need.
entities <- get_tracked_entities(
program = 'PREnRHSp3be',
org_units = 'IWp9dQGM0bS',
org_unit_mode = 'DESCENDANTS',
fields = c('trackedEntity', 'orgUnit', 'attributes')
)
entities
#> # A tibble: 270 × 3
#> trackedEntity orgUnit attributes
#> <chr> <chr> <list>
#> 1 qkU5JI6SQcd NRcrkSgDX5G <list [3]>
#> 2 ytRUQrTYLFz QoGeggHGC30 <list [3]>
#> 3 xIOlpNNRcNY o0Q54FVyIzn <list [3]>
#> 4 MNUPROje7ZP wQUe9Hl9pDP <list [3]>
#> 5 rlTI0qJF8fl mNaSC8nPj12 <list [3]>
#> # ℹ 265 more rowsFiltering by attribute value
get_tracked_entities() also accepts one or more
[tracked_entity_filter()] attribute filters, or their infix-operator
shorthand. This is a separate, smaller operator set from
[metadata_filter()] — it applies only to the
trackedEntities endpoint, and the in operator
joins values with semicolons rather than a comma-separated bracket list.
metadata_filter()’s operators (%.eq%,
%.in%, etc.) are not interchangeable with these —
passing one to get_tracked_entities() raises an error
rather than sending malformed filter syntax to the server.
| Operator | Infix operator | Meaning |
|---|---|---|
eq |
%.teq% |
Equality |
ne |
%.tne% |
Inequality |
ge |
%.tge% |
Greater than or equal |
gt |
%.tgt% |
Greater than |
le |
%.tle% |
Less than or equal |
lt |
%.tlt% |
Less than |
like |
%.tlike% |
Match anywhere |
sw |
%.tsw% |
Starts with |
ew |
%.tew% |
Ends with |
in |
%.tin% |
Match one or more values |
null |
(none — pass NULL to
tracked_entity_filter()) |
Attribute has no value |
!null |
(none — pass NULL to
tracked_entity_filter()) |
Attribute has a value |
# Tracked entities whose attribute mTYYajEhlPY starts with "Ann"
get_tracked_entities(
program = 'PREnRHSp3be',
org_units = 'IWp9dQGM0bS',
org_unit_mode = 'DESCENDANTS',
tracked_entity_filter('mTYYajEhlPY', 'sw', 'John')
)
#> # A tibble: 2 × 6
#> trackedEntity trackedEntityType createdAt updatedAt orgUnit inactive
#> <chr> <chr> <chr> <chr> <chr> <lgl>
#> 1 qkU5JI6SQcd DnxQe1mgmlp 2024-06-06T10:40:2… 2024-06-… NRcrkS… FALSE
#> 2 y8m3PCtGLUu DnxQe1mgmlp 2024-06-06T10:38:4… 2024-06-… JziqEv… FALSE
# Equivalent, using the infix operator
get_tracked_entities(
program = 'PREnRHSp3be',
org_units = 'IWp9dQGM0bS',
org_unit_mode = 'DESCENDANTS',
mTYYajEhlPY %.tsw% 'John'
)
#> # A tibble: 2 × 6
#> trackedEntity trackedEntityType createdAt updatedAt orgUnit inactive
#> <chr> <chr> <chr> <chr> <chr> <lgl>
#> 1 qkU5JI6SQcd DnxQe1mgmlp 2024-06-06T10:40:2… 2024-06-… NRcrkS… FALSE
#> 2 y8m3PCtGLUu DnxQe1mgmlp 2024-06-06T10:38:4… 2024-06-… JziqEv… FALSERetrieving events
get_events() returns one row per event, with the data
collected at that visit in a dataValues list-column.
get_events(
program = 'PREnRHSp3be',
org_unit = 'NRcrkSgDX5G',
org_unit_mode = 'DESCENDANTS'
)
#> # A tibble: 4 × 8
#> event status program programStage orgUnit occurredAt scheduledAt dataValues
#> <chr> <chr> <chr> <chr> <chr> <chr> <chr> <list>
#> 1 viu095e… COMPL… PREnRH… mj1stImcUCi NRcrkS… 2026-05-2… 2027-06-06… <list [2]>
#> 2 fSOGbgx… COMPL… PREnRH… AIMTIOnbpFH NRcrkS… 2026-06-1… 2027-06-06… <list [8]>
#> 3 KMAQJpO… COMPL… PREnRH… Lp3XW1E8MzH NRcrkS… 2026-05-1… 2027-06-06… <list>
#> 4 BTIQsyl… COMPL… PREnRH… Mh2FDNOHqBO NRcrkS… 2026-05-1… 2027-06-06… <list [5]>DHIS2’s Tracker API does not currently document a way to filter
events by data element value the way trackedEntities
supports attribute filtering, so get_events() has no
filter argument. If your instance supports this via an
undocumented parameter, pass it through ...; otherwise
retrieve the broader event set and filter client-side after unnesting
dataValues.
Retrieving enrollments
get_enrollments() returns one row per enrollment, useful
for questions about who is enrolled and when, independent of any
particular visit.
get_enrollments(
program = 'PREnRHSp3be',
org_units = 'IWp9dQGM0bS',
org_unit_mode = 'DESCENDANTS'
)
#> # A tibble: 270 × 7
#> enrollment trackedEntity program status orgUnit enrolledAt occurredAt
#> <chr> <chr> <chr> <chr> <chr> <chr> <chr>
#> 1 VszQ0IsJ2xP qkU5JI6SQcd PREnRHSp3be ACTIVE NRcrkSgDX5G 2026-05-1… 2026-06-0…
#> 2 M8y3vuRdb6G ytRUQrTYLFz PREnRHSp3be ACTIVE QoGeggHGC30 2026-03-0… 2026-06-0…
#> 3 PBojRJ4RrlW xIOlpNNRcNY PREnRHSp3be ACTIVE o0Q54FVyIzn 2026-06-2… 2026-06-0…
#> 4 mx2PqVG8Thp MNUPROje7ZP PREnRHSp3be ACTIVE wQUe9Hl9pDP 2026-10-1… 2026-06-0…
#> 5 km3HmjIcA6P rlTI0qJF8fl PREnRHSp3be ACTIVE mNaSC8nPj12 2026-05-0… 2026-06-0…
#> # ℹ 265 more rowsRetrieving relationships
get_relationships() returns links between two tracker
objects — for example a TB index case and a household contact. It’s
scoped differently from the other three functions: provide exactly one
of tracked_entity, enrollment, or
event to retrieve that object’s relationships.
get_relationships(tracked_entity = 'qkU5JI6SQcd')
#> Warning: ! No data found for the specified query.
#> NULLAggregated Tracker analytics
For counts and dimensional breakdowns over Tracker data (e.g. events
by org unit and period) rather than the underlying individual records,
get_event_analytics() and
get_enrollment_analytics() query DHIS2’s Tracker analytics
engine — the same %.d%/%.f% dimension
operators used by [get_analytics()] apply here too, and
program is required as the first argument (used as a URL
path segment, not a dimension):
get_event_analytics(
program = 'PREnRHSp3be',
ou %.d% 'USER_ORGUNIT',
pe %.d% 'LAST_12_MONTHS'
)
#> # A tibble: 352 × 21
#> psi ps eventdate storedby createdbydisplayname lastupdatedbydisplay…¹
#> <chr> <chr> <chr> <chr> <chr> <chr>
#> 1 k9fsx4ww… Mh2F… 2026-07-… stefano Perotti, Stefano (s… Perotti, Stefano (ste…
#> 2 w8JY1dXn… Mh2F… 2026-02-… stefano Perotti, Stefano (s… Perotti, Stefano (ste…
#> 3 uQLiWvfe… Mh2F… 2026-05-… stefano Perotti, Stefano (s… Perotti, Stefano (ste…
#> 4 NizUCOPY… Mh2F… 2026-08-… stefano Perotti, Stefano (s… Perotti, Stefano (ste…
#> 5 FH0lQpMF… Mh2F… 2026-06-… stefano Perotti, Stefano (s… Perotti, Stefano (ste…
#> # ℹ 347 more rows
#> # ℹ abbreviated name: ¹lastupdatedbydisplayname
#> # ℹ 15 more variables: lastupdated <chr>, scheduleddate <chr>,
#> # enrollmentdate <chr>, incidentdate <chr>, tei <chr>, pi <chr>,
#> # geometry <chr>, longitude <dbl>, latitude <dbl>, ouname <chr>,
#> # ounamehierarchy <chr>, oucode <chr>, programstatus <chr>,
#> # eventstatus <chr>, ou <chr>Unlike every other paginated function in this package, this
endpoint’s pager lives in a nested metaData.pager field,
and — confirmed live against a public DHIS2 demo instance — silently
returns only the first 50 rows with no error or warning if you query the
underlying DHIS2 endpoint directly without paging through it.
get_event_analytics()/get_enrollment_analytics()
handle this automatically.
Date filters and pagination
All three functions accept
updated_after/updated_before, and
get_tracked_entities()/get_enrollments()
additionally accept
enrolled_after/enrolled_before and
occurred_after/occurred_before to bound
results by enrollment or incident date. get_events()
accepts occurred_after/occurred_before for the
event’s own occurred date. The underlying DHIS2 query parameter names on
get_events()/get_enrollments() were confirmed
by testing live against a public DHIS2 demo instance —
enrolled_after/occurred_after measurably
narrowed the result set on both endpoints, and
get_events()‘s single-value org_unit
requirement (unlike the other two functions’ org_units) was
confirmed the same way: passing more than one org unit is rejected by
the server outright.
Results are paginated automatically — page_size (default
500) controls how many records are requested per page, not the total
returned; all matching pages are fetched and combined into a single
tibble.
Further resources
For the full set of query parameters your DHIS2 instance supports, see the DHIS2 Tracker API documentation.
