Skip to content

Contributing and feedback

Questions, corrections and proposals are all welcome, from anyone reading this spec — the team that maintains it, other teams at WELL, and partners integrating against it. You do not need repository access to contribute usefully.

Reach us

Email — shawn.vincent@well.company is the front door. It works for everyone, internal or external, and it is the right choice if you are not sure where something belongs.

Also fine:

  • Microsoft Teams — message the Nexus EMR development team, if you already work with us there.
  • Slack #discuss-fhir — where the modelling conversation actually happens: proposals, questions, and the announcement of every release. Open to WELL staff; the link resolves only inside the WELL workspace.
  • A pull request against the FSH source, if you have access to the repository — see Contributing a change below.

We aim to respond to email and Teams within 2-3 business days. If something is time-sensitive, say so in the subject line.

What is most useful to tell us

Implementation experience is the most valuable input this spec gets, and the most valuable reports are the specific ones:

  • an example that does not validate, or a constraint that contradicts itself;
  • an element you need that is not modelled, with the use case that needs it — the IG profiles a type when code demonstrably reads or writes it, so the use case is the argument;
  • guidance that reads two ways, or a description you had to guess at;
  • a rule that fails for conformant data — that is a defect in the spec, not in your data.

Include the IG version (see the footer of any page), the profile or example you were working against, and the payload if you can share one.

Contributing a change

Changing this IG has two tracks, and which one you are on decides how much you do.

  • Contributors (everyone) -- propose a change by editing the FHIR Shorthand (FSH) source. That is all you do: no generators, no version bump, no release.
  • IG owners -- run npm run build, which regenerates every derived artifact and refuses to pass until the regeneration is committed; then npm run build <x.y.z> to cut a version and npm run publish to publish the package and this site. The owners are the teams in CODEOWNERS, which adds them to your PR automatically. What they do is written down in the Release Process.

Do not regenerate or commit derived artifacts

The FSH is the source of truth. Everything else -- the compiled conformance resources, the TypedFhir types for TypeScript, Python and .NET, the rendered pages of this site, the synthetic patient corpus -- is generated by the owners, not by you. Do not run the generators, and do not commit their output.

That is not a formality about tidiness. Regeneration needs the whole toolchain (SUSHI, three language generators, the seed-data generator), and every generated artifact is keyed to one IG version, so a PARTIAL regeneration is worse than none: it puts the artifacts out of lockstep with each other while looking finished. Asking every contributor to own that would turn a one-line FSH correction into an infrastructure problem.

What that means in practice. Your PR contains FSH, an example, and a changelog line. The generated output arrives before it merges, added by an owner running npm run build -- which regenerates everything and refuses to pass until the result is committed. So a merged IG change does land 300-400 files; they are simply not files you produce. Say in your PR that you have not run the gate, and an owner will.

