Skip to content

Generate the IG source

This is the step that reads your instance. One command pulls the data sets, the event and tracker programs, the tracked entity types, the option sets, the category combinations, and the organisation-unit hierarchy, and writes them out as the source of a publishable package - one form per data set, one per event program, one per tracker stage, a code list per option set, and two files per organisation unit. Nothing is written to DHIS2 and nothing is published yet; this writes files into your project directory.

Who this is for: the operator regenerating after a metadata change, and the implementer running generation for the first time.

Before you start: a scaffolded project pointing at your instance (Set up an IG project), ideally after a clean validate - generate refuses the codes and names validate marks as build-aborting.

You will be able to:

  • run the whole pipeline, or one target of it, and read the summary
  • say which directory each target owns and what a re-run does to it
  • narrow the selection so a national instance stays reviewable
  • find the notes a run raised and know which ones repeat validate

Run the whole pipeline

$ d2w fhir generate
running 8 step(s)
[1/8] instance metadata: 14 questionnaire target(s), 13 option set(s), 5 categories,
1,332 organisation unit(s)
[2/8] foundation: 23 files written, 0 files unchanged
[3/8] option sets: 13 option sets, 39 files written, 0 files unchanged
[4/8] categories: 5 categories, 15 files written, 0 files unchanged
[5/8] questionnaires: 14 questionnaires, 28 files written, 0 files unchanged, 1 note raised here
[6/8] examples: 14 examples, 14 files written, 0 files unchanged, 2 notes raised here
[7/8] organisation units: 1,332 organisation units, 2,667 files written, 0 files unchanged
[8/8] pages: 6 pages, 20 files written, 0 files unchanged, 1 note raised here
full pipeline: 2,806 file(s) written across 7 target(s)
info: local_basic (fhir.toml) -> /home/you/demo-ig
                                                   fhir generate (7)
┏━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃Target         ┃ Subject                  ┃ Directory                 ┃ Files written ┃ Files deleted ┃ Distinct notes┃
┡━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│foundation     │ -                        │ ig/input/fsh/foundation   │ 23            │ 0             │ 0             │
│option-sets    │ 13 option sets           │ ig/input/resources/termi… │ 39            │ 0             │ 0             │
│               │                          │ resources/concept-maps    │               │               │               │
│categories     │ 5 categories             │ ig/input/resources/categ… │ 15            │ 0             │ 0             │
│               │                          │ resources/concept-maps    │               │               │               │
│questionnaires │ 14 questionnaires        │ ig/input/fsh/data-sets,   │ 28            │ 0             │ 1             │
│               │                          │ fsh/event-programs,       │               │               │               │
│               │                          │ fsh/tracker-programs,     │               │               │               │
│               │                          │ fsh/tracked-entity-types, │               │               │               │
│               │                          │ fsh/data-dictionary,      │               │               │               │
│               │                          │ resources/assignments,    │               │               │               │
│               │                          │ resources/attribute-opti… │               │               │               │
│               │                          │ resources/concept-maps    │               │               │               │
│examples       │ 14 examples              │ ig/input/fsh/examples     │ 14            │ 0             │ 1             │
│org-units      │ 1,332 organisation units │ ig/input/fsh/organizatio… │ 2,667         │ 0             │ 0             │
│               │                          │ resources/registry        │               │               │               │
│pages          │ 6 pages                  │ ig/input/pagecontent      │ 20            │ 0             │ 0             │
└───────────────┴──────────────────────────┴───────────────────────────┴───────────────┴───────────────┴───────────────┘
note: the run's step lines count 4 note(s) and the table counts 2: a step line
counts what that target raised, and the Distinct notes column counts a note once
for the whole run, on the first target that raised it
note: 2 distinct note(s) across 2 target(s); full list in
/home/you/demo-ig/reports/fhir-generate-notes.md (--details to print)

Every line carries two different numbers. The subject is what the target covers on the instance - 1,332 organisation units, 14 questionnaires - and the file counts say how many files publishing that took, which is larger wherever one covered object ships as several resources: an organisation unit becomes both an Organization and a Location, so 1,332 units are 2,667 files. Foundation covers no instance object at all, so its line and its Subject cell are files alone.

The step lines and the table disagree about the examples on purpose: the step line counts what that target raised on its own, and the table counts each note once, on the first target that raised it - the greys-out note the examples and the pages both repeat is filed under the questionnaires. A run whose two totals differ says which counting each is, in a line under the table, so nobody has to reconstruct it from a target that announced 329 and shows 0.

