Skip to content

Identifiers and the D2 extensions

Who this is for: integration developers consuming a generated guide or a served facade, who need to know exactly where the DHIS2 identity of every artifact lives and what each D2* extension carries.

Before you start: read FHIR for DHIS2 people for what an extension and an identifier slice are; have a generated project nearby (quickstart) if you want to read real output alongside.

You will be able to:

  • look up any D2* extension you meet and know what it means in DHIS2 terms
  • resolve any generated artifact or concept back to its DHIS2 UID and code
  • resolve a person, an enrollment, or a form to the DHIS2 object it names
  • rely on the fall-back rules instead of special-casing missing codes

Every URL on this page has two variable halves. {canonical} is the IG's own canonical - every D2* extension is a StructureDefinition under {canonical}/StructureDefinition/<id>. {base} is [generate] identifier_system_base, which every DHIS2 identifier system hangs off. The worked examples below are real bytes from a facade whose canonical is http://localhost:8080/fhir and whose identifier base is http://dhis2.org/fhir.

The FSH names carry a configurable prefix, [generate.naming] prefix, D2 by default; the ids are the same tokens lowercased and hyphenated. A project that sets no prefix still gets D2 on these, because a definition may not shadow a core FHIR name like Period.

The extension registry

Twenty-one extensions, and every one of them says something DHIS2 states that FHIR has no element for. They divide by where they ride.

Extension Id Rides on Value
D2FormType d2-form-type Questionnaire, QuestionnaireResponse code
D2PeriodType d2-period-type Questionnaire code
D2AttributeOptionCombos d2-attribute-option-combos Questionnaire canonical(ValueSet)
D2OrganisationUnitAssignment d2-organisation-unit-assignment Questionnaire Reference(List)
D2DateLabels d2-date-labels Questionnaire complex, 3 slices
D2Repeatable d2-repeatable Questionnaire boolean
D2CollectsIncidentDate d2-collects-incident-date Questionnaire boolean
D2Description d2-description Questionnaire.item string
D2EntityLevel d2-entity-level Questionnaire.item boolean
D2Period d2-period QuestionnaireResponse, MeasureReport complex, 3 slices
D2AttributeOptionCombo d2-attribute-option-combo QuestionnaireResponse Coding
D2OrganisationUnit d2-organisation-unit QuestionnaireResponse Reference(D2Location)
D2TrackerEnrollment d2-tracker-enrollment QuestionnaireResponse Identifier
D2EnrolledAt d2-enrolled-at QuestionnaireResponse dateTime
D2IncidentAt d2-incident-at QuestionnaireResponse dateTime
D2SubjectExists d2-subject-exists QuestionnaireResponse boolean
D2OrganisationUnitLevel d2-organisation-unit-level Location Coding
D2AttributeValue d2-attribute-value Organization, Location, CodeSystem, ValueSet, Questionnaire complex, 3 slices
D2TrackedEntityAttributeValue d2-tracked-entity-attribute-value Patient complex, 3 slices
D2OriginalName d2-original-name CodeSystem, ValueSet string
D2OriginalCode d2-original-code CodeSystem, ValueSet string

Each ^context names exactly the resources that carry the extension. A context of bare Element would attach it anywhere, which the IG publisher's QA calls out as an unbounded extension - so no D2* extension has one.

What a form declares

These five ride the Questionnaire and say what a capture client has to know before it builds anything.

D2FormType

{canonical}/StructureDefinition/d2-form-type, a code bound (required) to D2FormType_VS. The kind of DHIS2 object the form was generated from, and the one extension that also rides the response, where it selects which of the five response profiles applies. Five codes, and they are the whole vocabulary:

Code Display Generated from
aggregate Aggregate data set form a DHIS2 data set
event Event program form a DHIS2 event program
tracker Tracker registration form a DHIS2 tracker program
tracker-event Tracker program stage form a DHIS2 program stage
tracked-entity Tracked entity type registration form a DHIS2 tracked entity type
$ curl -s localhost:8389/Questionnaire/PsAncVisit1 | jq -c '.extension[0]'
{"url":"http://localhost:8080/fhir/StructureDefinition/d2-form-type","valueCode":"tracker-event"}

D2PeriodType

{canonical}/StructureDefinition/d2-period-type, a code bound (required) to D2PeriodType_VS. The reporting frequency of the data set the form came from, so a client knows which ISO period format the instance will accept before it builds one. Only an aggregate form declares it - only a data set has a reporting frequency.

$ curl -s localhost:8389/Questionnaire/TuL8IOPzpHh | jq -c '.extension[1]'
{"url":"http://localhost:8080/fhir/StructureDefinition/d2-period-type","valueCode":"Monthly"}

