How things are generated: [generate] and [generate.naming]¶
Who this is for: the person editing fhir.toml.
Before you start: read The settings file - what
fhir.toml is, where it lives, and how to edit it without breaking it.
Naming choices are best made before the first real publish, so read this page
early.
You will be able to:
- put the guide's identifier labels under your own domain
- choose whether concepts are coded by DHIS2 id or DHIS2 code
- set the reporting time zone and the translation languages
- rename what every generated thing is called, and read the refusal when a name piece breaks the shared rule
This page covers the [generate] section - four options that shape what the
generated content says - and the [generate.naming] section, which decides
what every generated thing is called. Which DHIS2 objects get generated at all
is the next page, what goes in.
The [generate] section¶
identifier_system_base¶
In plain words. Everything the guide publishes carries its DHIS2 id (and
DHIS2 code, where one exists) as a labelled identifier, so a reader can always
trace an entry back to your DHIS2. The label on such an identifier is a web
address, and this option is the stem those addresses are built from - for
example http://dhis2.org/fhir/id/organisation-unit labels "this is a DHIS2
organisation unit id".
When you would change it. When the ministry wants those labels under its
own domain - https://moh.gov.sl/dhis2 instead of the generic
http://dhis2.org/fhir - so that identifiers from this DHIS2 are labelled
distinctly from any other country's. Decide before the first real publish:
anyone matching records on the old labels stops matching when the labels
change.
Example.
An organisation unit's DHIS2 id is now labelled
https://moh.gov.sl/dhis2/id/organisation-unit.
Default: "http://dhis2.org/fhir" - If you leave it out: every
identifier label starts with http://dhis2.org/fhir. That is a label under a
domain that publishes nothing at it, not an address: the guide's own
NamingSystems are the definitions a consumer resolves, so the guide validates
and serves as it should, but the labels say the identifiers belong to a DHIS2
in general rather than to yours. Set the stem before the first real publish;
the base, and what resolves
lists every family built on it.
If you get it wrong: a trailing slash is quietly removed. Beyond that nothing refuses this - you find out when the published identifier labels look wrong to the people consuming them.
The six special-url lines of ig/sushi-config.yaml name the same stem, so
the publisher accepts identifier labels that sit outside the guide's own
address. They follow this option: the next d2w fhir init --refresh (make
update in a scaffolded project) writes all six from what you set here.
What these labelled identifiers look like in the output is covered in Identifiers and the D2 extensions.
concept_code_source¶
In plain words. Every entry the guide's code lists carry has two possible
identities: its DHIS2 id (Qdm5fPK5Ra9 - stable, unique, meaningless to
people) and its DHIS2 code (FEMALE - readable, but only as good as your
instance's code hygiene). This option picks which of the two becomes the
code in the code lists the guide publishes; the other one always rides along
as a property, so no information is lost either way.
What it reaches. Three kinds of published entry, all of them things your
data is coded by: the options of an option set, the category options of a
category, and the category option combinations a disaggregated question's cells are
labelled with. It does not touch how anything is named - that is
naming.source below, a separate decision.
When you would change it. The people receiving your data already know your
DHIS2 codes - their systems are configured against FEMALE / MALE, not
against ids - so you set "code" to publish the familiar values. Stay on
"id" when codes on your instance are missing or inconsistent.
Try it before you commit to it. d2w fhir validate --code-source code
reads your instance and reports exactly what switching would cost - every
option with no code, every code two objects share, every code containing a
character a FHIR code may not hold - without changing a single line of the
guide. Run it, read the report, then decide.
Example.
An option coded FEMALE in DHIS2 is published with FEMALE as its code and
its DHIS2 id as a property. An option whose code is missing or not usable
falls back to its id, with a note in the run's report.
Default: "id" - If you leave it out: published codes are DHIS2 ids,
which always works on any instance. How the published code lists are put
together is covered in
Terminology and ConceptMaps.
If you get it wrong: any value except the two stops the run:
pydantic_core._pydantic_core.ValidationError: 1 validation error for FhirProjectConfig
generate.concept_code_source
Input should be 'id' or 'code' [type=literal_error, input_value='uid', input_type=str]
hostile_names¶
In plain words. What the run does with the two DHIS2 strings a published guide carries badly.
A name carrying < kills the build. The IG publisher writes a name into
pages it re-reads as HTML after writing, and a < opens a tag there, so the
build dies hours in - after every resource has already been rendered. DHIS2 names
carry the character legitimately: an age band is called 5 to < 15 years,
Female.
A code carrying a space builds fine and hurts afterwards. An R4 code admits
single internal spaces, so Pre eclampsia is legal FHIR; the publisher's anchor
slug then strips the whitespace, so Pre eclampsia and Preeclampsia render one
anchor id and its QA pass reports a duplicate (BUGS.md #107). Below the guide,
every URL has to escape the space, every CQL reference has to quote it, and each
terminology server round-trips it at its own discretion.
The two values. "refuse" publishes both byte-true: it writes nothing and
names the object when a name carries <, so the name is changed in DHIS2 or left
out of the selection, and it publishes a space-carrying code exactly as DHIS2
states it - a space is legal, so nothing about it is refused.
"substitute" publishes the name in wording the publisher survives - 5 to under
15 years, Female - and the code with every space hyphenated - Pre-eclampsia -
and notes each rewrite. DHIS2 is never written to either way, and no UID is ever
rewritten.
What a rewritten code preserves. Every published code stays joinable back to
the instance. Each rewritten concept states the DHIS2 code beside it as a
dhis2-code concept property, in the option-set, category, and category-option-
combo vocabularies and in the .../id/*-code identifier CodeSystems alike, and
the ConceptMaps keep taking a published concept to its DHIS2 UID. Two DHIS2 codes
that would land on one published code - Pre eclampsia beside a literal
Pre-eclampsia - are separated by an ordinal suffix: Pre-eclampsia-2. The
capture path reads the dhis2-code property, so a QuestionnaireResponse
answering with a published code still writes the DHIS2 code to DHIS2.
An option set's own CodeSystem and ValueSet have no concept to hang a property
on, so each states the DHIS2 code under the guide's D2OriginalCode extension
and the DHIS2 name under D2OriginalName - two string extensions the
foundation target defines, listed in the extension
registry. A resource
the run published byte-true carries neither.
What a live serve does with it. d2w fhir serve --live reads this key too,
so one project means one set of names whichever way it is served: under
"substitute" a form is served under the name the compiled guide publishes it
under, rewritten codes included. Under "refuse" and unset a live serve is
byte-true and aborts over no name - "refuse" is an answer about what a build
may publish, and serving is not building.
When you would set it. Set "substitute" for a project whose instance names
age bands the way most instances do, so nobody has to answer the same question on
every run. Set "refuse" for a guide whose published names and codes must repeat
the instance byte for byte, and take the rename in DHIS2 when one carries a <.
Example.
Default: unset - If you leave it out: d2w fhir generate shows the names
and codes and their rewrites and asks, when it has a terminal to ask on. Run from
a script or a CI job it asks nobody and rewrites nothing, so an unattended run
behaves as "refuse" does.
One run at a time: d2w fhir generate --substitute-hostile-names and
--refuse-hostile-names answer it for a single run and beat this key. The whole
picture is in Answer the hostile-name
question.
What validate does with it: d2w fhir validate reads this key too, and its
summary says which posture produced the counts. Under "substitute" a name
carrying < is graded informational and the finding names the wording the guide
publishes; under "refuse" and unset it stays an error. A code carrying < is
an error under either posture, because the substitution rewrites a space in a
code and never a <. d2w fhir validate --hostile-names <posture> reads the
instance under the other posture without touching this file - Grade under your
hostile-names posture.
If you get it wrong: any value except the two stops the run:
pydantic_core._pydantic_core.ValidationError: 1 validation error for FhirProjectConfig
generate.hostile_names
Input should be 'refuse' or 'substitute' [type=enum, input_value='rewrite', input_type=str]
timezone¶
In plain words. DHIS2 stores clock times without saying which time zone they were recorded in. Naming your zone here tells the generator "these are local wall-clock times in this zone", so a visit recorded at 09:00 is published as 09:00 in your country's time - daylight saving handled - instead of being read as 09:00 UTC.
When you would change it. Set it once for any project whose published
example responses or captured data carry times, using your country's zone from
the standard IANA list: "Africa/Freetown", "Asia/Vientiane",
"Europe/Oslo".
Example.
A DHIS2 timestamp of 2026-03-01 09:00 is published as
2026-03-01T09:00+07:00.
Default: unset - If you leave it out: times are published exactly as DHIS2 stored them, read as UTC. For a country not on UTC that shifts every published time by the zone difference.
If you get it wrong: a zone name the standard list does not hold stops the run:
pydantic_core._pydantic_core.ValidationError: 1 validation error for FhirProjectConfig
generate.timezone
Value error, unknown IANA time zone 'Asia/Vientane': name a zone from the tz database (e.g. 'Asia/Vientiane', 'Europe/Oslo', 'UTC') [type=value_error, ...]
locales¶
In plain words. Which languages the guide publishes translations in. Your DHIS2 metadata may carry names in several languages; this list picks which of them travel into the guide.
What a translated guide gives whom. A nurse filling in a form on a tablet reads the question, not the data element. A form published with translations carries the local-language wording of every question, every section heading, and every form title beside the English one, so an app rendering the form can show the reader their own language without asking DHIS2 anything. The answer lists are the same story and the bigger win: a choice question offers a list of options, and those options in Lao or Khmer script are what makes the list readable at all. Nothing about the form's identity changes - the codes and the question ids are the same in every language, so a response captured in Lao and one captured in English are the same response.
The person the guide is written for benefits too. A reviewer reading the published data dictionary sees the local-language name beside each entry, which is how they check the guide describes the thing they think it describes.
When you would change it. Your instance holds translations in five
languages but the guide is for a two-language audience: list those two and the
rest stay home. Language tags are the short standard ones - "lo", "en",
"pt-BR" - and DHIS2-style tags like "pt_BR" are understood too.
Example.
Only the Lao and English translations of names are published.
Default: unset - If you leave it out: every translation found on the instance is published.
If you get it wrong: nothing refuses an unknown tag - a language your metadata does not carry simply contributes nothing, and you find out when the expected translations are missing from the guide.
What DHIS2 has to hold for this to show anything. The guide publishes the translations you enter against these DHIS2 properties, and no others:
| DHIS2 translation property | Where it comes out in the guide |
|---|---|
| Name | the title of a form and of a code list; the wording of a section heading; the name of a facility in the registry; the name of every entry in the code lists and the data dictionary |
| Form name | the label on a question, wherever the data element or attribute has a form name |
| Description | the guidance text under a question or a section |
| Enrollment date label, Incident date label, Execution date label | the words a registration or a visit form puts on the dates it asks for |
A Short name translation is not published. An object nobody translated
publishes nothing rather than repeating its English name under a language tag,
so a missing translation is a gap in DHIS2, not a setting here.
Two consequences worth knowing before you look for missing words:
- A program stage form is titled
<program> - <stage>, so its title is translated only in a language that translates both the program and the stage. Translating the stage alone leaves the title in English, because the alternative is to publish a half-translated title nobody wrote. - A question labelled by a DHIS2 form name is translated from that form
name. If the data element has a form name and only its
NAMEis translated, the question keeps its English label - the guide will not put a translation of one piece of text under another.
The [generate.naming] section¶
Everything the guide generates needs a name and a web address:
D2OS_Qdm5fPK5Ra9_CS is the code list of one option set,
d2-os-Qdm5fPK5Ra9-cs its address segment. These names are assembled from
three kinds of pieces, and this section configures two of them:
- a prefix on everything the generator makes (
D2by default), - a kind piece saying what sort of DHIS2 thing it came from (
OSfor option set,OUfor organisation unit,DSfor data set...), - an identity stem naming the individual object - and whether that stem is
the object's DHIS2 id or its DHIS2 code is the
sourceoption, the one decision on this page that deserves real caution.
source¶
Read before you decide - changing this later re-identifies everything
source decides the identity stem inside every generated name, address,
and file name. Change it after the guide has been shared and every one of
those changes at once: every page gets a new web address, every link
anyone saved breaks, and every system matching on the old identities stops
matching. There is no partial version of this - it is a different guide
with the same content. Decide before the first publish, and treat a later
change as republishing from scratch.
In plain words. Whether generated names are built on DHIS2 ids
(D2OS_Qdm5fPK5Ra9_CS) or DHIS2 codes (D2OS_BirthType_CS). Ids always work
- every object has one, they never collide. Codes read beautifully but only
exist where someone maintained them.
When you would change it. Set "code" when your instance's codes are
complete and well kept and you want a guide people can read addresses off -
and you are willing to have the run refuse until every selected object's code
is up to standard. Set "code-or-id" to get code-based names where a good
code exists and id-based names elsewhere - the pragmatic middle while codes
are being cleaned up. Stay on "id" when in doubt.
Example.
An option set coded BIRTH_TYPE is published as D2OS_BirthType_CS; an
uncoded one keeps its id-based name, with a note listing every fall-back.
Default: "id" - If you leave it out: every name is built on DHIS2
ids. Safe on any instance.
If you get it wrong: a value outside the three stops the run:
pydantic_core._pydantic_core.ValidationError: 1 validation error for FhirProjectConfig
generate.naming.source
Input should be 'id', 'code-or-id' or 'code' [type=literal_error, input_value='name', input_type=str]
Under source = "code", a selected object whose code is missing, unusable, or
shared with another object refuses the run with a message shaped like:
error: [generate.naming] source = "code" needs a usable, unique code on every selected option set; 3 cannot serve as identity stems: Birth type (Qdm5fPK5Ra9) has no code; ... Fix the codes in DHIS2, or use source = "code-or-id" while migrating; `d2w fhir validate` names every offender.
The name pieces and their shared rule¶
The remaining nine options are the name pieces themselves. One rule covers all of them:
- a piece is letters and digits only, starting with a letter -
D2,Dhis2,MOH,OrgUnit; - a piece set to
""(empty) is dropped from names entirely - exceptorganisation_unit, which must stay non-empty; - pieces are joined with underscores in display names (
D2+OS+_BirthType+_CS) and with hyphens, lowercased, in web addresses (d2-os-birth-type-cs).
A piece that breaks the rule stops the run with one of:
generate.naming.prefix
Value error, token must be letter-leading alphanumeric (e.g. 'D2', 'Dhis2', 'OU') [type=value_error, input_value='2D', input_type=str]
generate.naming.organisation_unit
Value error, token must not be empty [type=value_error, input_value='', input_type=str]
Like source, these pieces are baked into every name and address: renaming a
piece after publishing renames everything it appears in. Decide early, then
leave them alone.
prefix¶
In plain words. The piece in front of every generated name, marking
"generated from DHIS2 by this project". D2 by default.
When you would change it. A ministry that wants its own stamp on the
guide's names - prefix = "SL" gives SLOS_..._CS, sl-os-.... Or "" to
drop the stamp entirely for shorter names.
Example.
Every generated name starts SL... instead of D2....
Default: "D2" - If you leave it out: names carry the D2 stamp.
If you get it wrong: the shared rule above refuses it before anything is written.
option_set¶
In plain words. The kind piece on names generated from option sets:
D2OS_..._CS is "the code list of an option set".
When you would change it. Preference for longer words
(option_set = "OptionSet") or shorter names ("" drops the piece). Almost
never worth changing.
Example.
Produces D2OptionSet_..._CS instead of D2OS_..._CS.
Default: "OS" - If you leave it out: option-set names carry OS.
If you get it wrong: the shared rule above refuses it.
category¶
In plain words. The kind piece on names generated from DHIS2 categories -
the code lists a category's options are published as: D2CAT_Sex_CS.
When you would change it. Almost never; same preferences as option_set.
Example.
Produces D2Cat_..._CS instead of D2CAT_..._CS.
Default: "CAT" - If you leave it out: category names carry CAT.
If you get it wrong: the shared rule above refuses it.
attribute_option_combo¶
In plain words. Some data sets report every value under an extra
dimension - by funding partner, by project. When a data set does, the guide
publishes the vocabulary of that extra dimension, and this is the kind piece
on its name: D2AOC_..._VS.
When you would change it. Almost never. If none of your data sets use such a dimension, the piece never even appears in your guide.
Example.
Produces D2Reporting_..._VS instead of D2AOC_..._VS.
Default: "AOC" - If you leave it out: these names carry AOC.
If you get it wrong: the shared rule above refuses it.
organisation_unit¶
In plain words. The kind piece on everything generated from organisation
units - the facility registry entries and, if switched on, the organisation
unit code list: D2OU_..._CS.
When you would change it. Preference ("OrgUnit" reads better to some).
This is the one piece that cannot be "": organisation unit names would
collapse into nothing distinguishable.
Example.
Produces D2OrgUnit_..._CS, and d2-org-unit-... in addresses.
Default: "OU" - If you leave it out: organisation unit names carry
OU.
If you get it wrong: emptying it stops the run with
token must not be empty (shown under the shared rule above).
data_set¶
In plain words. The kind piece on forms generated from data sets:
D2DS_<data set> is the form of one data set.
When you would change it. Almost never; preference only.
Example.
Produces D2Form_... instead of D2DS_....
Default: "DS" - If you leave it out: data-set form names carry DS.
If you get it wrong: the shared rule above refuses it.
program¶
In plain words. The kind piece on forms generated from programs: an event
program's form, and a tracker program's registration form, are both
D2PR_<program>.
When you would change it. Almost never; preference only.
Example.
Produces D2Prog_... instead of D2PR_....
Default: "PR" - If you leave it out: program form names carry PR.
If you get it wrong: the shared rule above refuses it.
program_stage¶
In plain words. The kind piece on forms generated from tracker program
stages - each stage of a tracker program becomes its own form,
D2PS_<stage>.
When you would change it. Almost never; preference only.
Example.
Produces D2Stage_... instead of D2PS_....
Default: "PS" - If you leave it out: stage form names carry PS.
If you get it wrong: the shared rule above refuses it.
tracked_entity_type¶
In plain words. The kind piece on the person-only registration form a
tracked entity type publishes - the form that creates a person and enrols them
in nothing: D2TET_<type>.
When you would change it. Almost never; preference only.
Example.
Produces D2Entity_... instead of D2TET_....
Default: "TET" - If you leave it out: person-only form names carry
TET.
If you get it wrong: the shared rule above refuses it.
Next: Serving it: the [serve] section - how the local
capture server runs.