The bare run is the one to reach for: it reads the instance once and every target builds off that single result, where seven separate commands each open a client of their own. It is also the cheap half of the chain - against a local instance a run like the one above is quick, and the real wall clock is all in the compile and publish that follows. uv run d2w fhir generate is the same command through the project's pinned toolchain, and the closing info: line names the profile it read and the project it wrote.

Re-running against an unchanged instance converges to zero:

$ d2w fhir generate
running 8 step(s)
[1/8] instance metadata: 14 questionnaire target(s), 13 option set(s), 5 categories,
1,332 organisation unit(s)
[2/8] foundation: 0 files written, 23 files unchanged
[3/8] option sets: 13 option sets, 0 files written, 39 files unchanged
[4/8] categories: 5 categories, 0 files written, 15 files unchanged
[5/8] questionnaires: 14 questionnaires, 0 files written, 28 files unchanged, 1 note raised here
[6/8] examples: 14 examples, 0 files written, 14 files unchanged, 2 notes raised here
[7/8] organisation units: 1,332 organisation units, 0 files written, 2,667 files unchanged
[8/8] pages: 6 pages, 0 files written, 20 files unchanged, 1 note raised here
full pipeline: 0 file(s) written across 7 target(s)

Know the eight targets

d2w fhir generate                All seven, in that order, off one pass over the instance
d2w fhir generate foundation     Identifier aliases + the extensions + the capture contract
d2w fhir generate option-sets    Option sets -> CodeSystem/ValueSet pairs
d2w fhir generate categories     Categories -> CodeSystem/ValueSet pairs
d2w fhir generate questionnaires Data sets + event programs + tracker programs -> Questionnaires
d2w fhir generate examples       Example QuestionnaireResponses answering those Questionnaires
d2w fhir generate org-units      Organisation units -> Organization/Location instances
d2w fhir generate pages          Narrative site pages + per-artifact intros
d2w fhir generate load-set       Synthetic QuestionnaireResponse corpus into load/ (not IG source)

Name a target when you want that target alone - a tight edit loop on one directory. A solo run prints its own detail table and all of its notes:

$ d2w fhir generate pages
running 2 step(s)
[1/2] instance metadata: 14 questionnaire target(s), 1,332 organisation unit(s)
[2/2] pages: 0 written, 20 unchanged, 1 note
                              fhir generate pages
┌──────────────┬──────────────────────────┐
│profile       │ local_basic (fhir.toml)  │
│project       │ /home/you/demo-ig        │
│target        │ ig/input/pagecontent     │
│files written │ 0                        │
│unchanged     │ 20                       │
│files deleted │ 0                        │
│pages         │ 6                        │
│intros        │ 14                       │
└──────────────┴──────────────────────────┘
note: data set 'Child Health' (BfMAe6Itzgt) greys out 8 disaggregated cells, which
are not published; a response answering one would not be of the form:
DUSpd8Jq3M7.hEFKSsPV5et, DUSpd8Jq3M7.psbwp3CQEhs, ca8lfO062zg.Prlt0C1RF0s,
ca8lfO062zg.V6L425pT3A0, d5xTg3WR3DP.Prlt0C1RF0s and 3 more

What each target writes, and where:

Target Writes
foundation ig/input/fsh/foundation/ - the identifier aliases and NamingSystems, the D2Period / D2FormType / D2AttributeValue and tracker extensions, the response profiles, $generate, and the capture CapabilityStatement. Depends on fhir.toml alone; never touches DHIS2. Prerequisite for a compiling IG.
option-sets Pre-built CodeSystem/ValueSet JSON into ig/input/resources/terminology/, ConceptMaps into resources/concept-maps/.
categories Its own CodeSystem/ValueSet JSON into ig/input/resources/categories/, ConceptMaps into resources/concept-maps/.
questionnaires One Questionnaire per data set (fsh/data-sets/), per event program (fsh/event-programs/), per tracker stage plus the program's registration form (fsh/tracker-programs/<program stem>/), per tracked entity type's person-only registration form (fsh/tracked-entity-types/), the shared data dictionary (fsh/data-dictionary/), assignment Lists (resources/assignments/), attribute-option-combo terminology (resources/attribute-option-combos/), and the two ConceptMaps that map an instance's own statements outward - D2Sex_CM and D2Section_CM - into resources/concept-maps/.
examples One Usage: #example response per target into fsh/examples/<target stem>-<n>.fsh.
org-units Profiles and level terminology into fsh/organization/; the registry as pre-built JSON - Organization-<stem>.json + Location-<stem>.json per unit - into resources/registry/.
pages Six site pages plus per-artifact intros into ig/input/pagecontent/ - markdown, not FSH.
load-set Test data into load/ beside ig/, for posting at a running facade (Serve the IG). Gitignored; the bare run never writes it.