It is the grain above D2Period: the form states the type, the response states one period of that type, and both draw on D2PeriodType_VS.

D2AttributeOptionCombos

{canonical}/StructureDefinition/d2-attribute-option-combos, a valueCanonical naming a ValueSet. The vocabulary of attribute option combinations this form's responses may be keyed under. Plural on purpose: its response-side sibling D2AttributeOptionCombo is singular and carries the one combo a submission chose.

$ curl -s localhost:8389/Questionnaire/TuL8IOPzpHh | jq -c '.extension[2]'
{"url":"http://localhost:8080/fhir/StructureDefinition/d2-attribute-option-combos","valueCanonical":"http://localhost:8080/fhir/ValueSet/d2-aoc-idcDPkDtepR-vs"}

Absent means the default. A data set on DHIS2's default category combo has exactly one attribute option combo, so naming it would be noise; its form carries no such extension and its responses carry no combo.

D2OrganisationUnitAssignment

{canonical}/StructureDefinition/d2-organisation-unit-assignment, a Reference(List). The organisation units the form may be captured against, as the published List of their Locations. A capture client constrains its Location picker by reading it, and a facade validates a submitted unit against it.

$ curl -s localhost:8389/Questionnaire/PrAncCare01 | jq -c '.extension[3]'
{"url":"http://localhost:8080/fhir/StructureDefinition/d2-organisation-unit-assignment","valueReference":{"reference":"List/d2-pr-PrAncCare01-org-units"}}

Absent means every unit the registry publishes. DHIS2 hangs an assignment on a data set and on a programme, never on a tracked entity type, so a person-only form never carries one.

D2DateLabels

{canonical}/StructureDefinition/d2-date-labels, complex, three optional slices - the words this instance puts on the dates the form captures:

Slice Cardinality Type Meaning
enrollmentDate 0..1 string what the instance calls the date a tracker enrollment began
incidentDate 0..1 string what the instance calls the date of the incident the enrollment follows
eventDate 0..1 string what the instance calls the date an event was captured on

Named for the dates rather than for the DHIS2 fields spelling them: enrollmentDateLabel, incidentDateLabel, and executionDateLabel are three DHIS2 form-rendering fields on two different objects, and what the contract states is the words put on three dates.

$ curl -s localhost:8389/Questionnaire/PsAncVisit1 | jq -c '.extension[2]'
{"url":"http://localhost:8080/fhir/StructureDefinition/d2-date-labels","extension":[{"url":"eventDate","valueString":"Date of visit"}]}

A slice is present only where the instance states a label, so a form the instance labelled nothing on carries no extension at all and a client falls back to its own wording. Each label carries its own translations the way any other string does.

D2Repeatable

{canonical}/StructureDefinition/d2-repeatable, a boolean. Whether one enrollment may capture this tracker program stage more than once. Only a tracker-event form declares it, and it declares it either way, so a client never has to guess before offering to add a second visit.

$ curl -s localhost:8389/Questionnaire/PsAncVisit1 | jq -c '.extension[1]'
{"url":"http://localhost:8080/fhir/StructureDefinition/d2-repeatable","valueBoolean":true}

D2CollectsIncidentDate

{canonical}/StructureDefinition/d2-collects-incident-date, a boolean. Whether the tracker program this registration form enrols a person into collects the date of the incident the enrollment follows. Only a tracker form declares it, and it declares it either way.

$ curl -s localhost:8389/Questionnaire/PrAncCare01 | jq -c '.extension[1]'
{"url":"http://localhost:8080/fhir/StructureDefinition/d2-collects-incident-date","valueBoolean":false}

Named for the fact rather than for the DHIS2 field spelling it - displayIncidentDate is a form-rendering flag, and what the contract states is that the program collects the incident date. A registration response carrying D2IncidentAt is answering exactly this declaration.

What a question declares

Two extensions ride Questionnaire.item.

D2Description

{canonical}/StructureDefinition/d2-description, a string. The free text DHIS2 holds about the object a question or a group is asked from - the description of a data element, of a tracked entity attribute, or of a form section. It is guidance for the person filling the form, which is why it rides the item rather than the item's text, and it is present only where the instance states one.

$ curl -s localhost:8389/Questionnaire/PsAncVisit1 | jq -c '.item[0].extension'
[{"url":"http://hl7.org/fhir/StructureDefinition/minValue","valueInteger":1},{"url":"http://localhost:8080/fhir/StructureDefinition/d2-description","valueString":"The number of this visit in the pregnancy, counting from one."}]

D2EntityLevel

{canonical}/StructureDefinition/d2-entity-level, a boolean. Which DHIS2 level a registration question's answer belongs to: true for a tracked entity attribute of the program's tracked entity type, whose answer is written onto the person; false for an attribute only the program asks, whose answer is written onto the enrollment.