To propose a change:

  1. Edit the FSH under src/fsh/. That is where profiles, extensions, value sets, code systems, and invariants live. Follow conventions.md (one profile per file, file-size caps, prefer portable FHIRPath invariants over unevaluable slices).
  2. Add or update an example in src/examples/ if you added or changed a profile -- every profile ships at least one conforming example, and bin/check-surfaces.ts fails without one.
  3. Add a CHANGELOG entry under the current in-progress heading in CHANGELOG.md -- the one that reads ## [x.y.z-candidate] - unreleased. One human-readable sentence written for the reader of the release (converter authors, bot developers, integrators), referencing your issue or ticket if you have one. Write what changed for them, not what you did.

    If there is no such heading, open ## [Unreleased] -- do not invent a version number. Branching from a released state is the normal case for a contributor and leaves a DATED heading at the top (## [1.19.0] - 2026-08-21): the candidate marker is put on when work on the next release starts and taken off when it is cut, so between the two there is nothing in flight to file under. Add ## [Unreleased] above the newest dated heading, put the ### Added / ### Changed / ### Fixed section your entry belongs in underneath it, and stop there. Which version your change ships in is not yours to decide, and a guessed number is worse than an open heading: an owner renames it when the release is cut, with npm run build <x.y.z>-candidate. That command accepts exactly two spellings for the in-flight heading -- ## [Unreleased] and ## [x.y.z-candidate] -- and RENAMES rather than creates, so the Keep-a-Changelog form is the one that hands off without anyone editing by hand.

    Your entry will not show up in the Releases table while it sits under ## [Unreleased]; that table is built from numbered headings only. Expected, not a symptom -- it joins the table when the heading is renamed.

    This file carries the compatibility contract in place of SemVer, so a breaking change is named as breaking, in bold, with the consumer action spelled out -- a reader is told to trust these entries rather than the version number, which only works if they are complete.

    Link the first mention of anything the spec defines. A profile, value set, code system, code system URI, or a base resource type this IG profiles: if your entry names it in backticks, the first mention gets a link, so a reader moves from "what changed" to "what it is now" in one click.

    First mention PER BULLET, not per page -- this file is the exception to the normal rule and everywhere else follows the normal one. Nobody reads a changelog top to bottom. They arrive at the entry that concerns them, read it, and leave, so a link spent in a different entry is one they never saw, and an unlinked name in the entry they ARE reading looks like an oversight. Each - bullet and its continuation lines is its own scope.

    Short forms link too, and they resolve against the bullet. Composition.date names one element outright. A bare `date` names one only in context -- and the context is the bullet: whichever resource the entry has most recently named is the one a following bare element name belongs to. Write `date` where the sentence reads better and link it to the resource the bullet is about.

    Two places that resolution does NOT hold, and where the short form should be written out or left alone:

    - text, and the other elements every resource inherits. A bare `text` in prose is almost always name.text or code.text, not the resource's narrative -- so write the path. On a DATATYPE profile the opposite is true (Annotation's own id and extension are exactly what an entry about note round-tripping means), and there the short form is right. - A type with more than one profile. ServiceRequest, Consent, Device and every extension have two or more, so priority under one of them has no single home to point at. Name the profile instead.

    Write the links yourself; nothing generates them. They are literal markdown in CHANGELOG.md, which is the authored source the site pulls in. A generator that linkified names on the way out would put the links somewhere nobody edits and make the authored file disagree with the published page. Write it site-relative -- fhir/StructureDefinition-<id>.md for a profile, fhir/ValueSet-<id>.md / fhir/CodeSystem-<id>.md for a vocabulary, guide/<area>/<page>.md for the handbook -- because the strict site build resolves and validates those, and an absolute URL to the published site would silently 404 after a rename. The cost is that the link does not work when reading CHANGELOG.md on GitHub; the site is where a partner reads it, and a validated link is worth more than one that renders in both places and rots in neither's view.

    One section per kind per release. ### Added, ### Changed, ### Fixed and so on appear at most once under a version. Appending a second ### Changed because yours is a separate PR splits the release in the reader's view and makes the anchors ambiguous; add your bullet to the section that is already there. Nor does a parenthetical make it a different section: ### Changed (documentation pipeline) is a ### Changed, and scoping like that belongs in the bullet, which is where a reader scanning the section will actually see it. Four releases had accumulated duplicates before this was written down, one of them hidden behind exactly that qualifier.

    A PR with no entry needs the no-changelog label and a reason. A change to TypedFhir belongs in this file too rather than in a separate library changelog: the IG is consumed through its typed accessors and validators, so a generator change is a change to what the IG means in practice.

  4. Open a PR. CODEOWNERS adds the IG owners automatically. If your change is incomplete or exploratory, say so -- a proposed FSH shape you want folded in is a perfectly good PR.

Leave to the owners:

  • cutting a version (npm run build <x.y.z>, which writes it into sushi-config.yaml and regenerates everything that derives from it -- never edit a version by hand);
  • tagging, releasing, and publishing this site;
  • deciding whether a change needs a matching validator rule or a data-generator update. If you think it does, note it in the PR.

Never, on either track: hand-edit a generated file. The next generator run reverts it silently, and the lockstep check will not catch it because the tree is clean at the moment it looks.

Checking your work before you commit

npm run build is the gate and it is thorough -- a SUSHI compile, a full regeneration and 18 checks. That makes it the wrong tool for a one-line typo, which is why the standards checks are also available as an opt-in commit hook:

git config core.hooksPath backend/lib/TypedFhir/githooks

It reports Python formatting and lint, TypeScript typecheck failures, and internal ticket keys in FSH caret text -- staged files only, non-blocking, about 0.3s when nothing relevant is staged. It catches nothing pre-pr does not; it just tells you in seconds rather than at the end of a fifteen-minute run. git config --unset core.hooksPath turns it off, and nobody gets it by cloning: core.hooksPath is local config. See backend/lib/TypedFhir/githooks/README.md.

One thing it checks that the gate can only check after a build: FSH caret text compiles. ^comment, ^purpose, ^definition, ^short and ^copyright land in the published StructureDefinitions and ship inside package.tgz to app partners. A // comment beside them does not. The two look identical while you are writing them, so keep internal detail -- ticket keys, deliberation, anything naming a third party's internals -- on the // side.

Why the two tracks

Regeneration is deterministic but it is not free, and it needs the whole toolchain: SUSHI, the TypedFhir generators for three languages, and the seed-data generator. Asking every contributor to own that would make a one-line FSH correction an infrastructure problem. Concentrating it in the people who run the gate keeps the barrier to proposing a change as low as editing one file, while still guaranteeing that what merges is in lockstep with a single, announced IG version.