What the artifacts contain - the extensions, the identifier slices, the value-type mapping, the stems in those file names - is the reference material in Identifiers and the D2 extensions and Terminology and ConceptMaps.

Each target owns its subdirectories and syncs each one: writes what changed, leaves what did not, deletes generated files that no longer belong. A JSON sync owns its directory outright - it deletes every *.json the run did not produce - which is why each terminology pair gets a directory of its own. concept-maps/ is the one shared directory; ownership there is stated by file-name prefix. The tracker-programs/ sweep prunes a per-program subdirectory it emptied. Hand-authored files are safe by the same rule the whole tree follows: the sweep only deletes files carrying the generated header, so ig/input/pagecontent/index.md and any markdown you drop beside it survive every regenerate.

A run that rewrites FSH removes the compile. ig/fsh-generated/ is SUSHI's output and nobody else's, and the moment a target writes a FSH source it holds resources compiled from sources that no longer exist. So the run removes it and says which tree went and why:

note: removed ig/fsh-generated: it held SUSHI's compile of FSH sources this run
rewrote, and check-artifacts, serve, forward and `make build` all read that
tree. Run `make sushi` in the project to compile the sources this run wrote.

A compile that stops on an error removes it too. SUSHI writes ig/fsh-generated/ as it goes and keeps whatever it had written when it stopped, and that half reads as a finished guide to everything downstream - the FHIR endpoint would publish those Questionnaires as though the compile had succeeded. So make sushi removes the directory when SUSHI exits non-zero, and says which tree went:

SUSHI stopped on an error. Removed ig/fsh-generated, which held a partial
compile: the FHIR endpoint has nothing to read until a compile finishes.

SUSHI's own exit status is what make reports, so a failing compile still fails the recipe. The presence of ig/fsh-generated/ therefore means one thing: a compile that finished.

make sushi writes it again from the sources on disk, and make build runs the publisher's own SUSHI before it publishes, so a build needs nothing extra. Serving and forwarding read the compiled tree too, and each already refuses a project holding none by naming those same two steps - generate, then make sushi - so a serve right after a generate is the flow those pages already teach. d2w fhir serve --live reads the instance instead and needs no compile at all.

A regenerate that writes nothing leaves the compile where it is: the sources it was compiled from are still the sources on disk. Nothing else is removed - ig/temp/ is the IG publisher's own scratch, which no command reads between builds, and ig/output/ is a published site, which is a thing to replace deliberately rather than to discard on a source edit.

The pre-built resources are not committed. The scaffolded .gitignore covers ig/input/resources/ - thousands of regenerable files - while ig/input/fsh/ is committed, so the FSH diff after a metadata change is still there to review.

Narrow the selection

Absent or empty selection tables mean all of that kind on the instance. A national instance carries hundreds of forms, so an IG meant for review names the handful of UIDs it is about:

[generate.data_sets]
include_ids = ["TuL8IOPzpHh"]       # EPI Stock

[generate.event_programs]
include_ids = ["EVTsupVis01"]       # Supervision visit

[generate.tracker_programs]
include_ids = ["IpHINAT79UW"]       # Child Programme - registration + one per stage

[generate.tracked_entity_forms]
include_ids = ["nEenWmSyUEp"]       # Person - register a person, no program

[generate.option_sets]
include_ids = ["OsVaccType1"]       # Vaccine type

[generate.categories]
include_ids = ["yY2bQYqNt0o", "Qzh0MSUx4RM"]

[generate.organisation_units]
max_level = 4                       # or root = "<uid>" for one sub-hierarchy

Name the terminology tables too, and not only the form tables. A table left absent selects everything of its kind, and one DHIS2 name carrying a raw < anywhere in that everything refuses the run - which is why every guide in examples/igs/ names its option sets and its categories explicitly.