$ curl -s localhost:8389/Questionnaire/PrAncCare01 | jq -c '.item[] | {linkId, level: (.extension[]|select(.url|endswith("d2-entity-level"))|.valueBoolean)}'
{"linkId":"TeaNationId","level":true}
{"linkId":"TeaBirthDat","level":true}
{"linkId":"TeaSex00001","level":true}
{"linkId":"TeaHousehld","level":false}
{"linkId":"TeaSystemId","level":true}
{"linkId":"TeaConsent1","level":false}

It rides the item rather than the data dictionary because membership is a fact about the attribute and the tracked entity type together: one dictionary is shared by every form of the run, and two programmes on different types can disagree about one attribute. Every question of a person-only (tracked-entity) form is true by construction - there is no enrollment for an answer to land on. It is also what D2SubjectExists interacts with: an entity-level answer has nowhere to go in an enrollment-only import.

What a response carries

Seven extensions ride the QuestionnaireResponse. Which of them a given response must, may, or must not carry is the capture contract, per form kind; what each one means is here.

D2Period

{canonical}/StructureDefinition/d2-period, complex. DHIS2 reporting periods have no FHIR equivalent: a FHIR Period is a pair of instants, while a DHIS2 period is a typed interval - 202401 is not merely 1-31 January, it is the January instance of the Monthly period type, and the type is what makes it comparable, aggregatable, and round-trippable. So all three facts travel:

Slice Cardinality Type Meaning
iso 1..1 string the DHIS2 ISO period identifier, e.g. 202401
type 1..1 code the period type, bound (required) to D2PeriodType_VS
period 0..1 Period the date range the identifier resolves to
$ curl -s 'localhost:8389/Questionnaire/TuL8IOPzpHh/$generate?seed=7' | jq -c '.extension[0]'
{"extension":[{"url":"iso","valueString":"202607"},{"url":"type","valueCode":"Monthly"},{"url":"period","valuePeriod":{"start":"2026-07-01","end":"2026-07-31"}}],"url":"http://localhost:8080/fhir/StructureDefinition/d2-period"}

Its context is QuestionnaireResponse and MeasureReport - the aggregate capture envelope and the later summary projection.

D2PeriodType_CS publishes every period type DHIS2 registers, each displayed with its ISO format: Daily (yyyyMMdd), Monthly (yyyyMM), FinancialApril (yyyyApril), and so on through the weekly variants, the bi-weekly and bi-monthly types, the November-anchored financial types, and the rest of the twenty-three.

The matching parser lives in dhis2w_fhir.period:

from dhis2w_fhir.period import parse_period

parse_period("2024BiW2")
# PeriodValue(iso='2024BiW2', period_type='BiWeekly',
#             start_date=date(2024, 1, 15), end_date=date(2024, 1, 28))

recent_periods is its inverse, and the example target's way of finding a period worth looking for data in: the most recent periods of a type whose end date is already past, newest first.

import datetime
from dhis2w_fhir.period import recent_periods

recent_periods("Monthly", 3, datetime.date(2026, 8, 2))
# ['202607', '202606', '202605']

It is written as an inverse rather than as a second transcription of the upstream month offsets: each type declares only how its ISO strings are spelled for a given year, and parse_period decides which of those exist and what dates they cover - so the two can never disagree.

D2AttributeOptionCombo

{canonical}/StructureDefinition/d2-attribute-option-combo, a Coding. The one DHIS2 attribute option combo this response's values are keyed under - a project or funder dimension that keys the whole submission. It is drawn from the ValueSet the form's D2AttributeOptionCombos extension names.

$ curl -s 'localhost:8389/Questionnaire/TuL8IOPzpHh/$generate?seed=7' | jq -c '.extension[1]'
{"url":"http://localhost:8080/fhir/StructureDefinition/d2-attribute-option-combo","valueCoding":{"system":"http://localhost:8080/fhir/CodeSystem/d2-aoc-idcDPkDtepR-cs","code":"BqblOcSwGey","display":"Provide access to primary health care"}}

Absent means the default attribute option combo, which DHIS2 fills in itself.

D2OrganisationUnit

{canonical}/StructureDefinition/d2-organisation-unit, a Reference(D2Location). The DHIS2 organisation unit an event was captured at.

An aggregate or event response reports for a place, so its subject is the Location and no such extension is needed. A tracker, tracker-event, or person-only response is about an entity, so its subject is the person and the place moves here.

$ curl -s 'localhost:8389/Questionnaire/PsAncVisit1/$generate?seed=7' | jq -c '.extension[0]'
{"url":"http://localhost:8080/fhir/StructureDefinition/d2-organisation-unit","valueReference":{"reference":"Location/Rp268JB6Ne4"}}

D2TrackerEnrollment

{canonical}/StructureDefinition/d2-tracker-enrollment, an Identifier under {base}/id/tracker-enrollment carrying the DHIS2 enrollment UID. It names the enrollment without inventing a resource for it - whether a DHIS2 enrollment is an EpisodeOfCare or a CarePlan is an open decision, and the identifier contract does not wait on it.

$ curl -s 'localhost:8389/Questionnaire/PsAncVisit1/$generate?seed=7' | jq -c '.extension[1]'
{"url":"http://localhost:8080/fhir/StructureDefinition/d2-tracker-enrollment","valueIdentifier":{"system":"http://dhis2.org/fhir/id/tracker-enrollment","value":"GncfBAepfJB"}}

A registration response mints the UID of the enrollment it creates; a stage response names the enrollment it was captured under.

D2EnrolledAt and D2IncidentAt

{canonical}/StructureDefinition/d2-enrolled-at and {canonical}/StructureDefinition/d2-incident-at, both dateTime. The two moments a DHIS2 enrollment holds: D2EnrolledAt is the enrollment's enrolledAt, the moment it began; D2IncidentAt is its occurredAt, the moment the incident it follows happened.

$ curl -s 'localhost:8389/Questionnaire/PrAncCare01/$generate?seed=4242' | jq -c '.extension[2]'
{"url":"http://localhost:8080/fhir/StructureDefinition/d2-enrolled-at","valueDateTime":"2026-07-20T05:00:00Z"}

Only a registration response carries them. Whether a response carries D2IncidentAt at all is what the form's D2CollectsIncidentDate declares.

D2SubjectExists

{canonical}/StructureDefinition/d2-subject-exists, a boolean. Whether the tracked entity a registration response is subject to is already held by the DHIS2 instance.

  • Absent or false - the default reading and the one every generated example shows: the client minted subject.identifier.value, and the response creates that person along with the enrollment.
  • true - subject.identifier.value is the UID of a person the instance already stores, and the response enrols that person. It is forwarded as a top-level enrollments array, with no tracked entity beside it.

Named for the fact it carries rather than for the capture-client gesture that produced it: a reader of the contract can resolve "the subject exists" without knowing what a client calls the button. The consequences of true - including why an entity-level answer refuses the whole response - are in the capture contract.

What a published resource carries

D2OrganisationUnitLevel

{canonical}/StructureDefinition/d2-organisation-unit-level, a Coding into the published level CodeSystem. The level of the DHIS2 organisation unit hierarchy a place sits at. It rides every Location the registry publishes.

$ curl -s localhost:8389/Location/O6uvpzGd5pu | jq -c '.extension[1]'
{"url":"http://localhost:8080/fhir/StructureDefinition/d2-organisation-unit-level","valueCoding":{"system":"http://localhost:8080/fhir/CodeSystem/d2-ou-level-cs","code":"level-2","display":"District"}}

The display is the instance's own name for that depth, read from /api/organisationUnitLevels, and Level <n> where the instance names no level at that depth. The code stays level-<n> either way.

D2AttributeValue

{canonical}/StructureDefinition/d2-attribute-value, complex. A DHIS2 Attribute is the metadata extensibility point: any object can carry typed key-value pairs under attributeValues, and instances use them for the codes that tie DHIS2 to everything around it - a national registry id on a facility, an external warehouse key, an ICD-10 code on a data element. Those pairs are instance-specific by definition, so no FHIR element holds them.

Slice Cardinality Type Meaning
attributeId 1..1 string the UID of the DHIS2 attribute the value belongs to
attributeCode 0..1 string the attribute's DHIS2 code, absent when the instance left it unset
value 1..1 string the value the object holds, as DHIS2 sends it

Its ^context names five resource types: Organization, Location, CodeSystem, ValueSet, and Questionnaire.

$ curl -s localhost:8389/Questionnaire/BfMAe6Itzgt | jq -c '.extension[2]'
{"extension":[{"url":"attributeId","valueString":"AtrFhirDsQ1"},{"url":"value","valueString":"Paper register 12A, revision 2025-03"}],"url":"http://localhost:8080/fhir/StructureDefinition/d2-attribute-value"}

attributeCode is optional because DHIS2 leaves most attributes uncoded. On the Lao instance eleven of twelve attributes have no code at all. An uncoded attribute gets no attributeCode sub-extension rather than an empty one - an empty code would claim the instance coded that attribute. The example above is one such attribute.

value is a string whatever the attribute declares. DHIS2 sends every attribute value as a string regardless of the attribute's valueType, and one real attribute on that instance carries a whole GeoJSON document that way. The extension takes the wire value as it stands rather than re-typing it.