Or seed the same lists while scaffolding: d2w fhir init --data-set ... --event-program ... --tracker-program ... --org-unit-max-level 4 (offline; written as given). Points worth knowing:

  • The two program tables select opposite programTypes, and a UID listed under the wrong one fails the run by name rather than being quietly reshaped. With empty lists the sweep routes each program by its live type.
  • [generate.tracked_entity_forms] is the person-only shelf. Left empty it publishes a registration form for each tracked entity type the selected tracker programs register - a form that registers somebody in DHIS2 without enrolling them in any program. Name UIDs to publish a different set.
  • The option-set closure: a selected form binding an option set outside [generate.option_sets] include_ids pulls the set in anyway, with a note. Validate resolves scope the same way, so the two never disagree.
  • Registry scale: every organisation unit emits two instances, and the IG publisher validates and renders every resource, so the registry sets the wall clock of the publisher run. org-units warns once a registry passes 2,000 instances, naming both dials (max_level, root). See Build and publish the guide.
  • Examples: [generate.examples] per_target sets responses per form (0 disables the target); source = "synthetic" (default, deterministic, no data endpoint called) or "instance" (answers from the values the server holds - deliberate on production, since real captured values land in a document you are about to publish).
  • Locales: [generate] locales = [] means every locale found on the instance; list BCP-47 or DHIS2-style tags (pt_BR and pt-BR both work) to narrow. NAME translations become concept designations and title/name translation extensions; a question DHIS2 gives a form name to takes its label translation from FORM_NAME. Nothing else is emitted.

Know which spelling a surface speaks

A DHIS2 data element carries two spellings, and they are written for two audiences. name is written for analytics and reference - it has to be unique and it reads like a report column. formName is written for input: not always to shorten, but to clarify what a person is being asked. So each surface speaks the one it is for.

Input surfaces speak formName, reference surfaces speak name. A Questionnaire.item.text - a data set question, an event programme question, a tracker stage question, a registration form's tracked entity attribute, and the group heading of a disaggregated element - is the form name wherever DHIS2 states one and the name wherever it does not. The data dictionary is the reference surface, so a D2DE_CS or D2TEA_CS concept keeps displaying the name and states the form name beside it as a form-name property; a consumer holding one concept reaches both spellings. The cells of a disaggregated question are labelled by their category option combo, which has no form name of its own.

Both spellings are person-written, so both pass the same gate: a < in a form name refuses a run and is graded a template-hostile-name error by d2w fhir validate exactly as a < in a name is.

Know what a run refuses to write

A DHIS2 name is kept byte-true on the resource it becomes, and a DHIS2 form name is kept byte-true on the question it labels; the IG publisher writes both into pages it strict-parses after writing. A < opens a tag, so the parse of the page it just wrote fails - hours in, once every resource has already been rendered. A run that would write such a name is refused whole before a single file is written, naming the object and which of its two spellings carries the character, and exiting 1:

$ d2w fhir generate
error: dataElements 'Vitamin A given to < 5y' (tU7GixyHhsv), asked by dataSets
'Child Health' (BfMAe6Itzgt), has a name carrying '<'. ...
$ echo $?
1

The whole run is refused rather than the one object skipped, because a quietly skipped object leaves a guide that disagrees with its own selection - a Questionnaire referencing terminology nobody wrote.

This is the same statement d2w fhir validate makes, in both directions. Validate reads the same hostile_names posture this run does, so under "refuse" - and unset, which refuses - every name graded a selection-scoped template-hostile-name error there refuses a run here, and every name refused here is graded that error there. Under "substitute" neither command stops, and validate grades those names informational naming the wording published in their place. It holds over every kind of object a selection publishes: option sets and their options, categories and their category options, organisation units, data sets, event programs, tracker programs and their stages, tracked entity types, and the data elements and tracked entity attributes those forms ask as questions. Codes are gated too, on the six collections whose codes become identifier values - optionSets, categories, organisationUnits, dataSets, programs, programStages.

Run d2w fhir validate first anyway: generate stops at the first object it cannot write, so on an instance with several offenders only validate lists them all. The fix is in DHIS2 - rename the object - or leave it out of the selection. examples/igs/refused-names/ is a working exhibit of both commands on one poisoned selection.

A build reads neither of them. make build publishes whatever ig/fsh-generated/ and ig/input/ hold, so artifacts written before this gate existed, artifacts from a generate an older lock pinned, and hand-authored FSH all reach the publisher without ever passing it. d2w fhir check-artifacts is the same refusal applied to those files, through the same two predicates, and it is what make build runs first - see The build refuses before it begins.