The code is a join, resolved once per generate run. The wire shape of an attribute value is {"attribute": {"id": "..."}, "value": "..."} - an id and nothing else, with no code, no name, and no value type. So each generate target calls resolve_attribute_code_index, which reads id,code for every attribute off /api/attributes unpaged: DHIS2 answers 50 attributes to a page by default, and an instance defining more than one page of them would otherwise lose the tail of the join silently. Attributes DHIS2 left without a code are absent from the index rather than present with an empty entry, which is what the optional attributeCode reads from.

Where the values land today. Organisation units carry them on both halves of the registry pair, option sets and categories on both the CodeSystem and the ValueSet, and data sets, event programs, and tracker program stages on their Questionnaire. Concept-level attribute values - those on individual data elements and options - are not emitted: a CodeSystem.concept has no carrier chosen for them yet, and that choice is its own decision, sized in fhir roadmap section 9.2.

A value of a unique attribute is not here. It is an identifier instead; see the per-attribute namespaces below.

D2TrackedEntityAttributeValue

{canonical}/StructureDefinition/d2-tracked-entity-attribute-value, complex, the same three slices in the same shape, contexted on Patient alone - the projection of a person the register serves.

A tracked entity attribute value is a DHIS2 answer about a person - a phone number, a household size, a consent flag. It is a family of its own rather than a second context on D2AttributeValue because a tracked entity attribute is a different DHIS2 object from a metadata attribute, and one extension claiming to carry both would publish a definition that is false of half its instances.

{
  "url": "http://localhost:8080/fhir/StructureDefinition/d2-tracked-entity-attribute-value",
  "extension": [
    {"url": "attributeId", "valueString": "ScTeaComPh1"},
    {"url": "value", "valueString": "+23276111001"}
  ]
}

Which attributes are unique - and therefore ride as identifiers instead - is read off the unique concept property D2TEA_CS publishes, not guessed from the value. Values collected at the program are carried alongside the ones collected at the tracked entity type, so a person found by a program attribute's value comes back holding it.

D2OriginalName and D2OriginalCode

{canonical}/StructureDefinition/d2-original-name and {canonical}/StructureDefinition/d2-original-code, both string, both contexted on CodeSystem and ValueSet. The name and the code the DHIS2 instance holds for the object a resource publishes, stated wherever the run published either in rewritten form under hostile_names = "substitute" - a name carrying < published as the words it stands for, a code carrying a space published hyphenated. A resource whose name and code reached the guide byte-true carries neither.

{
  "url": "http://localhost:8080/fhir/StructureDefinition/d2-original-code",
  "valueString": "Distance (in km)"
}

A concept states the same two facts as the dhis2-code and dhis2-name concept properties. An option set's own CodeSystem and ValueSet are one DHIS2 object each and have no concept to hang a property on, so they state them as these extensions - defined here rather than borrowed from the property URIs, because a concept-property URI names a property of a CodeSystem and defines no extension at all. Between them, a consumer reading a rewritten spelling anywhere in the guide has the instance's own spelling one element away.

Core FHIR extensions the generator also uses

Where R4 already publishes an extension for a fact, that one is used rather than a D2* one of this guide's own.

Extension Rides on What it carries
http://hl7.org/fhir/StructureDefinition/minValue Questionnaire.item the inclusive lower bound a DHIS2 value type implies
http://hl7.org/fhir/StructureDefinition/maxValue Questionnaire.item the inclusive upper bound
http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl Questionnaire.item how a group or question is rendered
http://hl7.org/fhir/StructureDefinition/translation _title, _name, _text one locale's rendering of a title, name, or question text
http://hl7.org/fhir/StructureDefinition/location-boundary-geojson Location the organisation unit's DHIS2 geometry, as a base64 GeoJSON attachment

Two of them repay a closer look. The bound extensions are typed to the item - valueInteger on an integer item, valueDecimal on a decimal one - and which DHIS2 value type implies which bound is tabulated in the capture contract. The boundary extension is named for boundaries and R4 documents it as one, but DHIS2 keeps a district's polygon and a facility's pin in the same geometry field, so most of a real registry's attachments are Point rather than Polygon; a reader treating the attachment as polygon-or-failure reports every facility as broken. translation is covered in full under Terminology and ConceptMaps.

Program rules

DHIS2 enforces program rules on import, not only in the Capture app: a tracker payload whose values a SHOWERROR rule refuses comes back E1300 and nothing lands. A form that stated none of that would ask for answers the server rejects, so every rule a published program holds reaches its forms - in one of three tiers, and each rule is in exactly one.