Answer the hostile-name question

Refusing is one answer, and for many instances it is the wrong one. DHIS2 names carry < legitimately: an age band is called 5 to < 15 years, Female and a disaggregation cell is called Male, <15y. Renaming several hundred of those in a production instance to publish a guide is not a fix - it is the guide's problem pushed onto the people who run the instance.

So a run has three outcomes, and you choose which:

Outcome How you get it What happens
Refuse --refuse-hostile-names, or hostile_names = "refuse" The run writes nothing and names the object, exactly as above. Every code is published byte-true.
Ask neither flag, no dial, and a terminal The names and codes are shown with their rewrites and you answer yes or no.
Substitute --substitute-hostile-names, or hostile_names = "substitute" The guide publishes the rewritten wording and the hyphenated codes, and every rewrite is noted.

A flag beats the fhir.toml dial, and the dial beats the question. With no terminal to ask on - a script, a CI job - the run never hangs on a prompt: it prints the same block, names the two flags, and leaves every name and code as DHIS2 states it.

The name rewrite is wording, not escaping. A comparison becomes the words it stands for - < reads "under", > reads "over", <= and </= read "at most", >= and >/= read "at least" - so a reader of the guide reads a sentence:

DHIS2 name Published name
5 to < 15 years, Female 5 to under 15 years, Female
Male, <15y Male, under 15y
Age <= 5 Age at most 5
>/=2500g at least 2500g
>10 sec over 10 sec

Codes carrying a comparison are reworded the way names are, and only under substitute. A < in a code opens a tag in the table cell an identifier value lands in, so under refuse the run is refused. Under substitute both < and > are reworded, so a code and the name beside it read alike, and then each space is hyphenated:

DHIS2 code Published code
<1500g under-1500g
>10 sec over-10-sec
>/=2500g at-least-2500g

Codes carrying a space are rewritten too, and only under substitute. A space is legal in an R4 code, so nothing refuses Pre eclampsia - and the IG publisher's anchor slug strips whitespace, so it and a literal Preeclampsia render one anchor id (IG_PUBLISHER_ISSUES.md 107), while every URL, CQL quotation, and terminology server below the guide escapes or quotes the space at its own discretion. Each space becomes a hyphen:

DHIS2 code Published code
Pre eclampsia Pre-eclampsia
IPT 1 IPT-1

A published code depends on its own DHIS2 code and nothing else, so adding a form to the selection or cleaning another code in DHIS2 never changes it. DHIS2 holds no duplicate codes, but a rewrite can land on one: Pre eclampsia beside a literal Pre-eclampsia, or <5 beside under 5, which both read under-5. Publishing either under a suffix would let a published code move from one object to the other between runs, so the run is refused instead, naming both codes; change one of them in DHIS2. d2w fhir validate reports the same pair as a code-substitution-collision error.

DHIS2 is never written to, and nothing is lost. No UID is touched. Every rewritten concept states the DHIS2 code byte-true as a dhis2-code 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 back to its DHIS2 UID. The capture path reads that property, so a QuestionnaireResponse answering with a published code still writes the DHIS2 code to DHIS2.

A substitute build says so. Every generated site page - Forms, Registry, Terminology, Identifiers, Periods and Capturing data - opens with a note saying that some names and codes are published in a different spelling from the one the DHIS2 instance holds, how they were rewritten, and that each rewritten concept states the instance's spelling as its dhis2-name or dhis2-code property. A run that substitutes always carries the note; a run that does not never does. The hand-written index.md is left as it is.

Every rewrite lands in the notes - a name-substitution note per distinct DHIS2 name and a code-substitution note per distinct DHIS2 code, however many resources carry them - so the notes report is the record of where the guide and the instance say different things:

note: the DHIS2 name '5 to < 15 years, Female' carries a comparison the IG
publisher's pages cannot carry as it stands; the guide publishes '5 to under
15 years, Female', states '5 to < 15 years, Female' as a `dhis2-name`
property, and DHIS2 keeps the name it holds

note: the DHIS2 code 'IPT 1' carries a space, which the IG publisher's anchors
and every URL downstream of them handle at their own discretion; the guide
publishes 'IPT-1' and states the DHIS2 code beside it as a `dhis2-code` concept
property