Tier 1 - a numeric refusal becomes a bound. A rule whose single action is SHOWERROR and whose condition compares one question against one number becomes the core minValue / maxValue extensions on that question, on the value[x] its item type takes. The bound is the complement of what the rule refuses: #{hemoglobin} > 99 with SHOWERROR admits up to and including 99, so the question carries maxValue 99. A refusal that is strict at the boundary (>= 99) has no inclusive complement in a decimal, so it bounds a whole-number question one step in (98) and goes to tier 3 on a decimal one. Where the question already carries a bound from its DHIS2 value type - a percentage admits 0..100 - the tighter of the two is published, once.

SHOWWARNING never becomes a bound. DHIS2 lets a warned value through, and a maxValue a server accepts answers past is a constraint nobody enforces. Neither does a refusal whose complement the question's own value type admits nothing of: #{count} >= 0 on a question typed INTEGER_ZERO_OR_POSITIVE would state a maximum of -1 under a minimum of 0, a range no answer is in, so the rule goes to tier 3 whole.

The generated examples answer inside both tiers: an example answers only the questions its form's own enableWhen leaves enabled given the rest of its answers, and every numeric answer it draws falls inside the question's minValue / maxValue.

Tier 2 - a single-question hide becomes enableWhen. A rule whose actions are all HIDEFIELD and whose condition compares one other question against one literal becomes core item.enableWhen entries on each question it hides. DHIS2 hides when its condition holds and R4 shows when its own does, so the operator is negated: a hide when the apgar score is over 7 shows when the score is 7 or less.

Two things keep that inversion faithful. A comparison against the empty string is DHIS2's spelling of "no answer", so it becomes the exists operator rather than an empty answerString - which R4 has no valid form for. And DHIS2 evaluates a rule over a blank question by substituting the value type's empty value, where R4 leaves a question whose enableWhen no answer can satisfy hidden. Where the DHIS2 condition is false of a blank answer - so the question starts out shown - the translation adds the arm that says so, joined by enableBehavior = #any:

* item[=].enableWhen[+].question = "a3kGcGDCuk6"
* item[=].enableWhen[=].operator = #"<="
* item[=].enableWhen[=].answerDecimal = 7
* item[=].enableWhen[+].question = "a3kGcGDCuk6"
* item[=].enableWhen[=].operator = #exists
* item[=].enableWhen[=].answerBoolean = false
* item[=].enableBehavior = #any

A hide on a question answered from an option set compares on answerCoding, and its code is the concept code the bound CodeSystem publishes for the option the rule names - the option UID under concept_code_source = "id", the option's own code under "code", hyphenated where the substitute posture rewrote a code carrying a space. The DHIS2 rule holds the option code the instance stores, so the two are joined through the very concept-code assignment the terminology target emits from. A literal no option of the bound set carries names no concept at all: that rule goes to tier 3 instead of stating a condition no answer can meet, and the run raises a note naming the rule, the question, and the literal.

Tier 3 - everything else is published, non-normatively. Every other rule becomes a repeating D2ProgramRule extension on the Questionnaire:

Sub-extension Type Cardinality Meaning
rule id 1..1 The UID of the DHIS2 program rule
name string 1..1 The name the instance holds it under, with its translations
description string 0..1 The rule's free text, absent when the instance states none
condition string 1..1 The DHIS2 expression the server evaluates, character for character
action code 1..1 What the rule does, from D2ProgramRuleAction_VS

Nothing about tier 3 is normative. It states that the server holds a rule this form cannot express, so a consumer knows an answer the form admits may still be refused - and can show the rule to a person even where it cannot evaluate it. A rule tiers 1 or 2 expressed is never repeated here.

The condition is verbatim, spacing included, because it is the string an administrator searches the instance for. Nothing is prettified.

The grammar is conservative by construction. The parser reads one shape and no other: a single comparison between one #{variable} and one literal, in either order, optionally joined by && to a d2:hasValue guard naming that same variable, with the variable resolved through programRuleVariables to a question the same form asks. Anything else - two variables, an || chain, a d2: function beyond hasValue, a negation, an A{...} attribute reference, a variable reading another program stage - goes to tier 3 whole. So does a rule whose actions this form cannot all state: a hide targeting a question on another stage's form is published rather than half-translated, because a rule half-read publishes a constraint that is neither what DHIS2 enforces nor nothing.

Every form of a program carries its program's rules. A rule belongs to the program rather than to one stage, so a stage form, its siblings, and the registration form beside them all state the same list - a consumer holding one form learns from that form alone which rules the server may refuse its answers under. An aggregate form carries none: DHIS2 states program rules over programs.

d2w fhir forward reads them back. DHIS2 names the rule that refused an import by UID alone, and the guide published that UID beside the rule's name, so the run's rejection roll-up reads Generated by ProgramRule (\Show error for high hemoglobin value`)rather than twelve characters. The UID itself stays untouched on the response's own.report.json`, which is where a reader goes for the machine record.

Only the published rules are nameable that way, which is the set that matters: a client answering a form cannot trip a rule the form already states, so the refusals that reach a reader are the ones tier 3 published. A UID the guide holds no rule for still generalises to `...`, so one cause stays one row.

Identifiers

Every FHIR artifact representing a DHIS2 object exposes both DHIS2 identifiers - the UID and the code - wherever FHIR gives it a slot. This is the standing rule for every generator, present and future.

The base, and what resolves

Every identifier system on this page is {base} plus a path, and {base} is [generate] identifier_system_base, http://dhis2.org/fhir unless the project sets it.

The default is a label, not an address. dhis2.org publishes nothing under /fhir, and DHIS2 defines no FHIR namespace there. A FHIR identifier system is a URI, and a URI need not resolve: what a consumer needs is the definition, and the guide carries it. foundation/d2-naming-systems.fsh declares a NamingSystem for each of the twenty-six fixed systems below, and for the six that the generated ConceptMaps target the guide also publishes a CodeSystem enumerating the identifiers the maps name, so the IG publisher validates a mapped identifier out of the guide instead of asking a terminology server. Those six are the special-url lines of ig/sushi-config.yaml: a canonical outside the guide's own address has to be declared there or the publisher calls it a mismatch.

Set the stem before the first real publish, under a domain the owning organisation controls, so identifiers from this DHIS2 are labelled apart from any other country's. The stem is part of every identifier the guide publishes, so changing it later relabels all of them, and anyone matching records on the old labels stops matching.

Family What it identifies Declared by
{base}/id/<kind> and {base}/id/<kind>-code the DHIS2 UID and DHIS2 code of one object of that kind - the fixed systems a NamingSystem per system, and a CodeSystem for the six the ConceptMaps target
{base}/attribute/<uid> the value of one unique DHIS2 metadata attribute - a unique attribute's values convention; the foundation layer never reads an instance
{base}/tracked-entity-attribute/<uid> the value of one unique tracked entity attribute, which is what names a person convention, as above
{base}/property/<code> a fact about a concept rather than an identifier of one - the concept-property namespace the property declarations of each CodeSystem
  • Instances carry identifier slices discriminated on system: {base}/id/<kind> holds the UID and {base}/id/<kind>-code holds the code. Both slices are always emitted, on the Organization and on the Location alike.
  • Option-set and category concepts carry the complementary identifier as a concept property: in id mode every concept gets dhis2-code, in code mode every concept gets dhis2-id. No option goes without the pair - a DHIS2 option must have a code, so there is always one to carry.
  • Data-dictionary and registry concepts carry dhis2-code only where DHIS2 states a code. A data element, a tracked entity attribute, a category option combo, or an organisation unit may have none, and the concept code is the UID already - repeating it under a dhis2-code label would publish a code the instance does not hold.
  • Option-set and category CodeSystems and ValueSets carry the source set's own pair as identifier business identifiers, under {base}/id/option-set and {base}/id/option-set-code (or {base}/id/category and {base}/id/category-code) - the same URLs the $DHIS2-OS / $DHIS2-OS-CODE aliases name, written out in full because these resources ship as JSON rather than FSH.

The fixed systems, and their NamingSystems

foundation/d2-naming-systems.fsh emits one NamingSystem per identifier system: a UID system for every DHIS2 object kind below, plus a code system for every kind that has a code attribute. Fourteen kinds, twelve of them coded - twenty-six NamingSystems.

Object kind UID system Code system
organisation unit {base}/id/org-unit {base}/id/org-unit-code
option set {base}/id/option-set {base}/id/option-set-code
option {base}/id/option {base}/id/option-code
category {base}/id/category {base}/id/category-code
category option {base}/id/category-option {base}/id/category-option-code
category combo {base}/id/category-combo {base}/id/category-combo-code
data set {base}/id/data-set {base}/id/data-set-code
program {base}/id/program {base}/id/program-code
data element {base}/id/data-element {base}/id/data-element-code
category option combo {base}/id/category-option-combo {base}/id/category-option-combo-code
program stage {base}/id/program-stage {base}/id/program-stage-code
tracked entity type {base}/id/tracked-entity-type {base}/id/tracked-entity-type-code
tracked entity {base}/id/tracked-entity none
tracker enrollment {base}/id/tracker-enrollment none

The last two are data objects rather than metadata: DHIS2 gives them no code attribute, so there is no code system to declare. They are also the two a response carries rather than a definition - subject.identifier and D2TrackerEnrollment respectively.

Each declaration is kind = #identifier with a single preferred uri uniqueId and a description of the convention, the code slot's UID fall-back included. Without them, a validator meeting {base}/id/org-unit has no definition to resolve and warns on every artifact carrying one. Because R4 makes NamingSystem.date mandatory, the declarations carry a pinned date rather than the time of the run - a generated timestamp would rewrite the file every time.

The code slot falls back to the UID. DHIS2 codes are optional, and plenty of instances have units without one. Rather than emit a half-populated identifier, the code slot repeats the UID whenever the DHIS2 code is missing or is not a valid FHIR code. That keeps the profiles conformant (dhis2code is 1..1) and keeps consumers from special-casing absence. It is a "for now" state, owned by the instance team: d2w fhir validate warns on every organisation unit without a code precisely so those fall-backs get replaced with real codes over time.

Which identifiers a Questionnaire carries

A Questionnaire carries the source object's own pair, plus - for the two tracker kinds - a third slice that is the grouping handle.

Form kind Own pair Third slice
aggregate {base}/id/data-set + -code none
event {base}/id/program + -code none
tracker {base}/id/program + -code {base}/id/tracked-entity-type
tracker-event {base}/id/program-stage + -code {base}/id/program
tracked-entity {base}/id/tracked-entity-type + -code none

A tracker program's registration form is the program, so its own pair is the program's. Its third slice names the tracked entity type it enrols a person as - what a client needs to know before it can name the person its response creates:

$ curl -s localhost:8389/Questionnaire/PrAncCare01 | jq -c .identifier
[{"system":"http://dhis2.org/fhir/id/program","value":"PrAncCare01"},{"system":"http://dhis2.org/fhir/id/program-code","value":"PR_ANC"},{"system":"http://dhis2.org/fhir/id/tracked-entity-type","value":"TetPerson01"}]

A stage form's third slice names the program the stage belongs to, which makes a program's whole capture surface one search:

$ curl -s 'localhost:8389/Questionnaire?identifier=http://dhis2.org/fhir/id/program|PrAncCare01' \
    | jq -r '.total, (.entry[].resource.title)'
2
Antenatal care
ANC follow-up - ANC visit

The registration form and every stage of the program come back together, because the registration form's own identity is that same program pair.

A unique attribute's values are identifiers

A DHIS2 attribute value is an arbitrary key-value pair, so it normally rides the D2AttributeValue extension. An attribute DHIS2 declares unique is a different thing: its value names the object rather than annotating it, which is what a FHIR Identifier is for. Those values leave the extension and join the resource's identifier list - after the UID and code slices, so the order stays stable across runs - under a namespace of their own:

{base}/attribute/{attributeUid}

The namespace keys on the attribute UID, not its code: a DHIS2 attribute code may hold spaces, and a system URI may not. Every emitting surface follows the same rule - Organization and Location, an option set's and a category's CodeSystem/ValueSet pair, and a Questionnaire.

A unique tracked entity attribute's values are identifiers too, under a family of their own:

{base}/tracked-entity-attribute/{attributeUid}

A tracked entity attribute is a different DHIS2 object from a metadata attribute - it is a question asked about a person, not an annotation on a metadata object - so it gets its own namespace rather than sharing the one above, and its own extension (D2TrackedEntityAttributeValue) for the values that are not identifiers. The rule for which is which is the same: DHIS2 enforces uniqueness on the attribute, so its value names the person. D2TEA_CS publishes that flag as a unique concept property, which is what a server reads to decide. This is the family GET /Patient?identifier=... searches on.

These per-attribute namespaces are declared by convention rather than as NamingSystems, and deliberately so: the foundation layer is built from fhir.toml alone and never reads an instance, so it cannot know which attributes exist, let alone which are unique. A NamingSystem naming an attribute the instance does not have would be worse than none. What d2-naming-systems.fsh declares is the fixed family above.

The concept-property namespace

Concept properties get URIs of their own, off the same base under a property segment rather than an id one - they name a fact about a concept, not an identifier of one:

{base}/property/{propertyCode}

So dhis2-code, value-type, unique, searchable, generated, pattern, and display-in-list are {base}/property/dhis2-code and so on, and each category a combo decomposes over is {base}/property/category-<stem>.

$ curl -s localhost:8389/CodeSystem/d2-aoc-idcDPkDtepR-cs | jq -c '.property[]'
{"code":"dhis2-code","uri":"http://dhis2.org/fhir/property/dhis2-code","description":"DHIS2 category option combo code.","type":"string"}
{"code":"category-yY2bQYqNt0o","uri":"http://dhis2.org/fhir/property/category-yY2bQYqNt0o","description":"DHIS2 category Project.","type":"Coding"}

The full property set of each vocabulary is in Terminology and ConceptMaps.

See also

Next: Terminology and ConceptMaps