Substitution reaches further than the refusal does. The refusal reads the names and form names of the objects a selection publishes and the questions they ask; the rewrite reads every name the emission publishes, which also covers the cell names of a disaggregation. Those reach a Questionnaire's cell labels and the data dictionary's category option combo displays without passing any refusal - one national selection generated cleanly and then handed the publisher 738 of them. Under refuse that class is caught by d2w fhir check-artifacts, before you spend a build on it; under substitute it never reaches the disk.

Read the notes

Each target raises aggregate notes - a selection entry that matched nothing, an option set the closure pulled in, a form skipped for a linkId collision. A note carries a kind, and three kinds (code-fallback, code-collision, stem-fallback) are restatements of what validate already reports. A bare run counts them - the echoes separately - and writes them all to reports/fhir-generate-notes.md, each note once, under the first target that raised it:

note: 3 distinct note(s) across 2 target(s) (+8 validate echoes); full list in
reports/fhir-generate-notes.md (--details to print)

Read that as: generation found three things worth your attention, and eight more it would only be repeating d2w fhir validate to tell you about. Nothing is hidden - the file carries every note, echoes under a trailing ### Restatements of validate findings heading. --details prints every note inline instead; a solo target always prints all of its notes. From the demo run above:

$ cat reports/fhir-generate-notes.md
# fhir generate notes

- Profile: local_basic (http://localhost:8080)
- Generated: 2026-08-15T18:02:36+00:00

## questionnaires

- data set 'Child Health' (BfMAe6Itzgt) greys out 8 disaggregated cells, which are not published; a response answering one would not be of the form: DUSpd8Jq3M7.hEFKSsPV5et, DUSpd8Jq3M7.psbwp3CQEhs, ca8lfO062zg.Prlt0C1RF0s, ca8lfO062zg.V6L425pT3A0, d5xTg3WR3DP.Prlt0C1RF0s and 3 more

## examples

- 1 question takes an attachment, a geometry document, or a reference to a DHIS2 object the IG does not publish; left unanswered in the synthetic examples: Birth certificate (uf3svrmp8Oj)

The greys-out note is a fact about the form, so the questionnaires, the examples, and the pages targets all raise it - and the file states it once, on the questionnaires, because they are the first target that did.

That first note is the one worth reading twice, because it is a DHIS2 fact about your form rather than a defect in the run. A data set section can grey out individual cells of a disaggregated group - DHIS2's own entry screen renders the cell and refuses to let anyone type in it. Those cells are not published, so no consumer of this package can answer one either; a response that did would not be a response to this form. The note names each one as <data element>.<category option combo> and counts the rest, and the same note is raised by every target that reads the form.

Four outcomes the notes do not carry

A national instance raises several hundred terminology notes per run, and a form nobody can submit would be lost among them. So when an organisation-unit assignment names no organisation unit the run publishes, the run closes with a line of its own:

warning: 29 published form(s) carry an empty organisation-unit assignment under
[generate.organisation_units] max_level 2: no organisation unit may report them,
and the facade refuses to draft a response for one. The assignment hangs on:
Child Programme (IpHINAT79UW), EPI Stock (TuL8IOPzpHh). Widen the
organisation-unit selection - raise `[generate.organisation_units] max_level`, or
set its `root` higher up the hierarchy - or narrow the form selection in
fhir.toml to the forms those organisation units report, then run
`d2w fhir generate` again.

The count is of published Questionnaires, which is what a capture client is refused at - a tracker program's stages each publish a form and share their program's one List - so this line, the facade's 422 and d2w fhir check-artifacts all state one number, and all three prescribe the same two selections. The names beside it are the data sets and programs DHIS2 hangs the assignment on, because that is the object a reader reassigns. Choosing a max-level carries the trade-off that usually produces it.

The second is a form every attribute option combo it declares is restricted away from. DHIS2 scopes a category option to organisation units on top of the assignment, and refuses a capture keyed to a combo not usable at the organisation unit it was filed from with E8025 - so a form whose vocabulary no organisation unit that may report it admits any concept of is a form nobody can submit, exactly as an empty assignment is, one axis over. The run already wrote the restriction Lists that prove it, so it closes with the fact rather than leaving it to a $generate 422:

warning: 3 published form(s) declare attribute option combos DHIS2 restricts
away from every organisation unit that may report them under
[generate.organisation_units] max_level 3: no capture for one of them can be
keyed to a combo this DHIS2 instance accepts, and the facade refuses to draft a
response for one. They are: EPI Stock (TuL8IOPzpHh), Life-Saving Commodities
(ULowA8V3ucd), Project Management (QX4ZTUbOt3a). Widen the organisation-unit
selection until one of the restricted organisation units is published - raise
`[generate.organisation_units] max_level`, or set its `root` to an organisation
unit above them - or narrow the form selection in fhir.toml to the forms those
organisation units report, then run `d2w fhir generate` again.

d2w fhir check-artifacts files the same fact as a warning-level finding, reading it back off the published Lists, so a build machine with no DHIS2 connection asks the same question. The examples target publishes no example for such a form: there is no capture DHIS2 would take, so drafting one would be publishing the very response the facade then refuses.

The third is that same form one axis further over: one DHIS2 has closed every combo of. A category option is scoped to a calendar window as well, and DHIS2 refuses a capture the window does not cover entirely with E8032 - so a form whose every declared combo closed before the period it reports now, which is the newest completed period of its own period type and therefore before every later period too, is a form nobody can submit. The run already wrote the windows that prove it, so it closes with the fact:

warning: 2 published form(s) declare attribute option combos DHIS2 has closed:
no attribute option combo of the form is valid for any period it reports, so no
capture for one of them can be keyed to a combo this DHIS2 instance accepts, and
the facade refuses to draft a response for one. They are: EPI Stock
(TuL8IOPzpHh), Life-Saving Commodities (ULowA8V3ucd). A category option's window
is DHIS2 metadata - `startDate` and `endDate` on the category option - and
fhir.toml has no setting that widens it. Reopen the category options in DHIS2, or
narrow the form selection in fhir.toml to the forms whose attribute option combos
are still open, then run `d2w fhir generate` again.

Unlike the other two, no fhir.toml dial reaches this one, and the line says so rather than implying one: the window lives on the DHIS2 category option. d2w fhir check-artifacts files the same fact as a warning-level finding, read off the windows the vocabulary publishes and the period type the Questionnaire declares, so a build machine with no DHIS2 connection asks the same question. The examples target keys an example to a combo still open for the period it reports, and publishes no example at all for a form none of whose combos is.

The fourth is a selection entry that matched nothing. A UID no longer on the instance - an object renamed, deleted, or never there - costs the guide a whole form, its examples and its page, so the run closes with that line of its own too, naming every UID the instance answered nothing for:

warning: 1 of 2 [generate.data_sets] include_ids entries matched no data set: BfMAe6Itzgu

A table none of whose entries matched says so outright - no [generate.event_programs] include_ids entry matched an event program on this instance, so this run publishes no event program at all - because a guide with nothing of a kind in it is rarely what the file meant to ask for. d2w fhir check-artifacts reports the same entry as a warning-level finding against fhir.toml, so a tree on disk answers for itself with no connection.

The fifth is not a loss at all - it is a gap the run made on purpose, and says so because nothing else would. A DHIS2 program rule whose action is ASSIGN computes a question's answer on import, and DHIS2 refuses a payload whose answer is neither empty nor byte-equal to the value it calculated (E1307), so every example this run writes leaves those questions unanswered:

note: 4 published form(s) ask 5 question(s) a DHIS2 program rule computes the
answer to on import, so every example this run wrote leaves them unanswered -
DHIS2 refuses a capture answering one anything but the value it calculated, with
E1307. They are: Antenatal care visit (WZbXY0S00lP), Child Programme - Birth
(A03MvHHogjR), ...

Without the line, a form whose corpus answers nine of thirteen questions reads as an emitter that lost four. d2w fhir check-artifacts files the other half of the fact: an example that answers one of those questions anyway, in the compiled JSON or in a hand-authored FSH source.

Every command with an instance behind it narrates its steps on stderr - a spinner on a terminal, one plain [k/N] label: summary line per step when redirected. --no-progress turns it off; d2w --json fhir generate implies it and puts the per-target file lists and counts on stdout as JSON.

Check the site pages landed

pages is the last target and the only one writing markdown: forms.md, registry.md, terminology.md, identifiers.md, periods.md, and capture.md - the scaffolded site menu - plus per-artifact intros the publisher injects into matching artifact pages (Questionnaire-<stem>-intro.md always; CodeSystem and Organization intros only when the DHIS2 object carries a description). index.md is scaffolded once and is yours. Every DHIS2 name and description on these pages is escaped on the way in, so a data set called "Mortality < 5 years by gender" renders as text rather than aborting the publisher's HTML parse.

Next: Build and publish the guide