Releases¶
Every version of this specification: what changed in it, and what you can still fetch. Subscribe to releases.xml to be told when it changes — the feed carries the entry and links the package directly, so a feed reader or a Teams/Slack connector is enough to keep up.
Every release¶
| Version | Date | Changes | Package | |
|---|---|---|---|---|
| 1.25.1 | 2026-09-10 | 5 added, 6 breaking changes, 3 document status reasons, salient fields, and form-template field mapping | nexus-1.25.1.tgz |
current |
| 1.25.0 | 2026-08-31 | breaking · 7 changed, 3 removed, 1 fixed | nexus-1.25.0.tgz |
|
| 1.24.0 | 2026-08-29 | 5 added, 3 changed, 1 removed | nexus-1.24.0.tgz |
|
| 1.23.0 | 2026-08-29 | breaking · 6 added, 4 changed, 1 removed | nexus-1.23.0.tgz |
|
| 1.22.0 | 2026-08-26 | breaking · 6 added, 15 changed, 1 removed | nexus-1.22.0.tgz |
|
| 1.21.1 | 2026-08-26 | 7 added, 4 changed | nexus-1.21.1.tgz |
|
| 1.21.0 | 2026-08-25 | breaking · 13 added, 13 changed, 3 removed, 4 fixed | nexus-1.21.0.tgz |
|
| 1.20.0 | 2026-08-24 | breaking · 14 added, 31 changed, 1 removed, 4 fixed | nexus-1.20.0.tgz |
|
| 1.19.0 | 2026-08-21 | breaking · 4 added, 23 changed, 2 deprecated | nexus-1.19.0.tgz |
|
| 1.18.0 | 2026-08-19 | 12 added, 5 notes for implementers | nexus-1.18.0.tgz |
|
| 1.17.0 | 2026-08-16 | breaking · 7 added, 3 changed, 16 fixed | nexus-1.17.0.tgz |
|
| 1.16.0 | 2026-08-14 | breaking · 2 added, 4 changed, 3 fixed | nexus-1.16.0.tgz |
|
| 1.15.1 | 2026-08-12 | 16 added, 3 changed, 1 removed, 3 fixed | nexus-1.15.1.tgz |
|
| 1.15.0 | 2026-08-11 | withdrawn | nexus-1.15.0.tgz |
|
| 1.14.1 | 2026-08-09 | 2 added, 4 fixed | not archived | |
| 1.14.0 | 2026-08-09 | 3 added, 8 changed, 4 fixed | not archived | |
| 1.13.0 | skipped — no withdrawn release, no aborted branch | |||
| 1.12.0 | 2026-08-05 | 6 added, 7 changed, 3 fixed | not archived | |
| 1.11.0 | 2026-08-02 | 18 added, 4 changed, 4 removed, 5 fixed | not archived | |
| 1.10.0 | 2026-07-23 | 1 added, 2 changed, 1 removed, 1 fixed | not archived |
Packages are archived from 1.15.0 onward. Earlier releases were built but not kept, so their package can be rebuilt from source (the FSH for any release is in the repository's history) but is not served here — nothing is missing from the record, only the prebuilt artifact. A tarball is self-describing once downloaded: its own package.json carries the name and version, and the archived copies are named nexus-<version>.tgz so the version is visible before you open it.
This IG is a fork of the Polaris FHIR IG taken at 1.10.0 (2026-07-23); releases before that belong to the upstream spec and its own history.
Versions are sequential, not SemVer. MAJOR is reserved for the release that declares this specification stable -- a decision to be taken deliberately and announced, not one to arrive at by arithmetic. Until then MINOR moves on any change and PATCH on fixes, which means the changelog carries the compatibility contract instead of the version number: a breaking change is named as breaking, in bold, with the consumer action spelled out. Read the entries rather than inferring safety from the number. A caret range on this package is not safe, and is not meant to be.
Each release heading records the TypedFhir version that generated it. This IG is consumed through TypedFhir's typed accessors, validators and dispatch tables rather than as raw StructureDefinitions, so a change to the generator is a change to what the IG means in practice and belongs here rather than in a separate library changelog. The two version numbers move independently -- TypedFhir tracks the library and vendors several IGs at once -- so "which library produced this release's artifacts" would otherwise be unanswerable after the fact.
Artifact names link to the spec. The first mention of a profile, value set, code system or handbook topic in an entry links to its page, so "what changed" and "what it is now" are one click apart rather than a search away. On the published site those links resolve; reading this file directly on GitHub, they will not, because they are written relative to the site and are validated against it by the build.
What changed, release by release¶
[1.25.1] - 2026-09-10¶
Generated by TypedFhir 0.15.0. Unchanged from 1.25.0; the library moves independently of this specification and did not need to move for this release.
Four bodies of work, six breaking changes. Read Breaking changes first.
The privacy and record-integrity model is the release. Item consent gains the three things a clinic
actually asks of it -- items the clinic defines itself, an answer that can expire, and a record of
which wording was agreed to -- and a statement of disagreement can now point at an element rather
than a whole resource. Two smaller bodies of work were carried in [Unreleased] before all of it
and ship here: a status reason on DocumentReference, and salient fields plus form-template field
mapping.
A patient can restrict their record, and dispute what it says¶
See: Consent, Privacy and Record Integrity,
Privacy Directives,
Consent Override,
Corrections,
Statements of Disagreement,
Security Labels,
Privacy Directive,
Statement of Disagreement
First pass: these controls are recorded here, and must be enforced by an implementation
This release defines how a restriction, an override, a correction, a disagreement and a disclosure label are recorded, and states what a conformant implementation must then do with each. No element of the model enforces itself.
A consumer of this API must apply the policy itself. No guarantee is made that a restricted resource has been filtered out of a response, or that a disclosure has been refused on your behalf. Each handbook page names the surfaces its obligation reaches.
The Privacy Directive profile models a standing patient restriction on the collection, use or disclosure of their own information under Ontario's Personal Health Information Protection Act -- the first Consent in this guide that expresses a restriction rather than a permission. The Statement of Disagreement profile carries an individual's own words about information a custodian refused to correct, with the associations a release path needs to include it wherever the disputed information goes.
Two extensions fill gaps R4 leaves. Corrected By
points from a record known to be wrong to the one that supersedes it: R4 gives DocumentReference a
relatesTo and gives Observation, Condition and Patient nothing, so a reader holding the stale
record has no address to go to. Record Severed
marks content retained and traceable but removed from ordinary clinical view -- a statement about
visibility, which no status code expresses, because status is about whether content is true.
Correction requests are ordinary
clinical Tasks under a new
privacy-request bin in Task code. There is no correction Task
profile: statutory work with a response clock has to land in the worklist people already read.
HL7's Patient Request for Corrections vocabulary is adopted for
task types and
business status; its profiles and its package
dependency are not.
Every Consent states its decision in the same element¶
See: Consent, Privacy and Record Integrity,
Item Consent,
App Consent,
Consent Family
Consent.provision.type on the ROOT provision now carries the decision on every Consent profile in
this guide. permit grants, deny restricts, and status says only whether the record is
operative.
Read
provision.type.status = activemeans the record is in force, and nothing more.
Before this release the three families disagreed. Item consent carried its answer in status alone
-- active was yes, rejected was no -- while app consent expressed refusal as a nested provision
and a grant as the ABSENCE of one. That was survivable while those were the only two Consents in the
store. It stopped being survivable when a privacy directive arrived, because an active directive
means the patient said no: one element then meant opposite things across families sharing a
resource type, a scope and a search, and a consumer that read status first read a lockbox as
permission.
Two smaller faults went with it. A grant expressed as the absence of a nested denial is not a
statement, so a truncated or partially-written app consent read as a grant. And rejected is an R4
lifecycle code -- a party rejected the consent -- which is the wrong kind of thing to carry a
patient's answer.
R4's element text says type is "not permitted in root rule", and this guide does not follow that
reading. IHE Privacy Consent on FHIR and Ontario's Provincial Consent Override Interface both
constrain the root type to 1.., because a consent whose base rule states no decision states none
where a reader looks first. Following the prior art puts the decision where a PCF or PCOI reader
already looks. A consumer meeting a Consent with no root type MUST NOT infer permit; treat it as
malformed and fail closed.
This is also the shorter path to R5, which moves the grant/deny answer out of status into an
explicit decision element and drops rejected from the status value set. R5 is doing to Consent
what this release does to these profiles, for the same reason.
The Consent Family code system, required on category and
searchable, remains -- but it now answers only what a decision governs, not how to read it.
Break-glass is deliberately not a fourth family. It is an exception inside the patient's own
directive -- a nested permit with purpose = BTG -- following IHE Privacy Consent on FHIR, and the
record that somebody actually invoked it is an
AuditEvent following IHE BALP, with the
reason on agent.purposeOfUse from
consent override grounds. Those grounds are
TREAT, BTG and PATRQT, shaped by what primary care actually does: the ordinary legitimate
override is a covering clinician treating the patient in front of them, not an emergency, and a
vocabulary offering only emergency grounds produces a reason field everybody fills in dishonestly.
Where none fits, the purpose carries free text and no coding.
Notice of an override goes to the patient's most responsible provider as a
clinical Task under a new
privacy-notice bin in Task code -- distinct from
privacy-request, because a notice is raised by the system and carries no statutory clock.
Security labels, including the notice that a record has a gap¶
See: Security Labels,
Disclosure control labels,
Disclosure completeness labels
Disclosure control labels gives operational
meaning to four HL7 v3 ActCode RefrainPolicy codes on meta.security: NODSCLCD for a record
restricted from transmission, and NORDSCLCD for a record ingested under a consent override, which
must be identifiable as usable only for the purpose it was collected for.
Disclosure completeness labels carries
REDACTED, and it exists because suppressing information is only half of a lockbox. Where a
custodian discloses for the provision of health care and the individual has restricted part of the
record, PHIPA obliges the custodian to notify the recipient that what they received is incomplete.
A receiving clinician who cannot see the gap reads the record as whole and treats the patient on that
basis, which turns a privacy choice into a clinical hazard. REDACTED rather than SUBSETTED
deliberately: SUBSETTED carries the usage note that it is not suitable where information was
removed for security reasons.
Every code in both value sets is already a member of R4's security-labels value set, so a resource
carrying one is structurally valid against base R4 without this guide. A label declares an
obligation; it does not implement one. A producer must apply the label wherever the stated
condition holds, and a consumer that cannot honour a label it encounters must refuse the disclosure
rather than proceed without honouring it.
These pages state requirements, not status. Every page in this section, and the Privacy
Directive and Statement of Disagreement profile pages, say what a conformant implementation must
do on each surface -- which paths a directive has to be evaluated on, when a label has to be
applied, when a disclosure has to be refused, and where the specification itself stops (no
criteria-based data selector, no element-level segmentation, no status code for a released access
request). The obligations are unchanged; they are stated as the specification rather than as a
snapshot of one deployment, and are therefore still accurate next release. No profile, extension,
value set or code changes shape.
Access requests, the response clock, and privacy agreements¶
See: Access Requests,
Consent, Privacy and Record Integrity,
Corrections,
Consent Override,
clinical Task,
Task code
This is documentation. No profile, extension or value set changes shape here, and no code is minted; the only source change is comment text on elements that already existed.
An individual's right of access to their own record has a page. The request is an ordinary
clinical Task in the same
privacy-request bin as a correction -- that code covers access by its own definition, because it is
named for the right being exercised rather than the outcome -- with Task.focus naming the responsive
records and Task.statusReason carrying the provision relied on where access is refused. Access is
also the precondition for a correction request, and the two pages now say so and link.
The statutory response clock is Task.restriction.period. Its end is the due date, per the base
element definition, so a privacy request's deadline sorts in any worklist that already sorts on due
dates and no privacy-specific element is needed to hold it. Which duration applies is a question for
the governing legislation and, for records held by another custodian, for that custodian; where the
date goes is not. Do not write it on Task.executionPeriod, which means when the work is being or
was done.
An access decision has no status vocabulary yet. The
correction business status codes carry over for
intake (queued, in-review, waiting-for-information, requester-cancelled) and for refusal
(denied), but the terminal states describe a record being amended and an access request does not
amend a record: nothing in the vocabulary says released, or released in part. No local code is
minted for it. What would settle it: an access vocabulary published elsewhere and adopted the same
way this one was, or a release path whose own states are worth naming.
Regulatory reporting is a query, not an artifact. Counts of requests, and of refusals with the
provisions invoked, come out of the request Tasks grouped by Task.businessStatus -- refusals are the
denied bucket, the provision is on statusReason, Task.code selects privacy work and
Task.authoredOn bounds the period. Because those buckets are HL7's published vocabulary rather than
a private status column, the report is portable: a denied count means the same thing in any
store carrying the vocabulary, with no mapping table in between.
Watermarking belongs to the export layer. A watermark is a property of a rendered output rather
than of a resource -- the same content released to two recipients carries two different watermarks --
so nothing is modelled for it. What travels with a released packet is the
Provenance of the assembly and the
REDACTED completeness marker on the
packet's meta.security.
Accepting a privacy agreement is a Consent, and this guide does not model it yet.
consentcategorycodes#npp -- "Notice of Privacy Practices", defined as acknowledgement of a
custodian's notice of privacy practices -- is HL7's code for that family, so nothing needs minting.
Presenting a notice is an event and belongs in
AuditEvent; the patient's acceptance or
refusal is a standing decision and belongs in a Consent stating it on provision.type, like every
other family here. No profile is added and no code is added to
Consent Family in this release: a family code in a
required, searchable element with no profile behind it would carry no contract for reading the
resource. What would settle it: a surface that captures the answer, and a consumer that reads it.
A clinic can define its own consent items, and a consent can expire¶
See: Item Consent,
Statements of Disagreement,
Item Consent,
Statement of Disagreement,
Consent Item
Clinic-defined consent items, without a vendor catalogue of them. No code is added to
Consent Item for photography, program contact or a student
sitting in, and none will be: a vendor-maintained list of every consent a clinic might invent is
incomplete on the day it ships, and each addition costs the clinic a release it has to wait for.
Instead item consent gains a second
category slice, clinicDefined, carrying LOINC 59284-0 "Patient Consent" as its marker plus
a tenant-scoped coding with the clinic's own code and a human-readable text.
The marker is chosen rather than minted, and it earns the job twice: it is a member of R4's own
consent-category value set, so a clinic-defined item consent satisfies a base extensible binding
that a nexus-consent-item coding cannot, and it is the single category IHE Privacy Consent on FHIR
puts on every consent it profiles -- so an outside reader that knows nothing of this guide still
recognises the resource as a patient consent. It identifies no item on its own, which is why the
tenant coding and text are both required beside it: a tenant code system is not resolvable by an
outside consumer, so without text there is nothing to render.
category[item] and category[clinicDefined] are both 0..1 and the new
item-consent-item-is-identified-once invariant (error) requires exactly one. Carrying both is
two answers to which item is this?; carrying neither is a decision the lookup search never returns,
which is worse than no record because staff believe it was captured.
The search that lists a patient's item consents has to ask for both markers now. A
clinic-defined coding is not in the catalogue system, so category=<catalogue>| returns none of
them and every clinic-defined item reads as Not Recorded. One token search with two values covers
both: category=https://fhir.apps.health/CodeSystem/nexus-consent-item|,http://loinc.org|59284-0.
A tenant MUST NOT reuse a catalogue code in its own system. The recommended deterministic
identifier is {patient-id}:{item-code} with the code system dropped, so a clinic-defined item coded
sms computes the governed sms item's key exactly. The next conditional update on either matches
the other and versions it: one consent silently replaces another, both resources are individually
valid, and nothing reports it. Nothing in this specification detects the collision and nothing can --
a profile cannot validate a tenant catalogue it has never seen -- so the control is a naming rule
where the tenant catalogue is authored. Writers that read-then-update by logical id are not exposed
to it, the one case where that path is safer than the conditional update.
A consent item can now be time-bounded, on provision.period, and an elapsed period.end is
not permission. A consumer reads the period as well as provision.type. Nothing filters an expired
consent out of a search result and status stays active, because the record is still an operative
record of what was decided -- so a reader that finds permit and stops will email a patient whose
consent ran out last year, with an audit trail showing it had permission. Outside the period there is
no answer whichever way the decision points: a lapsed permit is not permission and a lapsed deny
is not a refusal to rely on.
A lapsed consent is not a fourth display state. It maps onto the state the model already has for
there is no answer here -- the one a missing Consent and an entered-in-error Consent map onto:
unanswered in the UI, a no at the gate, a prompt to ask again. It is emphatically not Declined, since
the patient never refused. No invariant is added for expiry, deliberately: a resource whose period
has ended is an accurate record rather than a malformed one, so an error would be wrong, and a warning
would fire on every correctly-recorded lapsed consent and on every historical version that was in
force when written, which turns the guide's warnings into noise. R4's per-1 already refuses a period
whose start is after its end.
policy.uri is documented as the home of the consent WORDING, and should be versioned. No element
changes shape: policy.uri has been required on item consent since it replaced the retired
policyRule pin earlier in this release. What it is for is now stated. A consent is an answer to a
specific piece of text, and the text changes; a URI naming the policy but not its version cannot
answer what did this patient actually agree to. Re-wording an item therefore produces a new
policy.uri, not an edited one, and the old URI keeps resolving for the patients who answered it.
This matters most for a clinic-defined item, whose text no published catalogue holds. Nothing enforces
it -- the element is a URI, and this specification cannot tell a versioned one from an unversioned one.
A statement of disagreement can point at an element, not just a resource. about on
Statement of Disagreement accepts
HL7's targetPath extension (0..*, from the
hl7.fhir.uv.extensions.r4 dependency this guide already pins), naming the disputed element as a
FHIRPath expression -- Observation.value.ofType(Quantity) rather than the whole Observation. Put it
on every reference to the record, versioned and unversioned alike, for the same reason both
references exist: a focus carried on only one is lost to whichever consumer reads the other.
Two upstream limits travel with it. The extension is draft, so expect its definition to move. And
its FHIRPath is the simple subset with .resolve() additionally disallowed, so it addresses within
one resource and cannot reach across a reference. Neither narrows the obligation: it still attaches
to the record, and a release path that cannot honour element-level targeting carries the statement
against the whole resource -- coarser, and correct. Dropping the statement because the path could not
be resolved is the one outcome the right exists to prevent.
FHIR-466.
Breaking changes¶
See: App Consent,
Item Consent,
Communication
Consent.policyRuleis no longer fixed tocricon app consent and item consent; both now requirepolicy.uriinstead.consentpolicycodes#cricis 45 CFR 46 -- the United States Common Rule for informed consent in human-subjects research. It is not a communication-preference policy, not a data-sharing policy, and not Canadian, and the code system it belongs to contains no Canadian concept at all: its 27 codes are US federal and state instruments plus three European ones. Rather than substitute another wrong code, both profiles now name the actual governing policy by URI, which satisfies R4'sppc-1invariant and is jurisdictionally accurate.Consumer action: a writer that emitted the fixed
policyRulemust emitpolicy.uricarrying the applicable policy instead; a reader that keyed oncricto recognise a Nexus consent must key oncategory. Every shipped example is migrated in this release.
Communication.senderand.recipientnow admitRelatedPerson. Rights over a record are exercisable by an individual and their substitute decision-makers, and a proxy must not be flattened onto the Patient: where a right is exercised by proxy the record has to show that authority was delegated, because that is what gets examined when the exercise is challenged.Consumer action: a reader that switched exhaustively on the reference target type of
senderorrecipientwill meet a type it has not seen. Widening a reference is breaking for consumers even though it is additive for writers.
- Item consent moves its decision from
statustoprovision.type, and stops writingrejected. Provided is now (status = active,provision.type = permit); Declined or withdrawn is (status = active,provision.type = deny).provisionmoves from0..1reserved-and-unused to1..1with a requiredtype.Consumer action: a reader that tested
status = 'active'for Provided andstatus = 'rejected'for Declined must readprovision.typeinstead. A reader that treated any non-activestatus as "not permitted" now seesactiveon a declined item and will read it as permission unless it changes. Every shipped example is migrated in this release.
- App consent moves its decision to the
root
provision.type. Refusal was a nested provision withtype = deny; a grant was the absence of one. Both are now stated onprovision.typeat the root, which is1..1.Consumer action: a reader that walked the provision tree looking for a nested
denymust read the rootprovision.type. A writer that expressed a grant by omitting the nested denial must now statepermit.
QuestionnaireResponseis now listed in the server capability statement. The profile has declared read-only SMART exposure since the release that gave every scope a reachable permission, but the statement did not list the type, so a scope naming it resolved to nothing and an application asking for it was refused. Read-only: an application that could rewrite a response could rewrite an answer after the fact.Consumer action: none for existing clients. An application that previously had
questionnaireresponse:readrefused may now be granted it.
FHIR-465.
Consent.category[item]moves from1..1to0..1. It is no longer the only way an item consent identifies its item: a clinic-defined item carries no catalogue coding at all, and identifies itself on the newclinicDefinedslice instead. The newitem-consent-item-is-identified-onceinvariant (error) keeps exactly one of the two slices present, so no resource is left identifying nothing.Consumer action: a reader that reached straight for
category[item]and assumed it was present must handle its absence, and must read the item fromcategory[clinicDefined]-- the tenant coding for the code, andtextfor the label -- when it is the slice in play. A reader that treated the catalogue coding's absence as malformed will now reject valid item consents. Separately, a consumer that LISTS a patient's items must addhttp://loinc.org|59284-0to itscategorysearch or every clinic-defined item will read as Not Recorded. Writers are unaffected: a governed item consent is written exactly as before.
FHIR-466.
Document status reasons, salient fields, and form-template field mapping¶
See: DocumentReference,
MedicationStatement,
MedicationDispense,
FamilyMemberHistory
Carried in [Unreleased] before the privacy work and shipping with it. Additive throughout.
DocumentReferencegains a status reason: the newnexus-document-status-reasonextension (0..1, free text) is sliced onto the profile asstatusReason, carrying why a document was markedsupersededorentered-in-error. R4DocumentReferencehas nostatusReasonelement, and the code alone cannot say whether a retired consult was replaced by a corrected copy or filed on the wrong chart. The reason rides the version that recorded it -- the store's version history is the audit of who changed the status, when, from what to what, and why -- and writers remove it on reactivation, so an active document never carries a stale reason. Always optional; a status change with no reason is a complete record. Additive only.
PSD-999.
-
MedicationStatement,MedicationDispenseandFamilyMemberHistorynow declare salient fields, so all three gain synthesized accessors and a Salient fields tab, and a consumer rendering them into a chart or an LLM context is reading the profile's own statement of what matters rather than the whole envelope. Until now these three declared none: they were reachable and validated, but had no reader surface, so they arrived as bare elements. The sets aremedication/status/dosage/effective/asserted/reason_codeon MedicationStatement,medication/status/quantity/days_supply/handed_over/dosageon MedicationDispense, andrelationship/status/date/condition/deceasedon FamilyMemberHistory. The two medication sets deliberately mirrorMedicationRequestwhere they overlap, so one drug vocabulary covers prescribed, reported and dispensed. Additive only -- no element, cardinality or binding changes, and nothing a current consumer reads moves.Two of them collapse a repeating element and say so in their
comment:conditionis the FIRST recorded condition's code, anddeceasedreads only the boolean arm ofdeceased[x], so a relative whose death is recorded as an age or a date leaves it absent. Absent means "not stated in that form", never "no".
FHIR-355.
Appointment.participant.typeis documented and Must Support, with the participation type each actor carries —PART(Participation) on the patient,PPRF(primary performer) on the provider, both inside the boundencounter-participant-typevalue set. Consumers outside this ecosystem read the type rather than the referenced resource type, and Ocean drops a patient participant that has none, so an untyped booking never reaches its reminders list. No constraint is tightened: the element stays0..*on the base extensible binding, and the four appointment examples now carry it.
No ticket yet.
Every reportable field now has an identity, and this specification is where it lives. Three new
code systems: Nexus Field carries 458
concepts, one per reportable field, coded <category>.<field>; Nexus Field
Category carries its 21 domains and Nexus Field
Subcategory the 73 groups inside them. A field
concept declares 26 properties and carries 24: where the field sits in the picker, which
element and profile realise it, its data type, cardinality and repeat semantics, the code and the
code system that identify it within its resource -- by name in clinicalCodeSystem and by canonical
URI in codeSystemUri -- its expected unit, the permission needed to read it, how strongly it
identifies a person, whether it is computed rather than stored, and the comparison family a report
builder should offer, and -- where the path alone does not identify the field -- which entry of a
repeating element it means (selector) and under what condition on the resource (gate). English
displays sit beside fr-CA designations.
Renamed before release: these three were nexus-reporting-criteria,
nexus-reporting-category and nexus-reporting-subcategory when they first landed on main.
They never appeared in a cut release, but they were on main for a day and were announced
internally under those names, so anything written against them in that window needs re-pointing:
the ids and canonical URIs above, and the criteriaCode key in reporting-catalog.json, which is
now fieldCode. Codes, concepts, properties and the <category>.<field> shape are unchanged --
only the three system names and that one key moved. The reason is that the concepts are reportable
FIELDS: a criterion is a field plus an operator plus a value, and these carry only the field.
nexus-reporting-picker-id and its ConceptMap keep their names, because they genuinely are the
frozen record of what the report builder's picker shipped.
One field's CONTENT changed, and it is not part of the rename.
demographics.sociodemographic-equity-set moved from pending to mapped: it gained
fhirPath: Observation.value[x] and an elm mapping from a source change already on main, and
the regeneration in this release is where that surfaces. It is named here because it was not
attributable from the diff, which otherwise reads as a rename touching a generated table.
As first generated it projected UNGATED. Observation.value[x] with no code discriminator writes
every Observation's value into the equity-set columns -- unlike occupation beside it, which gates
on LOINC 11341-5 -- and that is not a narrower value but a value belonging to a different question.
The projection now REFUSES it: the columns exist in the marts and stay NULL, with the reason named
in the generated header, under the rule that an entry reading Observation.value[x] with no usable
code is refused rather than emitted. Making the field project needs a code discriminator stated
upstream on the concept; it is not something the projector can decide.
codeSystemUri is the machine-readable half of clinicalCodeSystem, and it is deliberately
incomplete. A projection that filters on a coded criterion has to emit system and code
together; a display name like LOINC or CDS-S CT-012 Phone Number Type cannot be matched against
stored data, so a consumer had to keep its own name-to-URI table and every consumer kept a different
one. 104 of the 138 concepts that name a code system now carry its canonical. The other 34 do not,
because their clinicalCodeSystem names more than one system (CVX / Health Canada DIN, LOINC
(observation code); SNOMED CT (value)) or names a table with no established canonical (the CDS-S
concept tables, ICD-9-CA, CA Baseline birth sex). A guessed canonical is worse than an absent
one -- it produces a query that matches nothing and reports zero rows rather than failing -- so
where the system is not certain the property is absent, and absence means "not established", never
"none".
Before this there was no identifier for a reportable field at all. Consumers keyed on a domain
number and a prose name, or on ordinals minted from a design mock, so nothing could be joined and a
stored report survived a rename by luck. A code here is permanent. Adding one is routine;
removing or renaming one invalidates every stored report and cohort that carries it, so retirement
is status = deprecated plus replacedBy, and the release checks refuse a concept-level removal.
identifierClass is a first pass and is not yet a masking control. It says whether a field
identifies a person directly, narrows a population, or neither, which is what a de-identified export
needs to key on. It was assigned by rule and classified up wherever it was arguable. It has not been
through privacy review, and nothing should rely on it to mask until it has. The same caution applies
to the fr-CA designations: coverage is complete at 458 of 458, but none of it has been through
linguistic review, and complete is not the same as correct. That is why these systems ship as
draft.
Element prose was carrying the wrong things, and 21 profiles were corrected. short is a
one-line summary of what an element IS -- the same contract SalientField states for its own prose
-- and in 37 places it was carrying something else: a conformance statement ("SHOULD be populated
for originating writes"), a slice rationale, or an enumeration of the bound value set. Rationale
moved to comment, 15 of which are new. Enumerations were dropped outright: a consumer that wants
the values should expand the binding, and a description that recites them goes stale the moment the
value set moves. Six elements that inherited an enumerating summary from base R4 now state their
own.
Four elements on AllergyIntolerance
gained error-severity binding enforcement, and ingest that passes today can start failing. No
cardinality, binding, invariant or canonical moved in the FSH. What changed is which elements the
differential mentions: giving type, category, criticality and reaction.severity a short
put each of them into the differential for the first time, and the facade generator emits a
validation rule for every element the differential names, reading the rule off the snapshot. Each of
those four therefore acquired the R4 required binding it always had on paper as an error in the
generated TypeScript, Python and C# facades. The bindings being enforced are R4's own -- allergy |
intolerance, biologic | environment | food | medication, high | low | unable-to-assess, mild |
moderate | severe -- and enforcing them is intended. Consumer action: an
AllergyIntolerance that today passes validate() carrying a value outside one of those four sets
will now fail it. Check ingest that maps a source system's own allergy vocabulary before upgrading.
No other profile in this release gained a rule; these four are the whole change.
Five elements still summarise by enumeration, all on ServiceRequest and Consent. Both are
profiled more than once, so which profile a reportable field means is genuinely open, and stating a
summary would be picking one. That ambiguity is tracked where it belongs, in the catalogue's
elmCandidates, and is not resolved here.
Added¶
- A form template can say which of its fillable fields a chart value pre-fills -- the new
nexus-form-template-field-mappingextension onDocumentReference(PSD-1594): a list of rules, each naming a fillable field of the template's file by its AcroForm field name, verbatim, and the chart value that fills it, coded from the newnexus-form-chart-valuecode system (value set:patient.fullName,patient.firstName,patient.lastName,patient.birthDatewith its.day/.month/.yearparts,patient.gender,patient.phone,patient.addresswith its.street/.city/.province/.postalCodeparts,patient.socialInsuranceNumber,patient.healthNumberwith its.number/.versionCodeparts, andpatient.mrp, the most responsible provider). A form started from a mapped template opens with those fields filled from the chart and every value still editable; an unmapped field, or one whose value cannot be resolved, stays blank. Rules are keyed by field name rather than widget because a name can be drawn on several pages and every widget carrying it is filled. The binding is extensible -- a deployment may carry a key the set does not list yet, and a reader that does not know a key leaves the field blank. Only a template carries the extension: the newnexus-documentreference-field-mapping-only-on-templateinvariant refuses it on any other document. The system-provisioned WSIB Form 8 template ships mapped; its example carries the full rule list.
- The Reviewed extension's context widens to
DocumentReference: a document now carries who signed it off and when, on the resource the question is asked about, so a reader renders the checkmark from the document alone rather than joining to the review task. It repeats there — a document is assigned to reviewers individually, and each stamp is one reviewer signing off for themselves. Absent still means unreviewed.
- New Resource Note extension: an
attributed note on a resource with no
noteelement of its own, anchored atDomainResourceso every such resource gets the same thread the same way. Each repeat is one Task Note turn — author and time required, machine authors as identifier-only references — and the thread is append-only, read oldest first bytime. First use: the comment thread on a document, riding theDocumentReferenceitself, so reading the thread is reading the resource. Where a resource defines a nativenoteelement, that element remains the home.
- Task code gains
document-review: the marker a document review assignment carries, and the selector the document form lists a document's reviewers by — a task that merely focuses on a document is someone else's work item, and the form acts on what it lists (priority fan-out, reviewer removal). Distinct fromreview-report, the inbound triage stream.
- New Document Category code system (with its
value set) under the canonical base: the document form's
category picker, recorded on
DocumentReference.category— a workflow taxonomy for sorting a practice's documents, several of whose concepts (RX renewal request, administrative item) no standard terminology publishes; where one maps onto a CDS-S Report Class the definition says so. Optional on purpose: uncategorized is an ordinary state, and a document keeps any classification another producer stamped alongside this system's.
[1.25.0] - 2026-08-31¶
Generated by TypedFhir 0.15.0. Unchanged from 1.24.0; the library moves independently of this specification and did not need to move for this release.
Nothing here changes the specification -- no new profiles, no changed constraints, no moved canonicals. If you use the TypedFhir library directly, read the breaking note at the end of this preamble; otherwise there is nothing to do. Four pieces of work went into it.
The release process is rewritten. bin/ was shell and Python; it is 32 TypeScript files now,
and a release is five commands rather than a chain that had to be run in the right order. Cutting a
version moved inside the same command that checks the spec, so a failed cut leaves an ordinary
working tree instead of a stamped, unpublished one, and publishing lost both of the flags that
could skip verification. That put the HL7 validator off the default path -- 27,162 ms against
284 ms for the in-process one, which was essentially all of publishing's cost -- and once it was
gone the site build was the release, so it was profiled rather than guessed. The navigation turned
out to be 61% of every page: the largest reference page carried 519 KB of it and 1,701 links
before a byte of its own content. Two other plugins were doing nothing at all. The build went from
about 31 seconds to 10, the published site from 650 MB to 158 MB, and no URL moved --
sitemap, search index and the body of all 1,207 pages are byte-identical.
The multi-IG selector is gone. The library could be asked to validate against a named IG
version: nexus-v1.24.0, or a bare nexus aliasing whichever version was vendored. Nothing asked.
No service, no generator and no frontend passed one; the .NET port never implemented it; the only
caller in the tree was a fixture that existed to demonstrate the feature. What it cost was
bookkeeping: the version was written by hand in nine files, and four of them -- both registries
and both registry tests -- were there only to spell that key. They are gone, along with the check
that compared the hand-written key against the generated tables, which existed only because
hand-written version numbers drift. sushi-config.yaml is now the one file in the repository where
this version is written; everything else derives it.
The IG build is out of CI. A job there compiled the IG and ran the conformance suites against it. Those suites compare committed generated code against freshly compiled source, and between releases the two are supposed to differ -- generated code tracks the last release, the FSH source runs ahead toward the next, and they re-converge at the cut. So the job went red on contributions that were entirely correct, with nothing the author could do about it. The suites now run before a pull request, where the person running them knows whether the difference is intended.
The pages that came out of internal notes are rewritten. Several under Contributing and Features began as working notes and had never been rewritten for the people who now read them; they say what is settled, what is not, and what would settle it. Two that were missing from the navigation entirely -- and so published with no sidebar -- are in it. The site build is also stock MkDocs now: Material for MkDocs reaches end of life on 5 November 2026 and its successor supports neither of the two plugins this site was built on, so both were removed. Nothing here commits to that migration; it removes what would have blocked the choice.
One breaking change, and it is in the library rather than in this specification. If you pass
ig: to a TypedFhir ingest entry point, or read igKey / ig_key off a report, remove it -- see
Removed for the full consumer action. Callers that never named an IG, which is every known
caller, need no change.
Changed¶
- The site build is 43% faster and the published site four times smaller, from four Material
navigation features --
navigation.prune,navigation.instant,navigation.instant.prefetchandnavigation.instant.progress. Measured on this branch with the pre-build hook disabled, so the numbers are mkdocs alone: 24.3s -> 13.9s, and 650 MB -> 158 MB on disk. Once the validator is off the path (publish --fast), the site build is the release cost.The navigation was 61% of every page. Material renders the whole site's nav into all 1,203 pages, so the largest reference page shipped 519 KB of nav and 1,701 links before a byte of its own content.
pruneemits only the active branch plus its ancestors and siblings; a cousin profile collapses to one link. It touches nav markup and nothing else --sitemap.xml, the search index and the<article>of all 1,207 pages are byte-identical to the unpruned build, so no published URL moves and the URL and fragment gates are unaffected.What it costs and what pays it back. A cousin profile becomes a link rather than an in-place expander, so reaching a different profile costs a navigation instead of a free CSS toggle.
navigation.instantturns that navigation into an XHR content swap rather than a document reload,.prefetchstarts the fetch on hover, and.progressshows a bar past 400ms. Handbook pages render identically either way: whatprunestrips there is the reference tree that was in the DOM but hidden behind a tab.
- A second 29% off the site build by removing an extension and a plugin that did nothing:
include-markdownandcodehilite. 14.8s -> 10.5s, output byte-identical. Profiling the build rather than guessing is what found them:include-markdownranon_page_markdownacross all 1,203 pages scanning for a directive that appears in no page and never has (the includes in use arepymdownx.snippets--8<--markers), andcodehiliteis an alternative topymdownx.highlightrather than a layer under it, so registering both left the second one inert. Together with the navigation work above, a strict site build is now ~31s -> ~16s end to end.
- The site build is stock MkDocs now: no plugins beyond search, no
hooks:, nowatch:. Navigation moves from themkdocs-awesome-navplugin's per-directory.nav.ymlfiles to one ordinarynav:, assembled bybin/gen-nav.tsfrom an authoredsrc/nav.ymlplus the generated reference tree and read through mkdocs' ownINHERIT. Renderingdocs/fhir/moves from the pre-build hook intobin/site.ts. Output is byte-identical.We were using none of awesome-nav's features -- no globs, no sort, no flatten, no hide -- only its ability to split the nav across directories, while the reference tree that is 96% of the entries was generated regardless. Removing it produced an identical 1,207-page site.
hooks:and awesome-nav were the two things tying this site to MkDocs. Material for MkDocs reaches end of life on 5 November 2026 and its successor, Zensical, supports neither yet. Nothing here commits us to that migration; it removes what would have blocked it, and it costs one dependency and one Python file rather than adding anything.doc/fhir/bin/is now Python-free.What it costs, named.
mkdocs serveno longer re-rendersdocs/fhir/when the IG source changes, so a baremkdocs buildis no longer self-sufficient --npm run siteis the entry point, andnpm run site-kickrestarts the server after an FSH orsrc/pagesedit. Edits underdocs/live-reload as before.INHERITis resolved at config load, before any hook could run, so the nav could never have been written from a hook anyway; the build gate grew anav is currentcheck so a stale nav fails loudly instead of publishing pages with no sidebar, which mkdocs reports only as INFO.
check-blast-radiusresolves the base branch instead of assumingorigin/main. IG releases stack -- 1.23.0 -> 1.24.0 -> 1.25.0, each PR based on the last -- and againstmaina stacked branch inherits every unmerged parent's diff. This branch measured 1,687 changed files that way, of which 1,295 were a single 1.23.0 commit untracking the rendered pages and only 67 were its own. A reviewer reads a number like that as noise, which is the exact failure the gate exists to prevent. The base now resolves in order:--base, thenGITHUB_BASE_REF(set by GitHub Actions on a pull_request event, and authoritative there), thengh pr viewfor the current branch, thenorigin/main. Every path is printed, including the fallback, because a wrong base fails silently in both directions -- too far back and the diff drowns, too far forward and real changes stop being counted.
check-blast-radiusgains a declared-IG-consumer list, andNativeFHIR/seed-generated.shis its first entry. That script readsigVersionandsourceContentHashout of the generated meta to decide whether a cached seed dataset is stale -- it is the only one of NativeFHIR's eleven files that reads any of it, and IG releases have legitimately edited it before (1.20.0, and again this release after 1.23.0 deleted thesourceCommitfield it had been reading). The test is "does this file read the IG's generated metadata", not who owns it: ownership is what the gate prints so you know who to ask, and it moves. Declared consumers are counted and printed but do not fail the gate, because an exemption nobody can see is one nobody re-examines.
set-versionstamps one site it used to miss; sixteen other version stamps were deleted instead. Cutting this candidate found seventeen places still naming 1.24.0: fiveusing Nexus version '<v>'declarations and all twelve measurement-catalogObservationDefinition-*.jsonresources. Only one of the seventeen turned out to be worth keeping.Neither set was silent by accident.
ts/test/cql-model-version.test.tsexists because theusingbump "was made four releases running, and the fourth is the one that shipped";ts/test/measurements.test.tsrecorded the catalog stamp being walked by hand through 1.21.0 -> 1.20.0 -> 1.22.0 -> 1.21.1 with no check behind any of those moves. Both tests caught the drift again here -- they work. The first fix was to make all seventeen version sites so the stamps could not drift, which was the wrong fix in both cases: it grew machinery to maintain text that should not have carried a version.Four of the five
usingliterals are deleted. Each said some form of "sousing Nexus version '<v>'works with no further ceremony", where the claim does not depend on the number -- any version would do. They now sayusing Nexus. One was not merely stale but false: it claimedDM-01-nexusneedsusing Nexus version '<v>'when the fixture declares bareusing Nexus, and the next comment in that same file says so, adding "A literal here is one more site an IG bump retires." The survivor iscodegen/cql/gen-modelinfo.ts, where the number is the point: the sentence contrasts the stripped version against-candidate.
- The measurement catalog drops its
versionfield entirely, and says plainly that it is test data. TwelveObservationDefinitionresources that exist to exercise the CQL engine's unit-vouching -- that a unit-bearing threshold is refused unless something vouches for the convention. Nothing in production consumes them: no service outside TypedFhir imports the module, and every caller ofcompileToTriggerthat passes a catalog is a test or a test-golden generator. Defining measurements for production is expected to be a richer mechanism that has not been designed.The field tracked nothing and was read by nothing -- the catalog keys entries on
code|systemand never looks at it -- so it was noise with a maintenance cost.measurements.test.tsnow asserts the field stays absent, with the reasoning in the test, so it cannot creep back as a stamp.gen-measurements.tsno longer readsigVersion()at all, and the module and generator headers now say what the content is for rather than describing an unbuilt deployment/tenant resolution model as though it existed. See FHIR-102 for the related finding that these resources' canonicals are declared nowhere -- which this reframes: they are test fixtures, so the question is whether they should be in thefhir.apps.healthnamespace at all.
Fixed¶
- Two Handbook pages were orphaned from the navigation and are now in it:
guide/features/scheduling/converter-contractsandguide/features/scheduling/oscar-template-schema-notes, both FHIR-18/FHIR-19 working notes for the Oscar-side converter owners. A page absent from the nav renders with no sidebar in mkdocs-material, and mkdocs reports that as INFO rather than a warning, sostrict: truestayed green while they published unnavigable. Both are still marked Draft.
Removed¶
- BREAKING -- TypedFhir's multi-IG selector is gone. If you pass
ig:to any ingest entry point, or readigKey/ig_keyoff a report, remove it.fhirToResources,fhirToResourcesLenientand their Python twins no longer accept anigargument,DecodeReportno longer carries the key, andDEFAULT_IG/resolveKey/resolve_key/registeredIgKeysare no longer exported.getIg()takes no argument. Callers that never passed a selector -- which is every caller we can find -- need no change.Why. Nothing selected a second IG. The .NET port never implemented the selector at all; no service, no frontend and no generator passed one; and the only non-default consumer in the tree was the
synthetic-v1.0.0fixture, which existed to prove multi-IG selection worked. A capability whose sole user demonstrates that capability is not one.What it was costing. The
<igId>-v<version>key put the IG version into two hand-maintained registries and two pinned tests, and needed a key-vs-generated-meta agreement check to catch the drift those literals invited. Removing it removes all five.
- The IG version is no longer written anywhere by hand. There were nine version sites --
eleven occurrences across prose, worked CQL examples, the two registries and three pinned tests --
each walked forward every release, each having drifted at least once. All nine are gone:
ts/codegen/lib/ig-specs.tsderives from the compiled IG viaigVersion();ts/test/ig.test.tscomparesIG_META.igVersionto the same source rather than to a literal; the guide's worked criterion declares bareusing Nexus, as theDM-01-nexusfixture always did; andREADME.mdno longer pins a version.sushi-config.yamlis the only place a version is written, and the generated stamps are asserted against it.
docs/theme/js/tab-anchors.js, dead since 1.16.0. It activated ancestor content tabs so an inbound#inv-<key>anchor landed on content the reader could actually see; one page per tab made every view separately addressable and dropped the script'sextra_javascriptentry in that release, leaving the file orphaned and loaded by nothing. Confirmed unnecessary rather than merely unwired: across all 1,204 content pages 26,481 fragment links resolve to visible content, none missing and none inside an unselected tab, and exactly one page in the site -- the hand-written CQL artifacts guide -- still uses a content tab at all.
[1.24.0] - 2026-08-29¶
Added¶
- A machine-written
Taskdeclares WHO produced it, onrequester-- two new identifier authorities,NamingSystem/nexus-cds-rule(a decision-support rule) beside the existingNamingSystem/nexus-harness-graph(an AI-harness graph). Both are used the same way, as a logicalDevicereference:Reference.type = "Device"plusReference.identifierunder the producing registry's namespace, resolving to noDeviceresource on the server. A consumer telling producers apart now reads ONE declared field. (FHIR-90)Two authorities rather than one, because two separately owned registries mint these values and either could mint the same string. That is not only an attribution problem: producer identifiers feed the business keys below, and a key is SEARCHED and the resource it finds is UPDATED, so a shared namespace lets one producer's re-run overwrite the other producer's work item. The system names the registry that ISSUED the identifier, never the runtime that executed the rule -- a rule invoked as a tool from elsewhere keeps its identifier.
- A decision-support rule keys its work under its own namespace:
NamingSystem/nexus-cds-key, the exact counterpart ofNamingSystem/nexus-harness-keyand separate from it for the reason above. Same rule, and the same one way to get it wrong: the value is derived from WHAT THE TASK IS ABOUT (the patient and the gap), never from the evaluation that produced it. CDS Hooks supplies ahookInstanceand a card uuid, and both are per-firing values that mint a fresh key every evaluation. (FHIR-90)
- An attachment packet is a
List: the ordered set of chart items assembled to travel with something going out. New profileStructureDefinition/nexus-attachment-packetwithNamingSystem/nexus-attachment-packetfor its identity. Membership and order areList.entry,modeisworking,subjectis the patient. The host points at the packet with a reference element it already has: a letter uses oneComposition.section.entry, a referral usesServiceRequest.supportingInfo. Both areReference(Resource)in base R4, andServiceRequest.supportingInfois what Ontario eReferral profiles for referral attachments, so a packet is legal in each today and what this release adds is the sanction. (SD-175, SD-222)Why a
Listand not a document. R4 has a resource for exactly this shape --DocumentManifest, "a collection of documents compiled for a purpose together with metadata that applies to the collection", carryingcontent,recipient,relatedandsubject-- and it is REMOVED in R5, replaced byList. So the shape is right and the container it would ship in is a dead end. IHE MHD reaches the same place:IHE.MHD.Comprehensive.SubmissionSetconstrainsList, and MHD'sihe-intendedRecipientextension sits ON THAT LIST rather than on a document -- recipient belongs to the packet prepared for someone, not to the document, which is whyDocumentReferencegains norecipientelement here.Modelling the packet as its host's own sections was the alternative and it does not survive contact with the second host: a
ServiceRequesthas no sections, so an eReferral would have needed a second design, and so would every other host in the requirement.entry.itemis deliberately not type-constrained. Enumerating the kinds shipped today would reintroduce the per-type modelling this profile replaces; a consumer renders the kinds it knows and reports the ones it does not.
- A packet member can say which of its pages were left out --
StructureDefinition/nexus-attachment-packet-excluded-pageonList.entry, repeating, one occurrence per excluded page, 1-based. IG-defined rather than a house extension because a third-party consumer is a named future reader and can only implement what is published.One occurrence per page rather than a range expression (
"2-4,7"): a range is a second syntax this specification would have to define and every reader would have to parse identically to agree on what was sent, where a repeating integer needs no parser and a validator can check each value alone. Page numbers are meaningful only relative to the render they were taken from -- a different renderer may paginate the same member differently -- so they record what the assembler chose, not a durable re-rendering instruction. What the recipient received is the document stored on send.
- LOINC
28562-7"Chart section Set" joinsValueSet/nexus-composition-section, which is what an enclosures section codes to, and a newenclosurescode joinsCodeSystem/note-block-typeso an attachments block renders through the same dispatch as every other block.28562-7isSCALE_TYP = Set/CLASS = ATTACH.CLINRPT: a SECTION code. LOINC11503-0is the DOCUMENT code for the rendered packet PDF and belongs onDocumentReference.type, never in a section slot -- the distinction this value set's extensible binding exists to catch. (SD-175)
Changed¶
Task.requesteris the producer, and the two things it is NOT are now stated on the elements themselves.intentis not a machine marker -- humans raise proposals, and the element says what the owner is being asked to do.groupIdentifieris a DISPLAY GROUPING and nothing more: grouping is available to every producer, since a harness capability proposing several chart entries from one note is one evaluation raising a set exactly as a decision-support card is. A consumer that infers the producer from either, or from the ABSENCE of either, is reading a coincidence of current behaviour. (FHIR-90)
Task.identifier,Task.ownerand the Task Note profile name both producer families.owneradmits a rule as a logicalDevicealongside a graph, while the approve/resume undertaking stays the harness's own (assigning a task to a rule promises no resume), andnexus-note-agent-authorwas widened so a rule-authored turn is held to the same identifier-only shape as a graph-authored one. (FHIR-90)
DocumentReference.context.relatedis sanctioned as the BACK pointer from a rendered outbound artifact to the packet it renders and the host that packet travelled with. Ontario eReferral profiles this element the same way, so the pattern stays; what changes is that it stops being the ONLY link, now that the host carries a forward reference. Do not discover the relationship by searching it:relatedis not among the search parameters this server declares forDocumentReference, and an undeclared parameter is dropped silently rather than refused, so a search that appears to work may be returning the whole compartment. Resolve the reference. (SD-175)
Removed¶
NamingSystem/nexus-harness-keydrops its documented legacy CDS-connector key shape ({hookInstance}:{cardUuid}:{suggestionIndex}). It was per-firing, so it minted a fresh key on every evaluation and defeated the duplicate suppression the key exists for -- the one shape in that file that broke the file's own derive-from-the-gap rule. CDS keys now live in their own namespace (nexus-cds-key, above). Consumer action: a producer fed by CDS Hooks derives its key from the subject and the gap, never from the firing. (FHIR-90)
[1.23.0] - 2026-08-29¶
Added¶
- The Patient profile gains the Indigenous-profile fields. The Indian Act registration or status
number is a
Patient.identifierunder the Canadian URI Registry's official system (…/ca-indigenous-northern-affairs-number; 9–10-digit value or a data-absent-reason while other card facets are recorded first; the validity period carries the card's issue/expiry, and the newnexus-emr-indian-status-card-typeextension on the identifier carries the presented document form — SCIS, CIS, or TCRD). Three optional flat extensions carry the rest: Indian Act registration status (nexus-emr-indian-act-registration-status) and the First Nation registry group and community (nexus-emr-first-nation-registry-group/nexus-emr-first-nation-community), both coded from the curated seed-managed band list (ValueSet/nexus-first-nation-band, code = federal band number). Every field is independently recordable, records what the patient states (never verified against the Indian Register), and a recorded expiry does not imply a registration-status change.
- Form documents are marked, and found, by one identifier system — the new
NamingSystem/nexus-form(PSD-1230): aDocumentReferencecarrying an identifier underhttps://fhir.apps.health/NamingSystem/nexus-formis a form, its value a kebab-case slug naming the form type (wsib-form-8), and a patient's forms are enumerable with the standardidentifiersearch's system-only token — the marker-identifier conventionnexus-templateestablished, applied toDocumentReference.
- Form templates are marked by a sibling identifier system — the new
NamingSystem/nexus-form-template(PSD-1230): a subject-lessDocumentReferencecarrying an identifier underhttps://fhir.apps.health/NamingSystem/nexus-form-templateis the tenant-wide blank of a form type, its value the same slug its filednexus-forminstances carry, and a tenant's templates are enumerable with the same system-onlyidentifiertoken search.
- Form templates are searchable by title (PSD-1230): the server declares the standard
DocumentReferencedescriptionstring search, andDocumentReference?identifier=https://fhir.apps.health/NamingSystem/nexus-form-template|&description:contains={text}finds the templates whose title carries the text anywhere, case-insensitively -- so a template picker reads pages of the matches rather than the whole catalogue.
- Form templates are uploaded and deleted by a tenant (PSD-1230): an uploaded template is a
subject-less
DocumentReferencecreated with its PDF inline,descriptionthe title as typed, and thenexus-form-templatemarker's value the kebab-case slug of that title (WSIB Form 8->wsib-form-8) -- the form type every form later filled from it carries. A tenant holds one template per form type: an upload whose slug is already marked by a live (not entered-in-error) template is refused, so a client must probeDocumentReference?identifier=https://fhir.apps.health/NamingSystem/nexus-form-template|{slug}before creating. A template is removed with the standarddeleteinteraction unless it carries the readonly tag (https://fhir.apps.health/tags/document-reference/readonly|true, the system-provisioned templates) -- those are never offered for deletion. Forms already filled from a deleted template stay on their charts: they carry the form type by slug and reference no template.
- A C# consumer can ask, at runtime, which IG the package was built against.
TypedFhir.Facade.Generated.IgMetacarriesIgId,IgName,IgVersion,FhirVersion,Canonical,SourceContentHash,ProfileCount,ValueSetCountandInvariantCount. The TypeScript and Python ports have carried the same facts asIG_METAfor several releases; the .NET package had them only in a generated header comment, where no program could reach them. All three ports are now populated from one derivation, so they cannot disagree about their own provenance. (FHIR-93)
Changed¶
- A form template no longer has to name a patient, and every other document still does.
DocumentReference.subjectrelaxes from1..1to0..1, guarded by the new invariantnexus-documentreference-subject-unless-template: aDocumentReferenceSHALL carry a subject unless it carries an identifier underNamingSystem/nexus-form-template. A template is the tenant-wide blank of a form type and is about no patient, which is the shape the form-template guidance describes, and the profile previously required a subject on it. Read the cardinality with the invariant, not on its own -- for every document that is not a template the requirement is exactly what it was, and it is now stated in evaluable FHIRPath rather than left to a cardinality. A consumer that treatedsubjectas always present must handle the template case. Stated as one invariant rather than a second profile, because a template profile answers the question once per resource type. Both form systems now ship a worked example: a filed WSIB Form 8 and its template. (FHIR-94)
nexus-first-nation-registry-groupandnexus-first-nation-communitybindrequired, notextensible. The extensible binding permitted a code from any system while both definitions said a band outside the curated list could not be recorded;requiredis what the definitions describe. It costs nothing to record a band the list is missing, becauseValueSet/nexus-first-nation-bandis seed-managed reference data rather than an IG artifact -- membership updates ship as data and need no release, so a closed binding never becomes a release gate on recording a patient. (FHIR-94)
nexus-first-nation-communitynow states that the band list APPROXIMATES its domain. A band is a registry group; a community is a place and a people. One band may hold several communities and reserves, and a person can affiliate with a community whose band does not hold their registration. Both extensions bind the band list because it is the only curated Indigenous reference data this specification carries. Read acommunityvalue as the community the patient named, resolved to a band, and never as a claim about registration. (FHIR-94)
- The IG version is no longer stamped into the header comment of every generated file. It has
a structured home in each port --
IG_METAin TypeScript and Python,IgMetain C# -- and that is where a consumer reads it. Generated files carrying a version string drop from roughly two hundred to eight: the IG-table modules,Metadata.csandPatientCompartmentMap.cs, each of which has a better claim to naming its IG than a per-resource facade does. No API moves; the affected lines are banner comments. (FHIR-93)
Removed¶
- BREAKING for library consumers:
IG_META.sourceCommitis removed from@awaremd/typed-fhirand the Python package. Consumer action: readIG_META.sourceContentHashinstead -- asha256:<hex>digest of the IG artifacts the tables were generated from, which is what a provenance stamp is for and which this release does not otherwise change.sourceCommitwas derived from the source repository's commit log and was unsatisfiable by construction: it named a commit that did not exist until after the commit carrying it was made, so it could never be correct in a working tree. Nothing in the published surface read it. The field-dictionary manifest, the delta register and the seed-generator manifest move theirigblock fromsourceCommittosourceContentHashfor the same reason.Named as breaking here rather than signalled by the version number, per this changelog's own rule: versions are sequential, not SemVer, and the entry carries the compatibility contract. The consumer action is one field name. (FHIR-93)
[1.22.0] - 2026-08-26¶
Generated by TypedFhir 0.15.0. Unchanged from 1.21.1, but what it generates is not: the FHIRPath subset gained real membership, type-test and substring evaluation, so twenty-four invariants this guide states are now checked by the generated validators rather than reported as unverifiable. The same library version therefore produces a materially stricter client against this release than against the last one.
Added¶
-
Imaging Modality-- modality on the report, because the withdrawal ofImagingStudyleft the requirement with nowhere to go. Modality is anImagingStudyproperty in R4, and this guide no longer profiles that resource; the imaging capability that requires it (ON RFP 1.5.4.2) did not go away with it.0..* MSonDiagnosticReport, repeating so a PET-CT report says two rather than choosing.It describes the DOCUMENT, not an acquisition. Saying a report came from a CT asserts nothing about series, instances or SOP UIDs, and needs no PACS to be honest about -- which is what lets this estate record modality while still declining to model
ImagingStudy.DiagnosticReport.codeis not a substitute, and that is the objection worth answering. A human reader infers modality from "Chest X-ray"; a results list filtering "show me the CT reports" cannot, without maintaining a procedure-to-modality mapping outside the record and getting it wrong silently. An implied fact every consumer must re-derive is not a recorded fact.Bound extensible to DICOM CID 29 -- the same value set and the same strength R4 binds
ImagingStudy.modalityto, so a consumer that already knows R4's imaging model needs no second rule, and heritage feeds carrying local modality codes stay conformant.
Nexus Composition Type, for the document kinds LOINC cannot name. Three generated-summary kinds exist in production and a consumer has to tell them apart:nexus-daysheet-summary(written ahead of a clinic for a booked patient),nexus-interval-summary(what happened between two points in time) andnexus-encounter-recap(one encounter, after it). All three are LOINC 34133-9 as a CLASS and none is distinguishable from the others by any LOINC code -- and they are not interchangeable, since a recap asserts that the encounter it covers took place. Plusnexus-letter-template, so the retiredhttp://services.well.com/fhir/categories/letter-templatehas a home on this guide's base if one is still needed (templates areListas of 1.18.0, so check first).
-
Meta Updated By, an extension this EMR has been writing undeclared onResource.metasince before this guide described it. It names the user account that last wrote a version, so a version list can say who touched each row without fetching aProvenanceper row. It is a display convenience, not an audit record -- anything reasoning about authorship should readProvenance. The value is an identity-provider subject (auth0|...), NOT aReference: it names an account, not every writer has aPractitioner, and a dangling reference would be worse than a string a consumer can match on.Like the two note extensions above it publishes at this guide's base while production still writes
http://services.well.com/fhir/StructureDefinition/UpdatedBy. This one differs in a way that matters: it ridesmetaon every version of every resource ever saved, so the old address does not stop appearing when writers move -- it stops appearing on NEW versions, and history keeps it. Readers should plan to see both indefinitely and match on the URL suffix.
-
First Nation Bandsand theFirst Nation Band CodeSystembehind it -- a canonical this guide bound two extensions to and declared nowhere. The First Nation registry group and community extensions added in 1.21.1 bindhttps://fhir.apps.health/ValueSet/nexus-first-nation-band, and nothing published it, so a consumer reading either extension could not discover the value domain and the link did not resolve. The canonical is unchanged: it is the one the EMR already writes, declared rather than minted, so no storedCodingmoves.The content stays out (
content = #not-present). The membership is the Indigenous Services Canada band list, held as seed-managed reference data on the same footing as the curated problem and procedure lists, and it changes on that list's schedule rather than this guide's release schedule. This publishes the system's identity and leaves its content where it lives, so an expansion is available only from a terminology server loaded with that seed. Both bindings areextensible, which is the honest strength: a band absent from the curated list cannot be coded.The code is the federal band number and the display is the band name as registered. Band names beginning with a literal
?are the names as registered, not encoding damage, and sort before "A" under ordinal comparison. The Canadian URI Registry names the same registry for IDENTIFIER use as.../NamingSystem/ca-federal-firstnation-band-id; this system is the CODING view of the same registry, which is why the two URLs differ and both are correct.
-
Composition Block TypeandComposition Block Data, the two extensions a block-authored encounter-note section carries, together with the Note Block Type code system and Note Block Types value set the first of them binds. The note body is authored in a block editor -- an ordered stack of independent pieces, each with its own identity, heading and kind -- and the EMR has been writing that shape undeclared. A block is oneComposition.section:section.idis its identity,section.titleits heading,section.textits narrative, and R4's own ordering ofsectioncarries the sequence. A block raised and left blank is kept, with anemptynarrative and anemptyReasonofnotstarted, because the gap is part of what the author wrote.This is the block surface returning on the terms 1.14.0 set for Composition itself: there is a committed consumer. It is not the shape 1.11.0 withdrew. That one nested sections recursively and carried its kinds in a complex extension whose slicing was among the unevaluable-slicing defects the removal cleared; these are two flat extensions on an ordered, non-recursive
section. It does enumerate block kinds asnexus-emr-embedded-section-typesdid, and the difference is what is enumerated: one kind that has shipped, rather than four that had not.The narrative is authoritative, and that is the invariant to build on. Every block-authored section carries its content in
section.textwhatever its kind, so a consumer that ignores both extensions reads a correct, complete note. A block kind that puts clinical content in the structured value and nothing in the narrative is malformed however well it round trips.The block-kind vocabulary is
extensible, and a reader that meets a kind it does not know must write that kind back unchanged. Every block-authored section declares its kind,textincluded; a section carrying no kind at all predates the extension and reads astext, which is tolerance for data filed before block authoring existed rather than a default, and is retirable once no such section remains.Note Block Typesis how a kind becomes NAMED -- a writer whose kind is listed uses that code rather than a synonym -- but a kind the guide has not named yet is conformant. Rendering an unrecognised kind as text is correct; storingtextin its place is data loss. Both extensions aredraft, and the vocabulary grows as kinds ship rather than ahead of them.Both extensions publish under this guide's canonical base,
https://fhir.apps.health/StructureDefinition/.... They were first minted underhttp://services.well.com/fhir/..., the hostname three olderCompositionextensions in the same library already use, and were moved before publication -- the only moment that move was free, since no note carrying either had been filed. Published canonicals are forever, and from this release these are published.
-
Composition BillingandComposition EHR Share, two extensions this EMR has been writing undeclared. Billing raised at note closeout (the service code, the diagnostic codes supporting it, and units where the fee code is charged per unit), and one record per completed share of a signed note to an EHR outside the practice. Both were live on filed notes while this guide described neither, so a reader saw aCompositionprofile with no billing concept and no sharing concept at all.These are declared at this guide's canonical base, which is NOT the URL the software writes today. Production emits them under
http://services.well.com/fhir/StructureDefinition/...; they publish here underhttps://fhir.apps.health/.... The block extensions added above could simply be moved, because nothing had been filed against them yet. These have live clinical data behind them and cannot be moved by editing a constant, so the specification states where they belong and the implementation is knowingly behind it on the URL. Writers: write the canonicals declared here. Readers: expect both for as long as filed notes carry the old address. Matching on the URL suffix (composition-billing,composition-ehr-share) reads both; resolving only the canonical silently misses billing and sharing on every note written before the writers move.
Changed¶
-
Twenty-four conformance rules that the generated validators reported as unverifiable are now CHECKED at runtime, in all three ports. The unverifiable set goes from 36 to 12. Consumer action: a resource that carries one of these defects now raises an error where it previously produced an
invariant-unverifiablenote. Every one of them was already enforced by the HL7 validator, so a resource that passed a full FHIR validation is unaffected; what changes is that the in-process validator no longer needs a second tool to see them. Callers that cannot absorb a new error usevalidate()and triage the issues rather thanassertValid().Newly checked:
Appointmentapp-2/app-3,Basicapp-state-system-inv, the nineMedicationRequesttim-*dosage-timing rules,Organizationorg-1,Questionnaireque-1/que-10/que-13,QuestionnaireResponseqrs-1,RiskAssessmentras-1, the three "resolves to a contained Bundle" rules this guide states itself --Listnexus-template-entry-is-contained-bundle,Tasknexus-sugg-2andQuestionnairequestionnaire-extract-bundle-is-contained-- and the three "that contained Bundle is a transaction" rules beside them:Listnexus-template-bundle-is-transaction,Tasknexus-sugg-3andQuestionnairequestionnaire-extract-bundle-is-transaction. Also newly checked, and an element rule rather than an invariant:Compositionsection.text.status, which R4 requires at 1..1 and which nothing here checked -- a section written as{"text": {"div": "..."}}passed this library and failed a full FHIR validation. The profile adds no constraint; it restates the base cardinality so the generated validators can see it.No profile changed. These are the same published rules, checked where they were previously only reported. The compiled FHIRPath subset gained membership (
in), union (|),%resource,%ucum,substring(), numeric addition, boolean literals,trace(),(expr).not(), boolean equality, and the ability to evaluate a rule declared on an element below the resource root.What is still not machine-checked, and why, so the remaining gap is a stated one rather than an absence:
CareTeamctm-1needs reference resolution (resolve());Questionnaireque-2andquestionnaire-extract-is-bundle-onlyneed a recursivedescendants()walk;Observationobs-7,Conditioncon-3and the twoPatient*-instances-cleanrules need set algebra over projected values;Questionnaireque-7andRiskAssessmentras-2need element-level type tests (is/asagainst a datatype rather than a resource);Consent.categoryandEncounter.classbind value sets that cannot be expanded offline; and theDocumentReferenceattachment-content rule is verified at document assembly rather than per resource. All twelve are still enforced by the HL7 validator and by any conformant external validator, and all twelve are still reported asinvariant-unverifiableat runtime rather than silently skipped.
-
BREAKING: the block-kind binding is
extensible, and a reader must write an unknown kind back unchanged.Note Block Typesboundrequiredthrough 1.21.x, and 1.21.1 stated the consequence in as many words: the vocabulary was closed, and a client reading a section whose kind was ahead of its own read and rewrote that section astext, losing the kind. That rule is withdrawn. The binding is nowextensible, andNote Block Typecarries two more codes besidetext--merge-field-body(the letter composer) andcited-claims(the summary graphs).Why it changed.
requiredmade this guide a release gate on the editor: ship a block kind and every note carrying one is nonconformant until the IG cuts a release, which is not a dependency a specification should impose on the software it describes. It also did not protect the record. A reader that cannot represent a kind writes the section back astext, so closing the set converted "a kind this guide has not named" into "a kind the record no longer holds".Consumer action: a reader built to 1.21.1's rule must stop rewriting an unrecognised block kind as
text. Render it as text -- there is no widget for a kind you do not know, andsection.textis authoritative, so nothing clinical is lost -- then write the code back unchanged. Display and storage are different decisions. A reader that keeps collapsing unknown kinds destroysmerge-field-bodyandcited-claimson every round trip. Rendering an unrecognised kind as text is correct; storingtextin its place is data loss. A writer whose kind is named in the value set uses that code rather than a synonym; a writer shipping a kind the guide has not named yet is conformant carrying its own.
- The block-type extension is the RENDER axis, not the authoring tool -- and this corrects how
1.22.0 described it earlier in its own development. A section answers three independent questions
and each has its own element: what it is clinically (
section.code), who produced it (section.author, which already admitsDevice), and how to render it (composition-block-type). Read the last as "which editor widget wrote this" and a letter and a generated summary have no kind at all, and a consumer is forced to infer rendering fromauthor, which answers a different question. Keeping the three apart is what makes an encounter note, a letter and a summary the same object: a problem-list section is 11450-4 whoever produced it, and a claims section renders identically however it was assembled. No constraint changed -- the definitions did.
composition-block-datacarries anAttachment, not a JSON string.contentTypemakes the envelope readable even where the contents are not described here; a JSON string in avalueStringis self-describing to nobody, and no published international IG carries structured state that way. Reach for SDC first: where a render kind is really a form (vitals, diabetes review, flowsheet), aQuestionnaireplus aQuestionnaireResponseis the modelled answer and getssection.entryfor free. Consumer action: a writer emittingvalueStringmust move tovalueAttachment. Nothing is filed against this extension yet. Also states plainly that unreadable is not absent -- a writer that cannot parse a stored value must round-trip it untouched rather than drop it.
-
BREAKING:
Composition.typeandComposition.categoryswap roles.typeis the CLASS and the element to switch on.type(1..1) bindsComposition Classificationextensible and carries exactly one of three LOINC codes:34109-9"Note",34133-9"Summary of episode note",51852-2"Letter".category(0..) bindsComposition Kindpreferred* and carries the refinement underneath -- which kind of note (11506-3,11488-4,18842-5,34117-2,34748-4), which kind of summary (the codes innexus-composition-type).Composition.section.codebinds the newComposition Sectionextensible, unchanged.Earlier drafts of this release had these two the other way round, and told readers to classify on
category. That was wrong, and the reason is structural rather than a matter of taste:typeis 1..1 in base R4 and cannot be relaxed, so it is the only element guaranteed present on every Composition. The one question every consumer must be able to answer -- am I rendering clinician-authored content or machine-generated content -- has to live there.categoryis 0..* and may be absent, which makes it the right home for refinements a producer may or may not know, and the wrong home for the distinction nobody can afford to miss.Consumer action: switch on
type, and search on it. A search for every summary a patient has, of every kind, istype=http://loinc.org|34133-9. Readingcategoryalone will miss documents.Producer action: write the class on
typeand the refinement oncategory. A progress note istype= 34109-9 withcategory= [11506-3]. A day-sheet summary istype= 34133-9 withcategory= [nexus-daysheet-summary]. Converters need remapping: OSCAR sends LOINC 11488-4 "Consult note" on everycasemgmt_notewhatever the note is, and that is a refinement code -- it belongs oncategory, with the real class written totype.Repeating the
typecode oncategoryis allowed and is the ordinary thing to do where a document has no kind finer than its class. The refinement set is a strict superset of the class set so the repeat always validates, and a build gate enforces that nesting rather than leaving it to prose.section.codeis bound for the first time, andextensibleon purpose. LOINC supplies both document codes and section codes out of one system in one format:34109-9("Note", a document) and8716-3("Vital signs", a section) are indistinguishable as data, so a document code in the section slot is a mistake the format itself cannot prevent and no reader can detect after the fact. Preferred would document the vocabulary and warn on nothing; extensible warns on a code outside the set, which is exactly what a document code there is. A legitimate section kind the set does not yet name will also warn -- accepted, because growing the set is one line and the alternative is catching this by eye forever.The strengths are asymmetric on purpose, and the stronger one sits on the smaller vocabulary.
typeis extensible because its set enumerates the class axis completely -- note, summary, letter is the whole space -- so a code outside it is nearly always a refinement written into the class slot, which is worth a warning.categoryis preferred because LOINC's document axis is large and no set here enumerates it: a producer holding a more precise code should use it without being penalised for the precision.
- The narrative/structured contract is written down on
Composition, and it is the most load-bearing rule in the profile. A section carries its content twice on purpose: the structured data is the display path for this EMR's UI, the narrative is for everyone who is not that UI. A renderer shows the structured data and ignores the narrative -- nothing appears twice. The consequence is that both sides must be complete: anything the UI needs must be structured, and anything clinically meaningful must also be in the narrative, including negatives. "No active problems" is a finding, not filler. Booleans and scalars count -- a value living only inside a structured payload leaves every consumer who is not this UI reading an incomplete document.
- Imaging date and modality follow the withdrawn ImagingStudy profile.
imaging dateremaps toDiagnosticReport.effective[x], which is the study date for an estate that models the report and is populated whereImagingStudy.startednever was.imaging modalitymoves to the newImaging Modalityextension above, minted in this release for exactly this reason: R4's DiagnosticReport carries no modality element, and the imaging capability asks for one explicitly.
-
BREAKING (TypedFhir):
Composition.sectionreads as aSectionsvalue type, andComposition.proseis gone. The note body is not a scalar.sectionwas handed over as raw JSON in all three ports, and the Python facade additionally carried aproseaccessor readingsection.first().text.div-- the FIRST BLOCK of a block-authored note, silently dropping the rest. That is a plausible, non-empty value that is a fraction of the record, and it got worse the more a provider wrote; a four-section SOAP note filed before block authoring existed had the same defect and returned Subjective alone.Both are replaced by a typed collection in the shape of
Notes, in which a section IS an authoring block: it carries the block's identity (section.id), heading (section.title), narrative and narrative STATUS (section.text), clinical code (section.code), authorship, entries, render kind (composition-block-type) and editor state (composition-block-data) as typed members, and appending returns a NEW collection.Consumer action, reading the whole body:
composition.section.body-- every section's narrative, in stored order, joined, derived where a reader can see that it is derived. Consumer action, writing: buildSectionvalues and assign, orcomposition.section = composition.section.append(block). Replacing ONE block's narrative and leaving the others alone issection.replacing(id, ...), and it is the operation the removed setter could not express: it wrotesection[0].text.divand nothing else, so on a multi-section note it overwrote the first block and orphaned the rest, and it omittedtext.status, which R4 requires.Sectionscannot express that mistake -- a narrative and its status are set together or not at all, andsection.text.statusis now checked by the generated validators (below).Nothing had adopted
prose. It existed in the Python port alone -- the TypeScript and C# facades emit no salient accessors -- and it was read-only for the length of this release only. In those two portssectionchanging from raw JSON toSectionsis a new breaking change in ports that had nothing to break, which is the right change and is stated rather than absorbed here. The chart attribute namedproseis unchanged: it is the salient chart name,DiagnosticReportpublishes the same one, and only the accessor behind it moved (toComposition.body, which is read-only for the reason above).python/examples/write_a_summary.pyshows the write path.
- The whole typed accessor surface now says which of its fields can be WRITTEN.
WRITABLE_FIELDS(a static set, also reachable from an instance) publishes the accessors that accept an assignment. A caller holding a polymorphically wrapped resource previously had to introspect the generated class for a property with a setter to find out -- reading the implementation to answer a question the API should answer. Fifteen accessors across the guide are withheld, all of them derived through a read that cannot be reversed; in Python, assigning one now raises aReadOnlyFieldErrornaming the field and the reason, in place of the built-inAttributeErrorthat named neither.ReadOnlyFieldErrorsubclassesAttributeError, so existingexcept AttributeErrorhandling is unaffected.
-
BREAKING (TypedFhir):
Composition.categorydoes not accept a bare code;Composition.typedoes. The rebinding above movedcategoryontoComposition Kind, which unionshttp://loinc.orgwith this guide's own composition-type system. A bare code has no unambiguous expansion across two systems, so the generated facades withhold the bare-code shorthand on that element rather than guess which system was meant.Consumer action: name the system on
category.composition.category = ["11506-3"]becomes[new Concept([new Coding("http://loinc.org", "11506-3")])]in TypeScript,[Concept(codings=[Coding(system="http://loinc.org", code="11506-3")])]in Python, and the same explicitConceptin C#. All three ports refuse the bare code at the call site. Note that a REPEATING coded field keeps a permissive static type in TypeScript, so the refusal there is a runtime one rather than a compile error -- unlike a scalar field, which narrows.Composition.typetakes a bare code, because its value set draws on LOINC alone:composition.type = "34133-9"writes the system for you. The discriminator is the system count of the bound value set, not the element and not the release.
Composition.section.codeis what a section clinically IS, and the block-type extension is a different axis. Where a section has a clinical meaning it carries it here: a vital signs section is LOINC 8716-3. It is not a place for a document-type code (LOINC 34109-9 "Note" types a document, not a section), and it is not interchangeable withcomposition-block-type, which says how to RENDER the section rather than what the section is. C-CDA on FHIR and IPS both makesection.codethe element a consumer navigates by; a note whose only statement of what a section is lives in a proprietary extension cannot be read by a consumer that does not implement this guide. The binding that now enforces this, and the reason it isextensiblerather thanpreferred, are in the vocabulary entry above.
- The block extensions' pages said the canonical was unsettled. It was not, and they now say
so.
Composition Block TypeandComposition Block Datapublish underhttps://fhir.apps.healthand have since before the first note carrying either was filed. The FSH commentary rendered on their pages still described the URLs asservices.well.comand framed the base as an open question, contradicting the declaration on the same page. Their descriptions also carried anAWAITING REVIEWnote saying a rename was likely -- which stopped being true once the canonical published, since published canonical URLs are forever. Both now say the canonical is settled. No definition changed; if you read those pages before this release, re-read them.
- Where the
services.well.comnamespace goes is IN SCOPE for 1.22.0 and is not settled by the two extensions above.Compositionis not the only resource in this estate carrying identifiers and extensions under that host, and the two declared this release are the subset that could be ruled on without moving data. The general question -- which of those artifacts this guide adopts, which it redeclares at its own base, and what a migration costs where records already carry the old address -- is the larger piece of this release and is not answered by this entry. No consumer action follows from this note. It is here so that a reader who finds aservices.well.comURL on the wire knows the guide is aware of it rather than silent on it.
- Whether a block's structured value belongs in an extension at all is NOT settled. The shape of
the value is:
composition-block-datacarries anAttachment, and the consumer action is in the entry above. What remains open is the element -- an extension, orComposition.section.entry. What would settle it is the first block kind that promotes its own facts toentryon save, a vitals block being the obvious candidate: the editor state is whatever the widget needs, and the promotedObservationis what a consumer querying the chart actually wants. The question is stated on the extension's own page. No further consumer action follows from this note -- a writer that follows the entry above is correct either way, because a later move tosection.entrywould add a place to read the facts rather than change where the editor state lives.
-
BREAKING: the package publishes as
health.apps.nexus. It wasnexus, andnexus-emrbefore that. The old id was never publishable in the first place: the FHIR NPM Package Specification requires a name of "two or more namespaces separated by a dot", and a barenexusis one -- so every release to date has carried an id no registry would accept. That stayed invisible only because this package is handed out as a tarball rather than resolved by name, and it stops being invisible the moment it is published anywhere. The namespace is the reverse of this guide's own canonical host;fhiris deliberately not repeated as a segment, since the canonical already carries it and every package in a FHIR registry is FHIR. The value is on IG metadata.Consumer action: a dependency line reading
nexus: 1.21.xbecomeshealth.apps.nexus: 1.22.0. FHIR package caches key onname#version, so this does NOT present as an upgrade -- a cachednexus#1.21.1and a fetchedhealth.apps.nexus#1.22.0are two packages that coexist happily, and a consumer who bumps only the version keeps resolving the old one with no error. Change the name, not the number.Nothing is retroactive. Every already-published version keeps the id it published with, because rewriting one would break resolution for anyone depending on it. The site's download filenames are unchanged and still read
nexus-<version>.tgzfor every release: that stem is a site convention, deliberately not the package id, so a filename that does not match the id is expected here rather than a mismatch worth reporting.Package ids are permanent -- a published id keeps resolving for whoever depended on it -- so this moves once and is not expected to move again. It moves now because it has to move before a first registry publication rather than after one.
Removed¶
-
BREAKING:
NexusEmrCoreImagingStudyis withdrawn, andDiagnosticReport.imagingStudyis no longer must-support. The profile, its intro page and its example are gone, andDiagnosticReportleaves the element at its base R4 definition.Why: nothing produces it. ImagingStudy carries series and instance identifiers, modality and SOP references that only a PACS can mint. This estate models the imaging REPORT -- what a results viewer actually renders -- and no system in it holds DICOM acquisition metadata. So the profile described a shape with no producer, and the must-support flag required consumers to support a reference that is never populated. Keeping a first-pass profile published invites implementers to build against a shape this guide is not committing to, which is the same reasoning that withdrew the experimental clinical-document surface in 1.11.0.
It also cost something concrete. Because the specification asserted ImagingStudy and the corpus could not honestly synthesize one, the gap reappeared on every coverage audit and had to be suppressed by a standing exclusion in the seed generator. The exclusion suppressed the symptom in one report; it did not retract the claim, and the claim is what kept resurfacing.
Consumer action. A consumer resolving
DiagnosticReport.imagingStudyshould stop expecting it to be populated by anything in this estate, and no longer needs to support the reference for conformance. A report that genuinely originates from a system holding studies MAY still carry the element -- it survives at base R4 -- but this guide makes no claim about it. Imaging content is on the report:conclusionfor the impression,conclusionCodefor the coded conclusion, andpresentedFormfor the body.15 published URLs are removed and will 404: the profile page and its element, example, FSH, invariants, SMART and TypedFhir-API tabs. This is the second canonical removal in this guide's history and it is deliberate. Anything linking to them should link to the DiagnosticReport profile instead.
[1.21.1] - 2026-08-26¶
Generated by TypedFhir 0.15.0. Unchanged from 1.21.0: the specification below is unchanged in shape, and the client library's own additions in this release are new opt-in packages rather than a change to what it generates.
Added¶
-
The CQL toolchain is now embeddable from .NET and Python, so the chain this specification documents -- CQL in; validated ELM, a module-definition
Libraryand a manifest out -- runs in-process from all three client languages with no service to deploy. One translator is embedded rather than reimplemented per language: every additional engine that can decide what a criterion means is a new authority on clinical semantics, and such engines diverge exactly where it shows least (null propagation in three-valued logic, interval boundary inclusivity, timezone handling at date boundaries, unit conversion). A divergence of that kind inside a signed clinical rule is a patient-safety problem rather than a bug.The surface is opt-in in every language --
@awaremd/typed-fhir/cql,Well.Services.TypedFhir.Cql,typed-fhir[cql]-- so a consumer who wants a facade, a patient cache or a FHIR query starts no engine and acquires no language runtime. Which of the three are published today is stated in Evaluating one patient; consumers of compiled criteria need none of them and a JSON parser is enough.
- Three extensions that let a criteria artifact carry what its ELM cannot say. A
module-definition
Librarysays which resources a criterion needs; it does not say which of their elements the logic reads.Criteria Elements Readrecords one element path per repeat, relative to the resource retrieved. That is finer-grained thandataRequirement, which bounds which resources are fetched rather than which of their fields have to be materialised for the logic to evaluate -- so a consumer projecting a narrow view can size it from the artifact instead of from the CQL. It is a lower bound: derived from retrieves and property accesses, without inference across included libraries, so an absent path is not proof the logic never reads it. Criteria Relative Datesays whether a criterion's membership can go stale on its own.truemeans the logic is anchored to a moving reference point ("no visit in the last six months"), so membership changes with the clock and a periodic re-evaluation is required;falsemeans it changes only when data changes, so a change feed is sufficient. A consumer deciding whether to schedule re-evaluation reads this rather than inferring it from the logic.Measurement Rationale, required on every measurement definition, records the specific ambiguity or unit trap that definition prevents. The failures it guards against -- affine unit conversions, off-by-one ordinal scales -- produce numbers that render and validate while being wrong, so a definition that cannot name the failure it prevents is a guess. R4'sObservationDefinitionhas no narrative element, which is why this is an extension; it retires intodescriptionon a move to R5.- The CQL model this specification is authored against, published as an artifact.
nexus-cql-modelinfocarries the generated Nexus ModelInfo, so a criteria author writingusing Nexus version '<x.y.z>'can fetch the model from the specification rather than reconstructing it from the profiles. - CQL is now a documented part of this specification, with a companion page on criteria artifacts covering the boundary that surprises implementers most: you do not need a CQL compiler to consume clinical logic from this specification. Translation happens once, when a criterion is authored. Everything downstream reads JSON.
-
The
Patientprofile gains the Indigenous fields. This entry was written when the work merged and was missed when 1.21.1 was cut, so it is recorded here late.The Indian Act registration or status number is a
Patient.identifierunder the Canadian URI Registry's official system (.../NamingSystem/ca-indigenous-northern-affairs-number): a 9 or 10 digit value, or a data-absent-reason while other card facets are recorded before a number is. The identifier's validity period carries the card's issue and expiry, and the newIndian Status Card Typeextension on the identifier carries the presented document form: SCIS, CIS or TCRD.Three optional flat extensions carry the rest:
Indian Act Registration Status, and theFirst Nation Registry GroupandFirst Nation Communityaffiliations, the latter two coded from the curated seed-managed band list (the code is the federal band number).Every field is independently recordable, and each records what the patient states rather than anything verified against the Indian Register. A recorded expiry does not imply a change of registration status.
Changed¶
- Five profiles now declare the element a bare CQL retrieve filters on, via HL7's own
cqf-modelInfo-primaryCodePathextension:DocumentReference,ImagingStudy,Invoice,ScheduleandSlot. Base R4's own CQL ModelInfo declares no primary code path for these five, and the consequence is not a compile error:[Invoice: "some-code"]compiles, the translator only warns, and the criterion silently filters on nothing while its data-requirements manifest emits acodeFilterwith nopath-- which violates FHIR's owndrq-1invariant. Authoring against this specification's model makes the natural form correct rather than merely accepted.
- Declared the two extensions a block-authored encounter-note section carries — Composition Block Type and Composition Block Data — which the EMR has been writing undeclared, together with the Note Block Type code system and Note Block Types value set the first of them binds. This is the block surface returning on the terms 1.14.0 set for Composition itself: there is a committed consumer. It is not the shape 1.11.0 withdrew — one section per block on R4's own
sectionordering rather than recursive containment, and two flat extensions rather than the sliced complex one whose slicing was among the unevaluable-slicing defects that removal cleared. It does enumerate block kinds asnexus-emr-embedded-section-typesdid, and the difference is what is enumerated: one kind that has shipped, rather than four that had not.
- The block-kind vocabulary is closed, and a kind outside it is not preserved. The binding is
requiredand every block-authored section declares its kind, text included. A client reading a section whose kind is ahead of its own reads and rewrites it as text: narrative, heading, identity and structured value survive, the kind does not. That is a deliberate trade — a specification that can describe everything the record holds, against forward compatibility with writers that are ahead — and deployments spanning versions should upgrade readers before writers. A section carrying no kind at all predates the extension and reads astext; that is tolerance for legacy data rather than a default, and is retirable once no such section remains.
- Both extensions are published under this IG's canonical base,
https://fhir.apps.health. They were first drafted at the address the EMR front end already wrote (http://services.well.com/fhir/StructureDefinition/...), on the reasoning that declaring a URL nothing emits would document a fiction. That was settled the other way before publication and the front end moved: published canonical URLs are forever, and reconciling the namespaces was cheapest while no note carrying either extension had been filed. One open question remains, and it is a modelling question rather than a naming one: a block's editor state is carried as a JSON-encoded extension rather than insection.entry.
[1.21.0] - 2026-08-25¶
Generated by TypedFhir 0.15.0. Unchanged from 1.20.0; the library moves independently of this specification and did not need to move for this release.
Two bodies of work: what a task means, and what a machine wrote.
Tasks had two ideas sharing one word, and the model showed it. A proposal is a task offered
for a decision (Task.intent), which a user sees in a Suggestions list rather than a Tasks list. A
suggested-resource task is one carrying a suggested chart entry inside it. Those are
independent -- an authorized order can carry a suggested entry, and a proposal can carry none -- and
treating either as a synonym for the other produced a model where Task.code had exactly one
legal value and Task.intent was being read as a containment check. Both are now real
vocabularies, suggestion is no longer a CODE anywhere in this specification, and
three verbs that were being conflated -- approve, accept, instantiate -- are named apart.
The suggested payload stops being a bespoke shape and becomes a template. Prototype resources
now ride in a contained transaction Bundle, which is what a reusable letter template carries
too and what SDC has carried all along. One mechanism, three wrappers: a List for a named
template, a Questionnaire for a form, a Task for a suggested chart entry. Instantiating any of
them becomes an ordinary FHIR transaction rather than an operation this specification has to define,
and the shape buys three things the old one could not express: multi-resource suggestions,
create-versus-update stated per resource, and atomicity stated in the model instead of in prose.
One label answers "was this machine-produced": AIAST, spec-wide. Any resource whose clinical
content was produced, in whole or in part, by a non-human agent carries
http://terminology.hl7.org/CodeSystem/v3-ObservationValue#AIAST in meta.security, whatever its
resource type. It is searchable, it costs one comparison, and it is the label HL7's AI Transparency
on FHIR guide uses. The label answers whether; the producer reference and Provenance answer
which agent, from what inputs, under whose supervision.
Most of the task work is breaking. Read these six before consuming this release:
CodeSystem/nexus-emr-suggestionis deleted. It shipped in 1.20.0; its canonical no longer resolves, and storedCoding.systemvalues pointing at it are now unresolvable. Both its codes moved -- see Removed.Task.codeno longer acceptssuggestion. A search forTask?code=...|suggestionreturns nothing after this release.Task.statusbinds a required subset.requested,received,rejected,failedandentered-in-errorare no longer conformant. Every stored task carryingrequestedneeds rewriting toready.- Every stored template needs reshaping, and so does every stored suggested-resource task: the prototypes move into a contained transaction Bundle.
- Every stored form needs the same reshaping, and its per-item
templateExtractextensions removed.NexusEmrCoreQuestionnairenow prohibits that spelling -- prototypes are entries of the Bundle named bytemplateExtractBundle. See Added. AIASTbinds producers and is not retroactive. Nothing backfills it, so an absent label means unknown, not human until every producer applies it. Read it as evidence a machine was involved, never as evidence one was not.
Added¶
Task vocabularies¶
- Nine categories for
Task.code--reminder,call,appointment,review-report,prevention,referral,billing,medication,other-- in the new Task Codes value set, bound extensibly.review-reportis named for the work rather than the source on purpose: diagnostic imaging and other non-laboratory reports arrive on the same inbound stream, so a category calledlabwould either exclude them or quietly redefinelab. (FHIR-71)
- A required subset of
TaskStatus(Clinical Task Statuses):draft,ready,accepted,in-progress,on-hold,completed,cancelled. These are meanings, not a path -- any state may follow any other, andreadystraight tocompletedis an ordinary day's work.on-holdis present for snooze and pause. (FHIR-71)
intent = #optionjoins the Clinical Task Intent value set, for a proposal that is one of a set of alternatives. Approving one moots its siblings, which are thencancelledwith astatusReasonrecording that they stopped being on offer rather than that anyone declined them. Additive: no existing writer changes. (FHIR-71)
CodeSystem/nexus-task-input-type, givingTask.input.typea published label for the one slot a consumer branches on:suggested-resource, which points at the contained Bundle. The element stays otherwise free-text. (FHIR-71)
- A priority below
routine, as the priority-detail extension carryingtrivial.Task.prioritybinds required torequest-priority, whose floor isroutine, and a profile may only strengthen a binding; the element is also a barecodewith no second coding slot. So the refinement rides beside it under a rule that makes it safe to ignore: when the extension is presentTask.priorityshall beroutine, so a consumer that does not read it under-states the task rather than mis-sorting it. (FHIR-71)
NamingSystem/nexus-cds-card, the identifier namespace for a decision-support card. One evaluation raises several proposals; each is its own Task, and every member carries the card ongroupIdentifierso the set comes back from oneTask?group-identifier=search with no parent resource. Values follow the harness key's rule -- derived from the gap the advice addresses, never from the evaluation that produced it. (FHIR-71)
AI attribution¶
- An AI Attribution pattern page stating the
AIASTrule once for the whole specification, and applying it to Composition (generated summaries and AI-scribed notes, including after a clinician edits and signs one -- attestation is a separate axis and both labels are carried), to RiskAssessment, whose "do computed scores carry it" question this settles, and to Task, where a proposal a graph or a decision-support rule put forward carries the label on the Task itself. Never on the contained payload: perdom-5a contained resource cannot carrymeta.securityat all.Conversion from a source EMR is not authorship, so converted notes carry no label. Consumers must preserve the label when writing a labelled resource onward -- dropping it turns machine-produced content into apparently human-authored content. (FHIR-74)
Form extraction¶
- BREAKING: a form carries its prototypes in a contained transaction
Bundle, and only that.NexusEmrCoreQuestionnairedeclares SDC'ssdc-questionnaire-templateExtractBundle(0..1, Must Support) and prohibits the per-itemsdc-questionnaire-templateExtractit carried in 1.20.0. Same container asnexus-template-listand a suggested-resourceTask, and the same two invariants: the reference resolves to a containedBundle, and that Bundle is atransaction. One container, three wrappers. (FHIR-71)Consumer action: every form stored against 1.20.0 needs its prototypes moved into a contained transaction Bundle, each as an entry with a
fullUrland arequest, and its per-itemtemplateExtractextensions removed. A form producing one resource has a one-entry Bundle.
- Output that depends on the answers is expressed inside the Bundle. Each
Bundle.entrycarries atemplateExtractContextexpression naming the part of the response that fills it. An entry whose context selects a repeating item is instantiated once per repeat; an entry whose context matches nothing is removed, which the extension's own definition states. A repeating entry names itsfullUrlby atemplateExtractValueexpression over anextractAllocateIdvariable, because one authoredurn:uuidcannot distinguish iterations -- legal becausetemplateExtractValueis declared with contextElement, and spelled_fullUrlbecausefullUrlis auri. (FHIR-71)This is a reading of SDC rather than a pattern SDC demonstrates.
extract-complex-template2leaves a single authoredurn:uuidon an entry whose context is a repeating group, which is under-specified rather than wrong. If SDC lands a different answer, a stored form's repeating entries are the part that would need revisiting; nothing else here depends on it.
- Why only one spelling, since SDC blesses two. Mixing them leaves a form with two prototype
pools that cannot reference each other: a
#idresolves againstQuestionnaire.contained, so an item cannot name a prototype carried as a Bundle entry, and inside an entry a#idresolves against that entry's own contained list, so an entry cannot name a loose one. A form needing an answer-driven output to cite a fixed one would have no legal way to author the link. This is a deliberate narrowing and a reversible one -- admitting the item spelling later invalidates nothing written against this profile. SDC ships the same demonstration form authored both ways (extract-complex-templateand-template2, identical item trees over the same five outputs), which is what settles them as one capability rather than two features. (FHIR-71)
- A worked example:
Diabetes Annual Review. Three
entries in one Bundle -- an
Observationand the recallTaskthat cites it byfullUrl, produced whatever the answers say, and oneObservationper reported hypoglycaemic episode, whose entry names itsfullUrlby expression because it repeats. The Tobacco Use form is reshaped to the one-entry case. (FHIR-71)
Documentation¶
- A Proposals page and a Suggested-Resource Tasks page, one per axis, both under Tasks. Between them they cover proposal-versus-order, where the prototypes actually sit, add / update / retire, the three verbs, the evaluation context, decision-support cards, citations and the idempotency key. (FHIR-71)
- Three worked examples covering shapes that had none: a suggestion that retires a chart entry
via a conditional
PUT, and a pair of card alternatives atintent = #option. The lab-review example is now the workedreview-reportcase: an incoming report already in the chart, named onfocus, atintent = #order. (FHIR-71)
Changed¶
The suggested payload becomes a template¶
- BREAKING: a suggested chart entry is carried in a contained transaction
Bundle. A task carrying one contains aBundleoftype = transactionwhose entries are the suggested resources, and thesuggestioninput references that Bundle rather than a resource directly.Why a Bundle, and not a
Listor loose contained resources.dom-2says a contained resource SHALL NOT contain nested resources (contained.contained.empty(), error severity, unchanged in R5). Anything contained in a Task is forbidden from containing anything itself, so a containedListcannot hold the prototypes.Bundleis the one container exempt: it derives fromResourcerather thanDomainResourceand has nocontainedelement for the rule to find. This is SDC's own answer --sdc-questionnaire-templateExtractBundlepoints at a contained transaction Bundle.What the transaction buys beyond legality.
entry.request.methodis per entry, so one task can create aConditionand update aMedicationRequest-- inexpressible before, because add-versus-update sat onTask.focus, which is0..1.type = transactionstates atomicity.ifNoneExist/ifMatchcarry conditional writes.entry.fullUrlis how prototypes reference each other, sidestepping#id, which resolves against the root resource and would be ambiguous for a graph inside a Task. And instantiating becomes an operation every FHIR server implements: fill the prototypes in and POST the Bundle.Consumer action: read the suggested resources from the contained Bundle's
entry.resource, and readentry.requestfor what each would do to the chart. (FHIR-71)
- BREAKING:
nexus-template-listcarries its prototypes the same way.List.containedis now exactly oneBundlewhose entries are the prototypes, andList.entryis exactly one item pointing at it; previously the prototypes were loose contained resources named individually. TheListkeeps the half a Bundle cannot do -- identity, name, lifecycle, tenancy stamp -- and the Bundle holds the graph.List.subjectmoves from0..0to1..1and is a placeholder: present, carrying a population expression as an extension, with noreferenceand noidentifier(SDC's template-extraction pattern;Referencehas no required children, so this is valid base R4). It SHOULD carry adisplaysaying the value arrives on use. A template therefore still enters no patient compartment, which is the guarantee0..0used to buy, and it is now an ERROR-severity rule rather than a convention:nexus-template-subject-is-placeholderrequiressubjectto carry neitherreferencenoridentifier. Four invariants hold the profile, two of them new (nexus-template-subject-is-placeholder,nexus-template-bundle-is-transaction) and one renamed:nexus-template-entries-resolvebecomesnexus-template-entry-is-contained-bundle, restated for the Bundle. A validator keyed on the old invariant id will not match.Consumer action: every stored template needs its prototypes moved into a contained transaction Bundle, each with a
fullUrland arequest, and a placeholderList.subjectadded. The placeholder must be EMPTY -- asubjectnaming the patient the template was authored for, or carrying an identifier, is now an error-severity failure and would put every stored template into a patient compartment. The template picker's query (List?identifier=https://fhir.apps.health/NamingSystem/nexus-template|) is unaffected. (FHIR-71)
- An evaluation context for template instantiation, named after SDC's
launchContextand through it after SMART's launch parameters:%patient,%user,%encounter,%locationand%clinical(the resource being reviewed --Task.focuson a suggested-resource task). The notation is SDC's; the context is defined here, because SDC evaluates against aQuestionnaireResponseand this does not. (FHIR-71)
Task vocabularies¶
- BREAKING:
Task.codeno longer carriessuggestion, and the nine categories replace them outright.suggestionforced an orthogonal fact onto an element that holds one value: whether a task carries a suggested chart entry is answered by the containment, and acall, anappointmentand areview-reportcan each carry one. Consumer action: a search forTask?code=...|suggestionreturns nothing. To find tasks carrying suggestions, read the containment; to build a Suggestions list, searchTask?intent=proposal. (FHIR-71)
- BREAKING:
Task.statusbinds a required subset, sorequested,received,rejected,failedandentered-in-errorare no longer conformant.requestedwas a second name forready-- the profile documented them as one state,requested/ready-- and every stored task carrying it needs rewriting.rejectedis the one that changes meaning: a declined proposal is nowcancelled, the same as one that became moot, andTask.statusReasonis what tells them apart. It is close to required in practice on a cancelled proposal, because without it the record cannot say whether a clinician considered this and said no. (FHIR-71)
- BREAKING:
identifieris no longer Must Support on ten profiles -- AppState, Task, Communication, Composition, DetectedIssue, DocumentReference, QuestionnaireResponse, Specimen and both Devices. The instance-scoped identifier scheme (.../NamingSystem/<instance>-<resource>-identifier) was retired in this release, and with it went the only thing that minted business identifiers onto these types. Must Support asserts that an implementer can be expected to populate an element; claiming it for one the specification neither mandates nor produces states a guarantee that is not being made.The element is unchanged in every other respect: same cardinality, same documentation, no binding, no mandated system. A writer that has a source-system identifier should still carry it, and a reader must still tolerate one.
Patient,PractitionerandOrganizationkeep theirs, because those are minted and their slices are real.Consumer action: none required. A consumer that drove behaviour off the must-support flag rather than off the element should stop expecting these identifiers to be present -- which they largely were not already. (FHIR-71)
Task.focusSHALL NOT be a local reference, enforced bynexus-task-focus-not-contained.focusnames a resource that already exists in the chart, which was the intent throughout but was only ever implied. Contained content is referenced frominput, never fromfocus. On a suggested-resource task it does not say whether a resource is created or updated -- that isentry.request.method, stated per resource, so one task can do both. (FHIR-71)
Task.focusacceptsNexusEmrCoreAllergyIntolerance, a ninth target type. The list shipped without it and the omission was invisible: the retire worked example -- the one the suggested-resource handbook leads with -- carries an identifier-onlyfocus, and a reference with noreferenceand noReference.typegives a validator no target type to check. It conformed by being unresolvable. A producer doing the ordinary thing and writingAllergyIntolerance/123would have been rejected by the example that taught them the pattern. Purely widening, and noted here becausefocus's type list was itself a named breaking change in 1.19.0. (FHIR-71)
NexusEmrCoreAllergyIntolerance.clinicalStatusrelaxes from1..1to0..1, because1..1made a retracted allergy unrepresentable. R4 carries a matched pair on this resource:ait-1requiresclinicalStatusunlessverificationStatusisentered-in-error, andait-2forbids it when it is. The profile kept the first and collided with the second, so the one shape that must dropclinicalStatuswas the one shape the profile required it on -- and retiring a chart entry, the case this release exists to model, is exactly that shape.Consumer action: none for a writer.
ait-1still requiresclinicalStatuson every allergy that is not retracted, which is what the1..1was reaching for. A reader that treatedclinicalStatusas always-present should now handle its absence, which means the entry was entered in error and must not be surfaced at all -- a different statement frominactiveorresolved. (FHIR-71)
- Both CapabilityStatements move their resource id off the FSH
Instancename.NexusEmrServerCapabilityStatementbecomesnexus-emr-serverandNexusEmrSubscriptionsCapabilityStatementbecomesnexus-emr-subscriptions. The publisher reportsRESOURCE_ID_MISMATCHunless the id is the last segment of the canonical, and the id was set explicitly rather than by renaming the Instance so that the canonical URLs stay byte-identical -- those are what a conformant server is checked against.Consumer action:
GET [base]/CapabilityStatement/NexusEmrServerCapabilityStatement(and the subscriptions equivalent) no longer resolves, and anyImplementationGuide.definition.resourcereference naming the old id breaks. Use the new ids. No published page URL moves: neither statement renders a page. (FHIR-71)
- BREAKING: the tobacco-use
Questionnairecanonical moves fromhttps://fhir.apps.health/Questionnaire/tobacco-usetohttps://fhir.apps.health/Questionnaire/nexus-emr-questionnaire-tobacco-use, bringing it into line with every other artifact id in this guide. The old canonical shipped in the 1.20.0 package.Consumer action: stored
QuestionnaireResponse.questionnairevalues pointing at the old canonical no longer resolve and must be rewritten. This is a one-line update per stored response; there is no automatic redirect. (FHIR-71)
AI attribution: two open questions narrow¶
NexusEmrCoreProvenance: the model-accounting question splits in two, and one half has an expected direction. Model identity and version and operational telemetry were recorded as a single item and are separable. The position on telemetry is settled and unchanged -- token spend, latency and cost are not clinical facts and stay out of the record. Model identity is genuinely open, and now names what would decide it: a consumer that must answer "which model version produced this statement" from the record alone, to withdraw or re-review every output of a defective version without the producing system's logs. External AI-transparency standards are moving toward requiring that, so expect this to be revisited rather than to stay open. No element changed. (FHIR-74)
NexusEmrCoreComposition: the ORIGIN question is narrower. The coarse question -- was any of this machine-produced -- is answered byAIASTand settled spec-wide. What stays open is only which mechanism carries the finer question of what a machine contributed and from where:authorcarrying aDevicealongside the human, or aProvenancerecord. Neither is foreclosed. (FHIR-74)
Removed¶
- Two
Questionnaireinvariants, with the extraction spelling they governed.questionnaire-extract-templates-are-containedandquestionnaire-extract-templates-resolveboth constrainedsdc-questionnaire-templateExtract, which this release prohibits; they are replaced byquestionnaire-extract-is-bundle-onlyplus the two Bundle rules. A form conforming to 1.20.0 does not conform to this release -- see Added. (FHIR-71)
- The
*-has-raw-identifierinvariants are gone, from all 34 profiles that carried them. They were warning severity and advisory: each fired unless a resource carried at least one business identifier from a system other than the Nexus canonical one. Nothing replaces them. Carrying a source identifier is still the right thing to do where the resource was converted from a source EMR, and every profile still says so in prose, but it is no longer checked.Consumer action: a validation run that used these warnings as a lossy-conversion detector loses that signal in this release, and gets no warning whatever it does. There is no substitute in the specification; a converter that wants the check now owns it.
identifiercardinality and slicing are unchanged on every profile -- nothing about what you may write has moved, only what is reported back. (FHIR-71)
- BREAKING:
CodeSystem/nexus-emr-suggestion, which shipped in 1.20.0. Its canonical no longer resolves and storedCoding.systemvalues pointing at it are unresolvable. It bundled a task category and a structural input label under one name because both mentioned suggestions; they are separate axes and now live apart. The codes are unchanged, the systems are not:was is now nexus-emr-suggestion#suggestiononTask.codedeleted -- see the Task.codeentry abovenexus-emr-suggestion#suggested-resourceonTask.input.typenexus-task-input-type#suggested-resourceConsumer action: rewrite the
systemon stored input labels tohttps://fhir.apps.health/CodeSystem/nexus-task-input-type. StoredTask.codevalues need a category fromhttps://fhir.apps.health/CodeSystem/nexus-task-code, or removal --codeis optional. (FHIR-71)
Fixed¶
- Two value sets had an id that disagreed with their own canonical, and the page URL followed
the id rather than the canonical.
vs-nexus-emr-indigenous-identityandvs-nexus-emr-racialized-groupboth published a^urlof.../ValueSet/nexus-emr-indigenous-identityand.../ValueSet/nexus-emr-racialized-grouprespectively, so the artifact a consumer resolves was never the one the page was named for. The ids now match the canonicals.The canonical URLs do not move -- they were always these values, and stored data referencing them is unaffected. Two page URLs do:
ValueSet-vs-nexus-emr-indigenous-identity.htmlandValueSet-vs-nexus-emr-racialized-group.htmlare gone, replaced by the same names without thevs-prefix. A bookmark or partner reference to either page needs updating. (FHIR-71)
- A suggested-resource example carried a business identifier under a namespace that does not
exist.
NamingSystem/nexus-harness-suggestion-identifieris defined nowhere in this specification; the example now carries the harness key it was supposed to demonstrate, valued from the gap rather than from the run. (FHIR-71)
- The specification said a form could not use a contained Bundle, and gave a reason that does not
survive reading SDC. The claim was that "a repeating item produces one resource per answer and
none if unanswered, which a static transaction Bundle cannot express". A Bundle entry is not static
in that sense:
sdc-questionnaire-templateExtractContextis legal onBundle.entry, its own definition says an element whose expression yields no results is removed, and SDC'sextract-complex-template2example runs one authoredRelatedPersonentry over a repeating group. Corrected across the Templates page, both profile intros, and theQuestionnaire,nexus-template-listandTaskprofile text. The claim was load-bearing: it was the whole reason a form was modelled as the exception, and with it gone the form uses the same container as everything else. See Added. (FHIR-71)
- Handbook links on profile pages sent readers of a local build to the live site. The source
must keep the absolute URL -- the IG Publisher renders
src/pages/at the site root, so a relative../guide/...climbs out of the output directory and aborts the run. The docs generator now rewrites that published prefix to a relative path when it embeds the page for this site, so the same source is correct in both places. Eleven links across eight pages. (FHIR-71)
[1.20.0] - 2026-08-24¶
Generated by TypedFhir 0.15.0. Bumped from 0.14.0, which generated 1.19.0. All four published packages move together.
Two things this specification could not describe, it now can. A referral is not an order, and
the order profile's fixed intent, required category binding and mandatory code made a referral
unrepresentable rather than merely awkward; NexusCoreReferral is a sibling profile for it, and
NexusEmrCoreServiceRequest is unchanged in every constraint. A Questionnaire profile closes
the half of forms the IG had left ungoverned -- it profiled the answer set and not the form that
produced it -- and forms and letter templates turn out to be one mechanism rather than two that
resemble each other.
Drug identity moves off the DIN alone. All three medication resources and
AllergyIntolerance now slice ingredient-level codes, because every cross-record
question -- is this duplicate therapy, are these the same drug, does this medication match a
recorded allergy -- is an ingredient-level question, and a DIN identifies one manufacturer's
marketed product and so cannot answer any of them.
Terminology documentation is the largest single body of work in the release. Every code system this specification binds now has a page saying what it is, who publishes it, where to obtain the codes and whose licence decides whether they may be reproduced: nineteen new artifacts covering thirty-four pinned system URIs, an evenly-spread code sample on the nine systems whose terms permit one, an explicit allow-list so a system added later publishes nothing until its terms have been read, and full code lists on the fourteen value sets over this specification's own systems that previously said "All codes from this system" and listed none.
On the library side, 279 of the 736 typed accessors change their return type, and the Python module layout moves beneath the top-level import. The read side is where the exposure is, and most of it keeps working, which is the problem.
Read these four before consuming this release:
- The package id is
nexus, notnexus-emr-- one line in your dependency declaration. No canonical in the namespace changed and nothing in stored data moves. - The DIN code system URI moves, and that one IS in stored data, on
MedicationRequest. - 279 accessors change return type. Audit for
.coding/.system/.code/.idreached directly off an accessor named in the table under TypedFhir libraries. validate()is stricter twice over -- unknown elements now error, and twenty-two base-R4 required bindings are enforced that previously could not be read. Run your corpus through it before upgrading.
Added¶
Profiles, value sets and terminology bindings¶
-
A
Referralprofile -- the IG could not describe a referral, and now can.NexusCoreReferral(https://fhir.apps.health/StructureDefinition/nexus-core-referral) is a newServiceRequestprofile for a request that another clinician or service take the patient on. It is additive:NexusEmrCoreServiceRequestis unchanged in every constraint, so laboratory and imaging orders keep the guarantees they already had.A referral could not claim the order profile, and this was not a near miss. That profile fixes
intent = order, bindscategory(required) to the five-concept base FHIR value set, and requirescode 1..1. A referral isintent = proposal-- the referrer asks a receiving service to accept the patient and has no authority over whether it does -- itscategoryis the receiving service or specialty, which that five-concept set cannot express, and the Ontario eReferral contract sends nocodeelement at all. Three constraints, three mismatches.The new profile is a sibling derived from base R4, not a child of the order profile: a derived profile may tighten its parent but never loosen it, so a fixed
orderand a required binding are not recoverable downstream.intentis fixed toproposal,categoryis1..*bound extensibly to the newNexusReferralServiceTypevalue set,codeis0..1with an invariant requiringcode.textwhen it is present, andrequesteris required. A second invariant,referral-category-coded, asks for at least one SNOMED coding oncategoryat warning severity rather than error, for the same reason the binding is extensible: destinations do arrive with no usable code, and the right answer for one of those is to carry the text rather than map it to an approximate neighbour, because an approximate destination routes the patient approximately.This aligns with the pan-Canadian and Ontario standards rather than inventing beside them. CA-eReC fixes
intent = proposaland requiresrequesterandauthoredOn; the Ontario eReferral profile derives from CA-eReC; this IG already carriesca.on.erec.r4as a pinned dependency. Two divergences are deliberate and documented on the profile itself:performerstays0..*where CA-eReC requires it (that profile governs electronic messaging, where a destination is definitionally known, while this one also covers referrals recorded in the chart that were never sent electronically), andcategorydoes not adopt CA-eReC's binding -- see below.
-
A
NexusReferralServiceTypevalue set for referral destinations, mirroring Canada Health Infoway's activeReferralServiceCategoryCodeover SNOMED CT Canadian Edition reference set379431000087101.It is deliberately not called a specialty value set, and that is not cosmetic. Several destinations a primary-care referral routinely targets exist in SNOMED CT only as service concepts, with no specialty concept at all -- Sports medicine, Sleep medicine, Spinal surgery and Clinical neurophysiology among them. A specialty-only value set cannot describe the destinations this EMR already offers, which is why Infoway's set is a union of healthcare services and provider specialties. Implementers building a destination picker should expect both kinds of concept and must not filter to the specialty hierarchy.
CA-eReC's own
categorybinding is not adopted. It bindsInterventionCodeSubsetCare, which represents "the care procedures performed by a Provider" -- an intervention taxonomy, which is whatcodemeans, not the destinationcategorycarries -- and which Infoway retired in April 2026 along with the Primary Health Care EMR Minimum Data Set, marking it "not recommended for continued use".codeis therefore left with the base R4 binding until Infoway publishes a replacement.The binding is extensible, not required, and that is load-bearing rather than a hedge: Ocean sends
17561000 |Cardiologist|-- an occupation concept -- incategory, and it is not a member of this value set. A required binding would make the referrals actually received non-conformant. Preserve the source coding exactly as received and add the Nexus coding alongside it in the sameCodeableConcept; do not rewrite or drop the original. The new handbook page covers the Ocean relationship and the coding guidance in full, and the shipped example demonstrates the dual-coding pattern.
-
A
Questionnaireprofile, and forms are now the same mechanism as letter templates.NexusEmrCoreQuestionnairecloses the half of forms the IG never modelled: it profiled the answer set (QuestionnaireResponse) and not the form that produced it, so a response pointed at a definition the spec did not govern. The profile requiresurlandversion-- a response is only interpretable against the version it answered, because the question text, item types and answer options all live in the form -- and binds answers to the chart throughitem.definitionanditem.coderather than a private extension. The structural news for implementers is that a form and a template are one mechanism. Both carry contained prototypes of what they produce and are instantiated by cloning them and filling in what the definition could not know; they differ only in what drives the fill (aQuestionnaireResponse, or an instantiation context). The handbook's Templates page now documents that once for both, and each profile page points at it. Named as not settled rather than approximated: approximate dates and durations (a real clinical requirement with no FHIR datatype), body diagrams, note generation, and FHIRPath expressions.SDC extraction is governed by two invariants, and nothing checks them at runtime. (Both were removed in 1.21.0 along with the spelling they governed -- see that release. Named here without links, because the anchors they had are gone.)
questionnaire-extract-templates-are-containedrequires everysdc-questionnaire-templateExtractto target a prototype contained in the form, andquestionnaire-extract-templates-resolverequires that#idto resolve. Both traversedescendants().ofType(Extension), which reaches the root's own extensions, so an extraction at the form root -- the ordinary case -- is examined rather than passing vacuously; and both read the prototype from the complex extension'stemplatesub-extension (extension('template').value.ofType(Reference)). SDC prohibitsExtension.value[x]ontemplateExtractitself, so a form carryingvalueReferencethere is not valid SDC and does not pass. Both are declared#errorand both sit on the runtime allowlist, as do base R4'sque-1,que-2,que-7,que-10andque-13: the HL7 validator enforces all seven against this IG's examples andvalidate()enforces none of them. If you author forms programmatically, run the published validator or check containment yourself. The profile ships with a worked example, and the extraction rules were mutation-tested in both directions -- a stored-resource target and a dangling#ideach fail the rule that should catch them, and a conformant root-level and item-level extraction each pass.
-
Medications now carry drug identity at TWO levels, and all three medication resources carry the same model.
MedicationRequest,MedicationStatementandMedicationDispenseeach slice the medication concept'scodingintodin,ccdd,atcandaign. A DIN identifies one manufacturer's marketed product; CCDD (http://terminology.hl7.org/CodeSystem/hc-CCDD) and ATC (http://www.whocc.no/atc) identify the ingredient. Every cross-record question is an ingredient-level question -- are these two prescriptions the same drug, is this duplicate therapy, does this medication match a recorded allergy -- and a DIN cannot answer any of them, because the same drug from a second manufacturer carries a different DIN and a change of salt changes it again. Until now the spec offered a DIN slot and nothing else, and only onMedicationRequest; the other two named code systems in prose that constrained nothing. Nothing here is required and nothing existing is invalidated: every slice is0..*, the slicing isopen,textremains the only requirement on the concept, and instances carrying no coding, or an unsliced one, stay conformant. Consumer action, if you write these resources: carry the ingredient-level code the source actually has, alongside the DIN rather than instead of it. Do not synthesize one that was not given to you.The
aignslice carries Health Canada's DPD active-ingredient group number (http://terminology.hl7.org/CodeSystem/v3-hc-aign, published through HL7 Terminology and resolvable from thehl7.terminology.r4package this IG already depends on), and it is the one to reach for when there is no CCDD -- in practice most legacy EMR medication lists, where a DIN and a line of free text is all that exists. It gathers products sharing an active-ingredient composition across manufacturer and salt form and covers the whole DIN space, so it is reachable from a DIN when neither CCDD nor ATC is.The number is ten digits and it decomposes, which decides which of two systems a record should carry:
[2 digits: how many active ingredients][5 digits: which active-ingredient group][3 digits: which strength group]. The full ten-digit number is therefore strength-specific, which is what a prescription, a dispense and a medication statement mean -- so the medication profiles bind it.AllergyIntolerancebinds the five-digit middle portion instead (http://terminology.hl7.org/CodeSystem/v3-hc-aigc), because an allergy is to an ingredient and not to a strength. To match a medication against an allergen, compare positions 3 through 7 of the medication's number to the allergen's code -- they are different systems and different lengths, and comparing them as equal strings never matches.
AllergyIntolerance.codenow has a drug-allergen path, viaccdd,atcandaigcslices oncode.coding-- the same posture asCondition.code.coding[icd10ca]andProcedure.code.coding[cci], where the IG declares the slot and the system and does not enumerate codes. The element previously requiredcode.textand mandated no system at all, which left no join key between an allergen and a medication: matching meant string comparison over clinician-typed free text. An allergen coded with CCDD, ATC or the DPD active-ingredient group is now matchable against a medication coded the same way -- the three medication profiles slice the same systems, because a join key has to be sliced on both sides or it does nothing. The one exception is the active-ingredient group, where the allergen slice binds the five-digit code (v3-hc-aigc) and the medication slices bind the ten-digit number (v3-hc-aign): an allergy is to an ingredient, not to a strength, and matching the two means comparing positions 3 through 7 of the number to the code rather than comparing the two values directly. There is deliberately nodinslice. A patient is not allergic to a marketed product, they are allergic to what is in it; a DIN-to-DIN comparison misses the same drug from another manufacturer and misses a different salt of the same ingredient, so a DIN slot on an allergen would look like a join key while silently failing the cases the join exists for. All three slices are0..*and the slicing isopen, so existing allergy records are unaffected.
Terminology documentation¶
-
Every code system this specification binds now has a page that says what it is. The profiles pin thirty-four system URIs. Some were minted in this specification's own namespace and defined nowhere, so following one reached nothing; the rest were third-party systems with no statement of what they are, who publishes them, or where an implementer gets the codes. Both gaps are closed by nineteen new artifacts, and a pinned
.systemvalue on a profile page is now a link to the page describing it rather than a bare URI.Five artifacts define URIs this specification mints: a
CodeSystemfor the OHIP Schedule of Benefits (.../CodeSystem/on-ohip-schedule-of-benefits,content = not-present-- the system is declared, the Ministry of Health's fee codes are not reproduced), andNamingSystemresources for the OHIP billing number, the Ontario RMA / group billing number, the Ontario Service Location Indicator and the patient MRN. Each says what the identifier is, who assigns it, and what it does not mean -- the MRN page in particular, because an EMR-local chart number is unique only within the deployment that issued it and must not be used to match patients across systems.Thirteen third-party systems get a
ValueSetin this specification's namespace (.../ValueSet/all-<system>), each a singleincludeof the whole system with no codes enumerated: LOINC, SNOMED CT, UCUM, ICD-10-CA, CCI, DIN, CCDD, ATC, the two Health Canada active-ingredient group systems, the HL7 identifier-type codes, NullFlavor and data-absent-reason. Each page states what the system is, who publishes it, its canonical URI, where to obtain the codes, and where a licence gates use (ICD-10-CA and CCI are CIHI-licensed; SNOMED CT requires an Affiliate licence). NoCodeSystemis published at a canonical this specification does not own -- a second definition of a canonical its publisher already serves is how one identifier acquires several spellings, so these claim no foreign canonical and redistribute nothing.The internal EMR user-account system gets a
NamingSystemrather than a value set, because it names an identifier AUTHORITY and not a terminology: there is no concept list to point at and the values are opaque account ids, so "every code in the system" would be a category error. Its page says what the profiles already say -- it is private infrastructure and external consumers must not depend on it.Where the publisher's terms allow it, the page also shows a sample of the codes -- up to 500, spread evenly across the system rather than taken off the front, with the size of the population they came from stated above the table and again below it, so a list that stops at 500 cannot be read as a complete one. Nine systems qualify: LOINC, DIN, the two Health Canada active-ingredient group systems, ATC, UCUM, and the HL7 identifier-type, NullFlavor and data-absent-reason codes. Each carries the attribution its licence requires.
The LOINC sample excludes every term carrying a third-party copyright notice -- 7,355 of them, mostly scored assessments and survey instruments whose questions belong to someone other than Regenstrief. The LOINC licence would allow reproducing those provided the notice travelled with them; dropping them instead means no third-party instrument text is republished here at all.
Where the terms do not allow it, the page says so and says whose terms decide it. No ICD-10-CA, CCI or SNOMED CT code is reproduced anywhere in this specification, and none will be: rendering codes onto a published page is redistribution, and those are licensed by CIHI and SNOMED International respectively. CCDD is withheld as well, because redistributing its content is a permission separate from the free use it grants. The list of systems whose codes may be reproduced is an explicit allow-list, so a system added to this specification in future publishes nothing until its terms have been read and recorded. This applies to
CodeSystemartifacts that declare a system without carrying its content, not only to value sets -- from a reader's point of view they are the same page, so they answer the question the same way.OHIP fee codes are among the ten, on the King's Printer's legal-materials permission: the Schedule of Benefits is incorporated by reference into R.R.O. 1990, Reg. 552 under the Health Insurance Act, and the King's Printer permits anyone to reproduce the text contained in Ontario's regulations without permission or charge, provided the reproduction is accurate and is not represented as an official version. Both conditions are met on the page, which states in terms that it is not an official version and that the Ministry's edition in force governs. The sample omits the 406 codes that carry no published description in any schedule, rather than listing them against their own code.
- A value set over one of this specification's OWN code systems now lists its codes in full. Fourteen pages -- appointment modality, clinical roles, province/territory, pay program, task business status and the rest -- said "All codes from this system" and then listed none, which is the least useful thing a page about a five-code vocabulary can do. These are not a licence question at all: the concepts are in this specification's own artifacts, so the page shows every one of them rather than a sample.
- The OHIP Schedule of Benefits page now carries the ICD-9 warning about Ontario's DIAGNOSTIC
codes. They are a separate Ontario code set with its own system URI, they are ICD-9-LIKE in
shape, and they are not a subset of ICD-9 -- so an OHIP diagnostic code must never be derived
by truncating an ICD-9 code. The 895-971 range is a parallel Ontario space laid over ICD-9
numbers that mean something unrelated:
970is Prenatal Care in Ontario and poisoning by CNS stimulants in ICD-9. Measured against the Provincial Health Care Vocabulary Standard, a SNOMED CT to ICD-9 to truncate route reaches 69.4% of cases and 124 of them come out valid and wrong -- nothing downstream rejects them, so they reach a claim and the diagnosis on the record is not the one the clinician made. Eighteen codes carry two unrelated meanings within the Ontario set itself. The warning was in the specification's source and on no page a reader would find.
- The IP statements page names each terminology, flags the ones you must hold a licence for, and links where this specification binds them. It was sixteen unlabelled licence paragraphs in one flat list: nothing said which terminology a paragraph was about except the prose itself, nothing distinguished a notice you reproduce from a licence you must hold, and nothing connected a statement to the profiles and value sets that made it apply. The page now opens with a table -- terminology, system URI, and whether a licence is required -- and each statement is its own section, carrying the licence text unchanged plus the artifacts of ours that bind that system, linked. SNOMED CT, ICD-10-CA and CCI additionally carry a warning callout, so the three obligations the page's opening paragraph names are visible beside the terms rather than only in the preamble. No statement's text changed and the build check behind the page is unchanged: a bound code system with no statement still fails the release.
Guide, examples and site¶
- Structured Data Capture is a new package dependency (
hl7.fhir.uv.sdc), and it is the reason the above is one mechanism rather than two that resemble each other. SDC's template extraction is contained-prototypes-cloned-and-filled -- the same shape this IG reached independently in 1.18.0 -- so the Questionnaire profile adopts its vocabulary natively instead of minting a parallel one. Consumers must resolve one more package. Note that the extraction extensions aredraftinside a 4.0.0 ballot and will move, so the dependency is pinned to an exact build, and a templateListstill does not require a Questionnaire to exist -- only the substitution vocabulary is shared.
- Conversation Threading is now a guide topic of its own. One page states the mechanism behind
every conversation in the chart -- a colleague's question, an accepted suggestion, and a
back-and-forth with an AI agent are the same shape: the opening body in
Task.description, the turns inTask.note. It documents the accept/resume round trip end to end (promote the payload, record what was actually created, say what was decided, hand the task back to the requester so the harness resumes the named thread) and the rendering rule that a thread isdescriptionfollowed by the notes in order. No profile changed.
- Two worked examples, each for a relaxation that shipped with nothing exercising it.
NexusEmrComposition-converted-summary-exampleis a summary classified oncategoryalone -- the shape the profile's prose documents and the shape the attester exemption used to reject, so the example now holds that behaviour in place rather than describing it.NexusEmrCareTeam-nursing-pool-exampleis a CareTeam with nosubject, the population that relaxation was made for: a standing nursing pool belongs to the clinic rather than to a patient. Both relaxations were announced in 1.19.0 with no instance demonstrating them, which is how the attester contradiction survived a release -- the only Composition example in the IG happened to carry the summary code in bothcategoryandtype, so it passed for a reason the prose does not ask anyone to reproduce.
- The 404 page renders. It has been served unstyled since this site was Polaris's -- bare
checkboxes, a broken logo and the theme's icons at their intrinsic size -- because
site_urlwas never set. Every other page gets relative asset paths;404.htmlcannot, since it answers for a URL at any depth, so mkdocs writes root-absolute paths and derives the root fromsite_url. Absent, it assumed the domain root, and every stylesheet and script on that one page 404'd under/nexus-ig/. It lasted this long because a link checker never asks for a page that does not exist, so nothing ever fetched it.
- The release feed is reachable from every page, not only from the page source. An RSS icon
and the word RSS now sit beside the GitHub link in the site header, and in the drawer on narrow
screens, pointing at the same
releases.xmlthe Releases page links. Neither the feed nor its<link rel="alternate">autodiscovery is new; what was missing was anything a reader could see and click, so the one supported way to be told when this specification changes was discoverable only by viewing source or by already being on the Releases page. The feed carries each release's entry and links its package directly, which is enough for a feed reader or a Teams/Slack connector.
Changed¶
Specification¶
-
BREAKING -- the package id is now
nexus, notnexus-emr. The tarball,ImplementationGuide.idand.packageId, theImplementationGuidecanonical (https://fhir.apps.health/ImplementationGuide/nexus) andpackage-list.json'spackage-idall move together. If you depend on this IG, change the package name in yoursushi-config.yamldependencies (or wherever you resolve it) fromnexus-emrtonexus.It is breaking for one line in your build and for nothing else.
No identifier in the namespace changed, and nothing in stored data moves. Profile, extension, CodeSystem and NamingSystem canonicals are byte-for-byte what they were --
.../StructureDefinition/nexus-emr-core-patientis still that, and.../CodeSystem/canadian-phn-systemsis still that. This is deliberately NOT what 1.15.0 did: that release renamed the identifiers themselves, which changedCoding.systemandIdentifier.systemvalues that exist in stored data, and it was withdrawn the same day. The package id is not an identifier in the namespace; it is the name the package resolves under, and it appears in no resource.The
-emr-infix is being retired where retiring it is free. This and the registry selector (fhirToResources({ ig: "nexus" }), previously"nexus-emr") are the two places that cost nothing. The identifiers stay.
- The archived package downloads are renamed, and five previously published links move.
packages/nexus-emr-<version>.tgzbecomespackages/nexus-<version>.tgzfor 1.15.1, 1.16.0, 1.17.0, 1.18.0 and 1.19.0 as well as for this release. The version-history page links the new names; the old URLs 404. Consumer action: if you bookmarked or scripted an archive URL, update it. Nothing about the PACKAGES moves -- an already-published package keeps the id it was published with, and FHIR tooling resolves by id, not by the filename on this site. The filename is deliberately not derived from the package id for exactly this reason: the id has changed twice across releases and a single stem needs no per-version exception map.
-
BREAKING -- the DIN code system URI moves to
http://hl7.org/fhir/NamingSystem/ca-hc-din.MedicationRequest'sdinslice fixedhttp://hl7.org/fhir/sid/ca-hc-din, and it is the only slice ever published on that system:MedicationStatementandMedicationDispensegain theirs in this release (see Added) and carry the new URI from the start, so neither has ever carried the old one. No publisher declares the old URI. HL7 Terminology declares the Drug Identification Number system under OID2.16.840.1.113883.5.1105with three URIs: the NamingSystem form above, marked "official canonical URL going forward";http://terminology.hl7.org/CodeSystem/ca-hc-din, retained for backward compatibility; and a canada.ca URL. Thesidform is not among them, and the system is not served at it.This changes a
Coding.systemvalue that exists in stored data. Unlike the package rename above -- which moves the name a package resolves under and appears in no resource -- this one is in your medication records. Consumer action:- Writers must emit
http://hl7.org/fhir/NamingSystem/ca-hc-dinon thedinslice. An instance carrying the old value no longer matches the slice; because the slicing isopenit does not fail validation, it falls into the unsliced remainder, so anything readingcoding[din]silently gets nothing. - Readers should accept both for as long as their own data carries either. The two URIs name the same code system, and the OID is the identity that holds across every spelling. - The exposure isMedicationRequestonly. That is the one profile whosedinslice shipped with the old URI, so it is the only place stored data can be carrying it. - This release does not migrate stored data. NoCoding.systemvalue is rewritten by installing it, and nothing detects or converts the old value on ingest. Both remain to be done and are not part of this change.Nothing else about the slice moves: the code values, the cardinality, the slicing mode and the representative-DIN strategy are all unchanged.
- The server CapabilityStatement gains Questionnaire, and advertises two profiles on
ServiceRequest.nexus-emr-servernow declaresQuestionnairewith read, search, create and update and no delete, matching the profile'sSmartExposedNoDeletegrant: retiring a form is a status change, not a delete, because responses to a retired form must stay interpretable and removing the definition would strand everyQuestionnaireResponsepointing at its canonical.ServiceRequestnow lists bothNexusCoreReferralandNexusEmrCoreServiceRequestinsupportedProfile, so a client is told both canonicals it may meet at that endpoint. Which one applies is decided byintent, not guessed fromcategory. Consumer action: an app that enumerates reachable types from the CapabilityStatement will see one more, and a client that assumed one profile per resource type must branch onServiceRequest.
-
NexusEmrCoreServiceRequestno longer advertises referrals -- documentation only, no constraint changed. The profile'sDescription, its root andcategorydefinitions, itsintentdefinition and its narrative page all said it covered referrals. Its binding forbade one:categoryis required-bound to a value set with exactly five members (108252007 Laboratory procedure,363679005 Imaging,409063005 Counselling,409073007 Education,387713003 Surgical procedure) that contains no referral concept and is not a subtree one could be added under. An implementer reading the definition and then binding-checking found the two disagreed, and the nearest usable concept was Counselling, which would have mislabelled every referral.Nothing was relaxed and no published canonical changed meaning.
intentis still fixed toorder,categoryis still1..1required-bound,codeis still1..1withcode.text. The text now says what the constraints already said, and points atNexusCoreReferralfor the case it never covered. No action for consumers of laboratory or imaging orders.Worth stating plainly for anyone who assumed otherwise: base R4 puts
ServiceRequest.categoryat0..*with an example binding, and describes the resource as covering "a referral or a transfer of care request". This profile's required, exactly-one binding is a Nexus tightening, and the referral profile exists because that tightening is correct for orders and wrong for referrals -- not because the base resource is ambiguous.
- The medication coding slices are discriminated by
value, notpattern-- non-breaking.MedicationRequest.medicationCodeableConcept.codinghas been sliced onsystemsince it was first profiled, withdiscriminator.type = #pattern; it is now#value, matching theCondition.code.coding[icd10ca]andProcedure.code.coding[cci]slices and the two medication profiles that gain slicing in this release. No instance that validated before fails now. The discriminator path issystem, a primitiveurifixed with(exactly), and for a fixed primitive the two discriminator types resolve to the same test: exact string equality. The two diverge only on complex types, wherepatternis a partial match andvalueis not, and there is no complex type on this path. It is recorded here because it is a real edit to a publishedStructureDefinition, and a consumer reading the slicing metadata will see it change.
MedicationDispenseno longer recommends RxNorm. ThemedicationCodeableConceptdefinition recommended "DIN, RxNorm, or other standard terminology". RxNorm is a US drug vocabulary, and a Canadian dispense record has no route to an RxNorm code that does not pass through a mapping this specification does not define; Canada uses DIN at the product level and CCDD at the ingredient level, and the definition now says that instead. The slicing isopen, so a source that genuinely holds an RxNorm code may still carry it as an unsliced coding -- this withdraws a recommendation, not a permission.
-
BREAKING: the
composition-signed-has-attesterexemption admitted the wrong set, and rejected the documented one. The Composition profile's prose is explicit thatcategoryis the classifier and thattypecannot be one -- "in converted source data, often a constant (every OSCARcasemgmt_notearrives as LOINC 11488-4 whatever it is)". The invariant then required the summary code intype. A converted summary written exactly as the profile documents wasfinal, carried no attester, and failed. The IG's own example passed only because it happens to carry the code in both places, which is not what the prose asks for, so nothing surfaced the contradiction.The exemption now reads: EITHER classifier says summary, AND
categorysays nothing else. Theoradmits the documented shape and the type-only shape; theandstill closes the original hole, in which an authored note could buy its way out of a signature by gaining a second category. Consumer action: a generated summary classified oncategoryalone now validates where it previously did not -- if you worked around this by writing the code intotypeas well, you can stop, and the documented shape is the one to write.
-
BREAKING:
Task.noteis now actually validated againstnexus-emr-task-note, and 1.19.0's announcement of this was premature. That release said "everyTask.noteturn now REQUIRESauthorandtime". The profile said so; nothing checked it.Task.notenamesnexus-emr-task-noteintype[].profile, and the profile requiresauthor[x]andtimeand carries an error-severity invariant -- none of which ran in any of the three ports. Profile selection keyed onkind === "resource"and the rescue path for the other case keyed onisExtensionSd; a datatype profile iscomplex-typeand is not an Extension, so the IG's first and only one fell through both filters.typeProfileswas inert at the far end too: the IG model has always carried the field and no port has ever read it, so the reference pointed nowhere from both directions.Consumer action, and read this even if you already migrated for 1.19.0. If you took that release's word for it, changed your writer, and confirmed against a running validator that nothing failed -- the confirmation was worthless, because nothing could fail. Re-check against this release. Unattributed or undated turns now produce validation errors, and if you concluded from a green 1.19.0 run that your existing data was already compliant, that conclusion has not been tested.
The rules are FLATTENED onto the element's own path (
["note","author[x]"],["note","time"]) rather than resolved fromtypeProfilesat runtime -- that is the nested-cardinality shape the validators already run and already test, and resolving instead would have added a resolution step to three ports that then had to be kept in agreement. The invariant anchors per note, the way base-FHIR backbone invariants already do.
- Templates say why they carry no
urlorversion, and name a rule the profile cannot enforce. The asymmetry with the new Questionnaire profile is deliberate: a Questionnaire stays relevant to the rendering of a response, while a template's result is self-contained -- a letter made from a template is a completeCompositionand needs no template to render. The rule consumers must implement: at most oneListwithstatus = currentmay carry a given template identifier value, and more than one is a corrupt state in which a consumer MUST raise an error rather than choose between them. Not newest-by-date, not first-result, not both shown to the user -- every such heuristic makes the defect permanent and invisible while the picker keeps working. It is a cross-resource constraint, so no invariant can express it; the gateway check is prevention and the thrown error is detection, and both are needed.
- The handbook and the reference IG now cross-reference each other. The first mention of each resource and element in a guide page links to its reference page, elements to the exact row anchor; profile pages link back to their handbook topic. Reader-facing only, no profile changed.
- "Clinical Tasks" is now "Tasks" in the guide, with Conversation Threading nested under it rather than sitting as a sibling topic.
- The site's About section is gone, and two published pages now 404.
guide/about/version-history.mdandguide/about/downloads.mdare removed. Each was half an answer -- one listed which versions exist and what you could fetch, the other said what changed, and each opened by pointing at the other -- so both are now the Releases page, whose table carries version, date, the KIND of change the release contains and the package link on one row. Consumer action: update a bookmark to either retired page; the Releases page keeps thechangelog.htmlURL it has always had, so an inbound link to THAT one, and every item already sitting in a feed reader, still resolves. The two About pages that survive, IG metadata and IP statements, keep theirguide/about/URLs and move only in the navigation: they now sit under a top-level Releases tab beside Reference and Handbook, rather than four levels down the Handbook, which is the wrong place for the page carrying a licence obligation.
TypedFhir libraries¶
-
BREAKING: 279 of the 736 typed accessors change their RETURN TYPE, in all three published packages. This is the largest consumer-visible change in the release. Counted by diffing every generated
@propertyagainst 1.19.0 in the Python port; the TypeScript and C# ports move with it, name for name.Accessors Was Is What it is 87 str \| NoneFuzzyDate \| Noneevery date accessor -- authored_on,birth_date,recorded_date,issued,start,end,sent,expiration_date, ...85 Json \| Nonelist[Json] \| Nonethe *_rawtwins of the repeating elements below62 Concept \| NoneConceptsrepeating coded elements -- category,reason_code,specialty,body_site,interpretation, ...25 Reference \| NoneReferencesrepeating references -- author,performer,based_on,part_of,payor,focus, ...15 list \| NoneNotes/Names/Addresses/Telecomsnote threads, names, addresses, telecoms 5 str \| NoneConcept \| Nonecoded statuses -- clinical_status,verification_status,scope,policy_ruleNone of the supporting types (
Members,Concepts,References,Notes,Names,Addresses,Telecoms,Annotation,HumanName,Address,ContactPoint,FuzzyDate) existed in any port at 1.19.0.Why a repeating element had a singular accessor at all, and why that was the bug: the old accessor returned
element[0], so a caller read the first category and, writing it back, erased every other one. A collection is what makes append expressible.Consumer action, and the read side is where the exposure is. The WRITE side is defended: a bare
Conceptorstrassigned to a now-repeating setter raises a loudTypeErrornaming the expected type, in Python and TypeScript both, rather than silently replacing the element. The read side is quieter, because most of it keeps working:x.category.displaystill works: a collection'sdisplayjoins its members with"; ".if x.author:still behaves for a one-author resource.x.category.codingdoes NOT: it isAttributeError: 'Concepts' object has no attribute 'coding'. Take a member first --x.category[0].coding, or iterate.x.author.idlikewise;x.author[0].idorx.author.to("Practitioner", id).- Arithmetic or slicing on a date accessor as a
strnow sees aFuzzyDate. In Python it is astrsubclass so most of it keeps working; in TypeScript and C# it is not, and it wants.value/.Value. In exchange the value answers.precision,.yearand.overlaps.
Audit for
.coding/.system/.code/.idreached directly off an accessor named in the table above. That is the shape that breaks.
- BREAKING:
validate()now reports an ERROR for any element that is not part of the resource type in R4. Previously an unrecognised top-level key was ignored, so a typo (effectiveDatetimeforeffectiveDateTime), a field carried over from another type, or a hand-built resource with an invented element all validated clean and were then rejected or silently dropped by the store. The message says which of the two you should expect. Consumer action: run your existing corpus throughvalidate()before upgrading -- resources that passed may now report errors, and every one of them names something the store was never going to keep.fhir_commentsis exempt: it is a serialization convention rather than an element, and is legal anywhere.
- BREAKING: twenty-two required bindings are now enforced at runtime that previously were not.
They were on the frozen "cannot verify" allowlist because the vendored IG carried no snapshot to
read the binding from; vendoring the snapshot resolved it, and the allowlist drops from 47
entries to 32. The twenty-two are the base-R4 required bindings on
AllergyIntolerance.criticality/.type/.verificationStatus,CarePlan.intent,CareTeam.status,Composition.confidentiality,Coverage.status,DetectedIssue.severity,Device.status,DocumentReference.status,FamilyMemberHistory.status,Flag.status,Goal.lifecycleStatus,ImagingStudy.status,Invoice.status,List.mode/.status,Location.mode,MedicationStatement.status,QuestionnaireResponse.status,Specimen.statusandSubscription.status. Consumer action: a code outside one of those value sets now raises where it used to pass. These arerequiredbindings in base R4, so such a value was never conformant -- what changed is that this library can now see it.
-
BREAKING (TypedFhir Python): five public modules moved, and there are no forwarding shims.
typed_fhir.fuzzy,typed_fhir.unitsandtyped_fhir.vocabularyare nowtyped_fhir.values.fuzzy,typed_fhir.values.unitsandtyped_fhir.values.vocabulary;typed_fhir.import_istyped_fhir.ingest.import_; andtyped_fhir.facade_diagnosticsistyped_fhir.diagnostics.facade. The root package had accumulated a module per concern and its own headers had stopped describing it, so each cluster is a directory now and the root is entry points and primitives. The TypeScript port moves the same way (values/,ingest/,lib/,diagnostics/,validator/); its published subpath exports are unaffected except./ig-docs, below. C# is untouched --Values.csbecameValues/, and the namespace is stillTypedFhir.Consumer action, and it divides in two. Four of the five re-export everything they hold from the top level, which did not move:
from typed_fhir import FuzzyDatetime, normalize_quantity, SYSTEMS, partition_everythingworks exactly as it did, and the top-level import is the supported surface. Only a SUBMODULE import breaks, and it breaks loudly --ModuleNotFoundErrorat import time, not a silent behaviour change. The exception isfacade_diagnostics:to_issue_codeandfacade_to_issueshave no top-level alias and must be reached attyped_fhir.diagnostics.facade. Grep for the five old paths.
- BREAKING (TypedFhir TypeScript): the
@awaremd/typed-fhir/ig-docssubpath export is removed. It was published as a package entry point in 1.14.0, when the renderer behind it became how this site's FHIR reference pages are built. It is a GENERATOR rather than a runtime API: it reads the compiled IG off disk, is Node-only, and its output is this specification's own pages. It now lives with the other generators, atcodegen/ig-docs/, and is not published. Consumer action: if you import it, pin0.14.0-- it is not returning as a supported entry point, and a generator that reads our build tree was never a contract we could keep. The remaining subpaths are unchanged:.,./ingest,./ingest/ndjsonand./build-diagnostics.
-
BREAKING (behaviour): the compartment checker no longer calls a resource malformed for having no identifier, unless the IG says it must have one. Rule A6 raised an
errorfor every resource type without a Nexus EMR identifier, on the strength of the guide sentence "Every resource stored in MedPlum must have a unique identifier with this canonical URL". Measured against the profiles, exactly one type requires one --List, vianexus-template-list(identifier 1..*). Every other profiled type declaresidentifieras0..*or0..1, and 1.19.0 deliberately relaxedTask.identifierandComposition.identifierprecisely so that populations which cannot carry one -- a human-created Task, an unidentified Composition -- are representable. The checker went on reporting those as errors, which is the checker disagreeing with the specification rather than enforcing it.Severity is now taken from the IG:
errorwhere a profile requires an identifier,warningwhere it permits absence, with the message saying which. Consumer action: if you gate onvalidateCompartment/validate_compartmentreturning no ERRORS, resources you previously had to special-case will now pass. If you gate on the issue LIST being empty, a warning will still stop you -- filter by severity. Rule B1 is unchanged: an identifier that IS present must still carry the Organization's product-instance, and a mismatch is still an error.All three ports agree on this as of 1.20.0. The C# checker was left on the old hardcoded verdict when TS and Python moved, so the same Task validated clean in two ports and hard-errored in the third; it now takes severity from a generated table derived from the same profiles the other two read at runtime. C# also now reads a
0..1identifier-- a single object rather than a list, which is its cardinality on QuestionnaireResponse, Composition and Bundle -- where it previously saw such a resource as carrying no identifier at all and reported A6 against it.Still owed, and stated rather than quietly resolved: the identifiers guide page still asserts the blanket requirement, and says it "is specified on each Nexus EMR resource profile as an Invariant, so is enforced when we do validation". No such invariant exists on any profile. What would settle it is a decision on whether the requirement is real -- in which case the profiles should carry it -- or aspirational, in which case the page should say so.
- BREAKING (TypedFhir C#):
EffectiveDateis aFuzzyDate, not astring?. Every profile's synthesized effective date,ClinicalResource.EffectiveDate,Metadata.EffectiveDateOfand thedatecolumn ofToRows()all move. TS and Python have returned the typed value since the accessor was introduced; C# was left on the raw string across all 45 profiles under a comment saying "C# has no type alias -- the string IS a FuzzyDatetime", which stopped being true in this release, because this release ADDSValues/FuzzyDate.cs. The port was internally inconsistent as well: its 32 per-field date accessors were already typed with it. Consumer action: mostly none --FuzzyDateconverts implicitly to and fromstring, so assignment, interpolation and comparison keep working. Code that names the type (string? d = x.EffectiveDate;is fine; aList<string>built from a LINQ projection is not) needs?.Value. In exchange the C# accessor answers.Precision,.Year,.Overlapsand the rest, which is what the other two ports have been able to do all along. Python'seffective_date_ofreturn annotation saysFuzzyDate | Nonenow too -- it always returned one at runtime,FuzzyDatebeing astrsubclass there.
- BREAKING (TypedFhir Python):
field_names()reports only the accessors you can SET. It listed every property, including derived read-only ones (display,effective_date,fingerprint,raw,type_, and per-profile readers likevalue_text). That list is printed verbatim bynew()as the answer to "what can I set", so a caller who read it and passed one back got a bareAttributeError: can't set attributecontradicting the list they had just been given.new()now names a read-only field and says to set the element it derives from. Consumer action: code enumeratingfield_names()to discover accessors for READING should read the class instead; the method answers a write-shaped question and now says so.
-
BREAKING (TypedFhir Python): four ways an idempotent write was not idempotent, or not safe. All four are in the client's write path and all four are silent in the direction that costs you data, so none of them announced itself.
The request timeout was disabled rather than inherited.
timeout=self._timeoutwas passed to httpx unconditionally and the docstring claimedNone"leaves it to the transport". httpx reads an explicittimeout=Noneas NO TIMEOUT -- inheriting is what itsUSE_CLIENT_DEFAULTsentinel is for. So the DEFAULT configuration stripped the transport's timeout from every request, and a server that accepted a connection and then went quiet hung the caller forever. The kwarg is now sent only when a timeout was actually asked for. Consumer action: if you relied on requests never timing out, set it explicitly.Search values were never escaped.
,|$and\are FHIR search operators -- OR, token separator, composite separator, escape -- so a literal containing one changed the query instead of being matched by it. A business key with a comma in it asked for "this OR that" and got the wrong resource back, silently, which forsave_by_business_keymeans updating a resource that was never yours. Escaping is a named helper rather than something applied to every parameter, because those separators are how composite values are SPELLED: blanket-escaping would breakidentifier=system|valuefor every caller composing one.Composition.identifieris 0..1 and the client wrote a list. Almost every resource is0..*, so the list was written unconditionally;CompositionandQuestionnaireResponseare the two profiled types where that produces a non-conformant resource.
- BREAKING (behaviour): a leading
#no longer makes free text a coding token.codeableToTokens({text: "#1 Diabetes mellitus, type 2"})round-tripped to{coding: [{code: "1 Diabetes mellitus, type 2"}]}-- the text lost and a code invented -- because the empty-alias form (#active, a coding with no system, which FHIR permits and source systems send) was recognised from the leading#alone.#1 ...is a live problem-list convention. The systemless form now also requires a code-shaped remainder: non-empty, no whitespace. Consumer action: a systemless code containing a space is now carried as text rather than as a code. That is the deliberate direction of the trade -- losing a code is recoverable from the text beside it, and a fabricated problem-list code is not.
- TypedFhir:
contain()deep-copies the resource it carries. Its contract has always been that the caller's resource is copied rather than adopted -- containment assigns a localid, and rewriting an identity the caller still holds is not something this layer may do. The copy was one level, which protected the top-levelidand left every nested object (code,coding,note,extension) shared between the caller's resource and the contained copy, so a later edit to either was an invisible edit to the other. Consumer action: none for correct code, but if you relied on mutating a resource AFTER containing it and seeing the change inside the container, that never worked in the direction you think and now does not work at all -- contain it after it is complete, or editcontaineddirectly.
FuzzyDate's part accessors no longer throw on an unparseable value. The type documents that a read never throws, because a server can send a dateTime the grammar does not cover and a getter that threw would make a whole chart unreadable over one field. The part getters did not hold to it:FuzzyDate("unknown").yearraisedValueErrorin Python, threwFormatExceptionin C# and returnedNaNin TypeScript. All three now return the absent value. A partly unreadable value still reports the parts it plainly has --"2024-XX-15"is year 2024, no month, day 15.
- An
Annotationnow carries the element it was decoded from, so a note thread round-trips whole. The type liftstext,authorandtime, and re-emitted only those -- sotask.note = task.note.append(...), which assigns the whole element back, strippedid,extensionand anything else from every turn already on the thread. Nothing changes for an annotation this library built; a decoded one keeps what it arrived with.
Reference's external and display-only forms have a wire form in every port. In Python both emitted the literal string"None#None"-- silently, from the pydantic serializer,to_conciseandencode_value, for any chart holding an absolute-URL reference or a display-only participant -- and neither decoded. In C# the display-only form emitted{ display }alone, which no port could read back as a Reference; it now emits the{ identifier: "", display }object TypeScript has always emitted. Andurn:uuid:/urn:oid:references -- how a transaction or document Bundle names its own entries -- are now recognised as absolute in all three ports, where the old://test missed them and they decoded as display-only or were dropped.
- TypedFhir (Python): subscription date matching is correct on Python 3.10 again, this
package's declared floor. An offset written without a colon (
+0500, which R4 permits) could not be parsed on 3.10 -- full ISO-8601 parsing arrived in 3.11 -- and the failure was silent: the value fell through to the raw-text comparison path, so an event carrying such an offset simply did not match a criteria window it should have. Silent non-delivery, indistinguishable from nothing having happened, on the exact code path that exists to prevent it. Offsets are now normalised before parsing, so both spellings of one instant are indistinguishable to a query on every supported interpreter.
Well.Services.TypedFhir.Firelyrejoins the version line. It had drifted to 0.8.0 against a core of 0.14.0 -- six minor versions -- under a comment asserting it was versioned in step with the core. It caught up to 0.14.0 in this line of work and ships 0.15.0 with the other three. The adapter converts Firely R4 POCOs into the DOM the core consumes, so the two are a matched pair by construction; a consumer taking 0.8.0 had no way to see the core had moved eight releases on. TheHl7.Fhir.R4dependency is unchanged and stays pinned to matchWell.Services.FhirR4, deliberately. All four published packages now carry one version and a gate holds them there.
- TypedFhir's generator entry points collapse to one:
npm run gen:all. The nine per-step scripts are removed from the publishedpackage.json--gen:ig,gen:facade,gen:facade-py,gen:facade-cs,gen:ig-docs,gen:subscriptions,gen:patient-compartment,gen:facade-parityandgen:cache-parity. Each read the compiled IG and trusted that something else had compiled it correctly, which held only while a vendored copy of the IG was committed -- and this release deletes that copy.gen:allstages, compiles and generates in the one order that works, and now recompiles when the tree it would otherwise reuse has been rewritten underneath it: the IG Publisher re-runs SUSHI in the same directory without--snapshot, which strips the snapshot from every StructureDefinition and leaves the generators without the effective element view they read. Consumer action: none, unless you invoked one of the nine;gen:allreplaces all of them and is cheap to run repeatedly.
- Consumers pinning a floor: the NexusHarness POC's build-time requirement is
typed-fhir[sources]>=0.12,<1.0, raised from>=0.4. Its runtime guard has refused anything below 0.12 since that line deleted public types fromtyped_fhir.sources._base; the build-time pin had not moved with it, so a resolver could select 0.4-0.11, the build would succeed, and the service would then refuse to start. The two halves now agree and a test holds them together.
Removed¶
Task.relevantHistoryis no longer profiled. It carried a Must Support flag and a "MERT requirement" note, and what that bought was a hand-maintained list of back-pointers to Provenance resources that a consumer can already find by searching Provenance for a Task target. The distinction it forced -- notes are what people SAID,relevantHistoryis what HAPPENED -- is a real one and is kept, in the prose besidenotewhere it belongs. This does not forbid the element. Base R4 still defines it, this IG's own examples still carry it, and a consumer writing it stays conformant; what is gone is the obligation to maintain the list. Consumer action: none, unless you were populating it only to satisfy Must Support.
Fixed¶
ValueSet/nexus-emr-note-classificationresolves again, which is what 1.19.0 said it would. That release retired the set in favour ofnexus-emr-composition-classificationand told readers the page "stays published withstatus: retired, so an old link resolves to an artifact that names its successor instead of returning 404". The rename had been done by deleting the file, so there was no artifact to resolve to and the URL returned nothing -- which reads as a broken site rather than as a deliberate replacement, the one outcome retiring an artifact exists to avoid. The retired ValueSet is published in this release:status: retired, naming its successor, carrying the single code (LOINC34109-9) it held at the moment of the rename. Its content is FROZEN, not maintained -- duplicating the replacement's list would create a second definition that can drift from the first, and frozen and empty are different things. No profile binds it. Consumer action: none if you have already rebound. The stub will be dropped once no published version of this IG still references the old URL.
- The
Compositionprofile's dating guidance still calleddateits effective date, which stopped being true at 1.17.0. That release changed the declaration toevent.period.start | date-- the clinical date first, the editing time only as a fallback -- and three published places were never updated to match: the FSH comment sitting directly above the declaration, thedateelement definition (which compiles into the StructureDefinition and into every TypedFhir port's generated documentation), and the profile page's "Dating a note" section, which describeddateas the effective date and did not mentioneventat all. All three now state what the profile declares, and the page leads with the clinical date instead of the editing time. No constraint changed and no consumer action is required; this is what the profile has meant for three releases.
- All five Composition examples now carry
event. The profile tells readers to sort a clinical timeline byevent.period.startrather than bydate, and then demonstrated that nowhere: every published example was dated only bydate, including the amended note whose entire purpose is to show thatdatemoves to the amendment while the clinical date does not. The examples now cover the three shapes that matter -- a point in time carrying the encounter it documents inevent.detail, an interval withendfor a summary spanning two visits, and an amended note whoseevent.period.startstays on the telephone call whiledatemoves 85 minutes forward.
-
Both OSCAR note converters wrote the clinical date into
dateand emitted noevent, so every converted note was undatable on a clinical timeline.typed-fhir'sOscarRestSourcemapped OSCAR'sobservationDate-- the date of the CARE, which a user sets and which back-dates to the visit -- straight ontoComposition.date, the editing time, andOscarFhirGatewaySourcecarriedDocumentReference.dateacross while droppingcontext.periodon the floor. Both now populateevent.period, anddatecarries the editing time it is defined as (updateDatefor the REST source, stillDocumentReference.datefor the gateway).eventappears only where the source states a clinical date; nothing is invented.This is visible to consumers.
Composition.dateon a REST-converted note now reportsupdateDatewhere it previously reportedobservationDate, andeffective_date_of-- which has walkedevent.period.start | datesince 1.17.0 -- now finds the first arm rather than falling through to the second. Concretely,last_encounter_note_datereturns the visit on a converted chart where it used to return whenever the note was last edited. Consumer action: if you were readingComposition.datefrom these two sources as the clinical date, readevent.period.start(oreffective_date_of) instead; that is what it now is.
[1.19.0] - 2026-08-21¶
Generated by TypedFhir 0.14.0. Bumped from 0.12.0 (which generated 1.18.0) -- 0.13.0 is
skipped, not missing, matching this IG's own absent 1.13.0. This release brings
the Python FHIR client into line with the frontend's fhir-client.ts and adds the surfaces the
harness writes through (the typed 412, the business-key write, vread/history, the attachment
client), gives PatientCache the client-bound half the frontend's stores have, adds the facade
constructor (new() / missing_required()), curates subject/patient, and teaches Reference
the logical and contained forms. All three manifests move together -- ts/package.json,
python/pyproject.toml and dotnet/TypedFhir/TypedFhir.csproj.
Two accessor changes reach what writers type. Complex datatypes are curated -- Annotation,
Period, Address, HumanName, ContactPoint, Attachment, Money, Dosage and Identifier
gain structured accessors across all three ports, 29 elements in all, so a Task.note turn is no
longer hand-assembled as raw JSON on the harness's hot path and Patient.name/telecom/address
are typed for the first time. And a Coding with a required binding now curates as a bare
code (encounter.class_ = "AMB"), the same trade the CodeableConcept branch already makes: the
accessor writes the system from the value set, so a caller cannot mistype or omit it. That reaches
exactly one element -- Encounter.class is the IG's only top-level Coding with a required
binding.
A contained reference now has a wire form, #proposed, where to_wire() used to raise. The
base contract is that every value can re-emit its wire form and the generic encoders call it
unconditionally, so a structure holding one -- which is what contain() hands back -- failed on
the serialization path rather than where the reference was built. Spec §5.5 gains the form; it is
unambiguous against Type#id by the absence of a type before the hash, and FHIR JSON is unchanged
(the codec still leaves contained references verbatim).
Consumers pinning a floor: the NexusHarness POC's TYPED_FHIR_FLOOR moved to (0, 12).
0.12.0 deleted public types from typed_fhir.sources._base inside a 0.x bump, which broke the
"every 0.x is rapid iteration" premise that guard was written on; a validated window is replacing
the floor.
Versions here are sequential, not SemVer -- see the note at the top of this file. Read these three before consuming this release:
- Two published vocabularies were renamed, and both breaks are in stored data. The suggestion
code on
Task.codeis nowsuggestion, notai-suggestion: a search forTask?code=...|ai-suggestionreturns nothing after this release, and stored tasks carrying the old code failnexus-sugg-2until migrated. The note-classification value set is nownexus-emr-composition-classification: the old canonical is no longer bound by any profile, so rebind. Its page is NOT removed --ValueSet-nexus-emr-note-classification.mdis still published,status: retired, so an old bookmark still resolves and says what replaced it. Neither rename is recoverable by tolerating absence -- both need a migration.
- Four elements that were required may now be absent, and readers must tolerate all four.
Task.for,Task.identifier,Composition.identifierandNexusEmrCoreCareTeam.subjectall relax in this release, each because the profile grew a population the requirement made unrepresentable. A reader that treats absence as malformed will reject valid records; one that lists a patient's care providers without filtering onsubjectwill now show the nursing pool as a clinician. Going the other way, everyTask.noteturn now REQUIRESauthorandtime, so a writer that emitted unattributed or undated turns must populate both.
- A given seed does not reproduce the 1.18.0 dataset. The generator no longer opens a task's
thread with a turn restating its
description, and arequestedfollow-up task now emits no turns at all. Regenerate and re-baseline; 1.18.0 stays archived for byte-fidelity.
Changed¶
-
BREAKING: the suggestion code is
suggestion, renamed fromai-suggestion(CodeSystem/nexus-emr-suggestion, used onTask.code). The old name asserted something the code does not carry. What made a suggestion "AI" was never this code — it isTask.requester, a logical Device reference underNamingSystem/nexus-harness-graphnaming the graph that produced it. A deterministic CDS rule produces the same Task shape with a different requester, so the old name made every CDS recommendation read as machine-learning output.Consumer action: a search for
Task?code=...|ai-suggestionreturns nothing after this release; use...|suggestion. Stored tasks carrying the old code do not match thenexus-sugg-2invariant and must be migrated. ReadTask.requesterwhen the producer matters (AI graph vs CDS service) andTask.codewhen it does not. An earlier revision of the CodeSystem argued against this rename on the grounds that the code is a stored value — that traded a permanently misleading vocabulary for one migration, and while this specification is pre-release the trade was wrong.
-
BREAKING: every
ClinicalTask.noteturn now requiresauthorandtime. Each turn is typed toNexusEmrTaskNote, which makes required two elements that are optional in base R4. Consumer action: a writer that emitted unattributed or undated turns must populate both on every note, or the resource no longer validates.This entry used to lead with what did NOT change -- "typed to a profile and stays
0..*; the ask isdescription" -- and put the breaking half in the middle of the paragraph. That is worth correcting rather than quietly rewriting, because the entry's own shape caused the damage. This file's preamble states the contract: a breaking change is named as breaking, in bold, with the consumer action spelled out. A reader skimming bold headlines for what would break saw a reassurance about cardinality and moved on. The note-model fallout in this release traces back here.The cardinality does not change. An earlier revision of this release made
note1..*and declarednote[0]the opening ask. That is withdrawn before it ships, and the reasoning is worth recording because it was wrong in a specific way: it treateddescriptionas a mere label whendescriptionis required on this profile and IS the ask — what is wanted and why the task exists, stable for the life of the task. Requiring a turn on top of it bought nothing and cost every writer a duplicated one, storing the same statement twice and opening the thread with something nobody actually said.noteis what has been said since the task was created; a task nobody has commented on carries none, which is the ordinary state of new work.Consumer action: read
descriptionto answer "what is this task about" andnoteto answer "what has been said about it". Render an empty thread as new work, not as a malformed record.
-
BREAKING (seed data): a given seed no longer reproduces the 1.18.0 dataset. Two generator changes alter what a thread contains, and both follow from the
Task.notemodel above rather than from any new decision: the generator no longer opens a task's thread with a turn that merely restates itsdescription, and arequestedfollow-up task now emits no turns at all. A task nobody has commented on carries no notes, which is the ordinary state of new work.Consumer action: regenerate and re-baseline anything pinned to generated output -- fixtures, golden files, benchmark stores, screenshots. The change is in the CONTENT for a given seed, not in the generator's determinism: same seed still means same dataset, and
test:seed-stabilityin the generator suite is what holds that. 1.18.0 stays archived for byte-fidelity if you need to diff against the old shape.This was listed among the three things to read before consuming 1.19.0 but had no entry of its own down here, so the detail a reader needed in order to act on it -- which two behaviours changed, and that determinism itself is intact -- existed only as a one-line summary.
-
TypedFhir (Python) gains a FHIR REST client (Jira PSD-1141, "Writeback of compositions" — not to be confused with GitHub PR #1141, which is the 1.18.0 release this branch is stacked on; the numbers collide by coincidence).
typed_fhir.sources.fhir_client.FhirClientports the frontend'sfhir-client.tsAPI to Python — same operations, snake_case names, same error contract (a failed search returns a Bundle wrapping theOperationOutcomeper R4; a failed direct request raises it). This is what makes generated-summary writeback possible:create_resource/update_resourcearrive as part of a client rather than as another hand-rolled POST.No third-party FHIR library, deliberately: they all own their own HTTP call, and this package's determinism depends on that not being owned (connectors build requests and parse responses with pure code; only bytes cross the
Transportseam, so a run replays against the frozen vectors). Credentials here also resolve per request from context — the harness sets a delegated bearer and tenant per run — which a client that binds authorization at construction cannot express.Both read postures moved onto the client with their measured knowledge intact: the degrading compartment sweep (one broken type must not cost the chart) and the loud single-query load (a truncated inbox is missing work items, not a smaller page), plus
next-link rebasing with all three of its hard-won exceptions, the HTTP-200-carrying-a-refusal check, and a page cap that is always reported. Public API unchanged:search_paged,next_linkandrebase_nextkeep their names and import paths. PHI: error messages carryissue.codeonly — a HAPIOperationOutcomeechoes the offending resource intodiagnostics, so the text rides on the exception, never in a log-bound message.
-
BREAKING (TypedFhir Python): the FHIR client's API is brought in line with
fhir-client.ts. Its docstring has always said it mirrors the frontend client; it was about 70% true, and the differences were spelling rather than substance.get_resource_by_linknow takes(resource_type, url)like the reference (and like every other method here) rather than the reverse;update_resourcetakes aFhirUpdateOptionsbag instead ofif_match: str | bool;search_errorsandwrap_operation_outcome_in_bundleare instance methods, not@staticmethod; and every search entry point accepts an ordered sequence of pairs, not only a mapping — a date window (date=ge…&date=lt…) was previously expressible in exactly one of the four.Consumer action: update those three call shapes. There were no consumers outside this package's own tests when the change landed, which is why it was made now rather than deprecated.
-
TypedFhir (Python) gains the write and read surfaces the harness needs, all mirroring the frontend:
FhirPreconditionFailedError(a 412 is the one write outcome a caller must handle differently — nothing was written and retrying unchanged will be refused again — and it was previously only recoverable by sniffing a status code);save_resource, routing create-vs-update onidpresence;bundle_entries/bundle_entries_or_raise/has_error_issues, mirroringBundleUtils, where the_or_raiseform puts the HTTP-200 refusal check in the same call as the entry filter so it is no longer one a caller can forget;get_version/get_history_page/get_history; andFhirDocumentDataClientfor attachment bytes, which streams and enforces its size cap while downloading rather than after.save_by_business_keyis the AgentWriteFhir contract in one call — search by identifier, then update in place or create — so re-running a capability updates the same Task or Composition instead of piling up a rival. It writes the key onto the resource (a key you searched by and did not store finds nothing next time), carries the storedmeta.versionIdforward so the read-then-write stays conditional, and refuses when a key matches more than one resource rather than picking a side.FhirClient(base_url)is now the whole call: the transport defaults, and credentials default to the delegated bearer and tenant in the ambient context.
- TypedFhir (Python):
PatientCachecan drive a client, not only be handed a list of resources —search/load_all/count/load/get_or_load/load_by_link/load_version/load_history/save/save_as_global/delete_by_id/upsert/evict/clear, pluson_write/on_read/on_deletelisteners. This is the frontend'sFhirStoreBasehalf that had no analogue here. Two things the stores need that it does not: a store registry (one cache holds every type, so an_included resource lands beside the one that included it with no routing) and per-type store classes (the type is an argument and the local query isresources()). A search the server REFUSED raises rather than merging nothing — a tenant refusal arrives as HTTP 200 with an empty searchset, and merging that silently leaves a cache reporting an empty chart.
- TypedFhir: starting a resource is a constructor call.
Observation.new(status="final", …)andnew_resource("Task", …)replace hand-writing{"resourceType": "Observation"}as the entry point to a typed API. Fields pass through the setters, so a code outside its required binding is refused at construction. What comes back is deliberately incomplete — you are still building it — andmissing_required()answers "what does this still need", returning the IG path plus the accessor to set it with, read off the profile validator rather than the curated field table so it cannot report "nothing missing" about an element the facade does not happen to type.
- TypedFhir:
subjectandpatientare curated. No profile in the IG had asubjectaccessor — they sat on the generator's global skip list, a rule that only ever held for READING (inside a patient cache the subject is a given). It stopped holding when the facade gained a construction path: a new Observation must carry a subject, and with no accessor the only way to set one was raw JSON. 24 profiles gain a typedsubjectand 6 a typedpatient, in all three ports.identifierstays uncurated — it is reached throughsave_by_business_key.
-
BREAKING (behaviour):
Referencedecodes the LOGICAL and CONTAINED forms. It handled onlyType/id, so a reference by business identifier — with no resource on this server — read asNonefrom the typed accessor and had to be written through.raw. That is the shape the AI stack always produces (Task.requester/Task.owner,Composition.author), sotask.ownerwasNoneon every Task the harness had ever written. Spec §5.5 has specified the logical form all along; this is the implementation catching up with its own normative text.Reference.to(...),Reference.to_identifier(...)andReference.to_contained(...)are the three constructors, in Python, TypeScript and C#.Consumer action:
reference_from_fhirnow returns a value where it previously returnedNone/undefinedfor identifier-based and#fragmentreferences. Code branching on "no typed form" to reach for the_rawtwin will now take the typed path — which is the point, but it is a behaviour change. Four shapes still stay raw deliberately, because no faithful typed form exists: an absolute URL (rebuilding it fromType/idwould silently retarget it at this server), a display-only reference, anIdentifierricher than{system, value}, and a non-resolvable relative reference.
-
TypedFhir ships a cross-verifier (
backend/lib/TypedFhir/crossverify/). One scripted server, a catalogue of edge cases, and five FHIR clients answering the same questions: TypedFhir, fhirpy, smart-on-fhir/client-py, the frontend's own@awaremd/sc-fhir-clientand bare SMART client-js underneath it. It is worth knowing about as a CONSUMER of this library, because it is the evidence behind claims this package otherwise just asserts -- that a paging link cannot walk your bearer token to another host, that aresourceTypecannot rewrite a request path, that an HTTP 200 carrying aforbiddenoutcome is not reported as an empty chart. Each case states the question, why it matters, and how a verdict is decided;MATRIX.mdis the output.Deliberately not in CI (it installs four third-party FHIR clients, and its answers change when other people release) -- it is step 15 of the owner-track pre-PR gate. Verdicts about the other libraries are recorded and never asserted on: the suite fails only when TypedFhir is the one behaving badly.
-
Known gap, stated rather than papered over:
Compositionpublishes nosearchParam. The generated-summary design needs three (subject,category, andidentifier), andidentifieris load-bearing — without it a producer cannot find the summary it wrote last time and update-in-place silently becomes duplicate-on-every-run. None of the three is published, and the guide says plainly that these are requirements, not guarantees. Measuring them against the store and declaring what works is owed.Be precise about what is and is not declared, because the two are easy to conflate. The CapabilityStatement DOES list the
search-typeinteraction onComposition— as it does on all thirty resource types it covers — so a reader of the statement alone will conclude that searching Composition is supported. What it does not carry is asearchParamblock naming which parameters work; todayListis the only resource with one, and that block exists because theidentifierparameter was measured against the NativeFHIR store on 2026-08-19 and its documentation records the measurement. The IG's rule — do not publish what has not been measured — therefore governs the PARAMETERS, not the blanket interaction list. An earlier revision of this entry said "nothing is declared here yet", which read as a claim about the whole statement and was contradicted by the statement's own#search-type.
-
BREAKING:
nexus-emr-note-classificationis replaced bynexus-emr-composition-classification, and the Composition profile now models three document kinds. The old value set carried its assumption in its name — it classified notes, because the profile modelled one thing — and the profile itself had already predicted the expiry date in prose: "that stops being true the moment a Composition is written for anything else — a generated summary, a letter, an assembled document." Both arrived. Letters shipped first and shipped off-vocabulary: PSD-1107 classifies them with a locally-mintedhttp://services.well.com/fhir/categories/letter-templatecode, because the IG offered nothing usable. Generated summaries are next and are on the demo's first path.The set now carries three existing LOINC codes, no local code system minted (the IG's standing rule, and why the old set used 34109-9 rather than coining one): 34109-9 "Note", 34133-9 "Summary of episode note", 51852-2 "Letter". 34133-9 rather than IPS/PS-CA's 60591-5 "Patient summary Document", because that is a full patient-summary-for-exchange, a different artifact from "what changed since you last saw this patient". 51852-2 ratifies the
typethe app team was already writing.Consumer action: rebind to
.../ValueSet/nexus-emr-composition-classification. The old canonical is no longer bound by any profile, but its page has NOT been removed —ValueSet-nexus-emr-note-classification.mdis still published, nowstatus: retired, keeping the one code it ever held so a reader arriving from an old link lands on an artifact that says what replaced it rather than on a 404. (An earlier revision of this entry said the page 404s. It was going to; retiring it instead was the better answer and is what shipped.)Populate
categoryon every Composition — a surface that ignores it will render a machine-generated summary as though a clinician authored it, andauthordoes not disambiguate (a scribe-drafted note also carries aDeviceauthor). Theletter-templatelocal category should migrate to 51852-2; coordinate with the letters work, since it is already in seed data.But do not write a reader that assumes
categoryis present and in this set. The profile makes it0..*with a preferred binding, so an instance that omits it entirely, or carries a code from somewhere else, is fully conformant and will not be flagged. That is deliberate — a source EMR's own classification should be neither rejected nor discarded — and the release ships a live example of it: the letters PSD-1107 already wrote are classified with a locally-mintedletter-templatecode and sit in seed data today. So branch on the LOINC codes when they are there, and fall back totypeand then to treating the document as an unclassified note when they are not; do not treat absence as malformed. Whether this element should instead be tightened to1..1andrequiredonce the letters migrate is undecided, and what would decide it is the migration: while an off-vocabulary code is still in stored data, tightening the binding would make existing conformant records invalid.A worked letter ships with this release:
NexusEmrComposition-letter-example.json, a referral classified 51852-2 on bothcategoryandtype,finalwith a legal attester, and carrying the letter itself as authored XHTML insection.text— letterhead, salutation, body and signature block are all narrative, exactly as composed. It is the counterpart to the generated-summary example and shows the contrast the attester exemption turns on: nothing attests to a machine-generated summary, but a letter goes out over a clinician's name and is never exempt.
- Generated summaries are modelled, with a guide topic. A summary is a Composition the harness
writes and maintains:
subjectis always the patient,authoris the graph as a logicalDevicereference, and the business key undernexus-harness-keyis what makes it update in place — one since-last-visit summary per patient, forever, regenerated by replacing content rather than by minting a second resource. The key therefore carries no period; superseded content is the resource's own version history. Semantically distinct summaries (day-sheet, since-last-visit, whole-chart) share the one FHIR classification and are separated by the capability prefix in the key, which is the thing that actually differs between them. New:features/generated-summaries/in the guide, and a worked example.
composition-signed-has-attesternow exempts generated summaries, and the exemption is mutation-tested (a summary passes, an unsigned final note still fails, drafts still exempt). Nothing attests to a machine-generated summary:finalmeans "this generation is complete", not "a human is responsible for this". The alternatives were worse — permanentlypreliminaryis false, and naming an attester who never attested fabricates a medico-legal signature. Letters are not exempt. This is what widening a profile's population costs: the invariant encoded an assumption true of every document the profile held when it was written, and the assumption had to be made explicit rather than silently broken.
- Swept the last places that still tied
Communicationto a Task. The ruling below is only useful if nothing contradicts it, so: theNexusEmrCoreCommunicationprofile now states outright that it is not the conversation on a Task (it is where a reader looks to check, and it was silent); and the AI-assisted-triage example was rebuilt onto the note model. That example carried a latent defect worth naming — itsfocuspointed at aCommunication, which is not in the typedfocustarget list, and nothing caught it because the validator does not enforce reference targets (FHIR-45). The patient's message is now a turn on the task (note[0], authored by the patient), which is what the one-rail model says it should be: there is no separate chart resource to point at, so the task carries nofocus. The ask itself is the task'sdescription.
- RULED: a task's conversation is
Task.note, andCommunicationis not used for task turns. Both were on the table and both were partly documented, which is worse than either. The turns areAnnotationrepeats on the Task, oldest first, and are what has been said since the task was created — the ask itself stays indescription, which this profile requires. Rationale recorded on the profile: a turn is not independently meaningful outside its task; "say something and hand it over" is one atomic write rather than two needing a transaction Bundle; and a single ordered repeating element needs no threading model, whereCommunication'sinResponseTo-vs-partOfquestion is explicitly unsettled. What this gives up is stated in the profile too — no per-turn security label, no per-turn Provenance, no structured attachment in a turn — with an additive graduation path toCommunicationif a consumer ever needs those. Guide correction: the design guide previously described task conversation asCommunicationresources with email-styleMessage-IDthreading. That section is superseded and marked as such.
- Guide correction —
Task.intenton a suggestion is#proposal, not#order. The design guide still asserted thatintent"stays#orderthroughout" with proposal semantics living only on the payload. That has been wrong since 1.16.0, whenintentgained its required two-code binding and became the discriminator between work to do (#order) and machine output to decide (#proposal). Corrected, with the consumer consequence spelled out: a reader that ignoresintentpresents unapproved machine output as though a human had assigned it.
-
Task.owneraccepts aCareTeam: a standing team can hold pooled work (PSD-949, adopted from PR #1148).ownerstays1..1— the one-owner rule is about the CARDINALITY, not the target type, andowneris still exactly one reference when it names the nursing pool. The mechanism this replaces wasgroupIdentifiernaming the work stream while some individual was named as holder, which could not say the true thing:groupIdentifiernames the stream, not the holder, so a person had to be picked in advance to stand in for a queue nobody had picked up. A team owner says who actually has it, and a member's claim becomes a real ownership transfer with real Provenance instead of a status change on a task that always named one nurse.groupIdentifierkeeps its own job (the batch or requisition a task belongs to) and is not the ownership mechanism.Organizationremains a non-target: a task waiting on a lab is owned by the human or team waiting for it.This reverses a correction made earlier in this same release, which said
CareTeamandOrganizationwere both barred and that "a team cannot be accountable; a person can". The half aboutOrganizationstands. The half about teams did not survive contact with the chart-task work, and the reasoning was wrong in a way worth recording: it attributed to the target type an accountability guarantee that comes from the cardinality.Consumer action: a surface rendering
ownermust handle four target types (Practitioner,PractitionerRole,CareTeam, logicalDevice); one that assumes a person will mis-render pooled and agent-held work. Note the asymmetry with the conversation — a task can be OWNED by aCareTeamorPractitionerRole, but neither can AUTHOR a turn (R4 permits onlyPractitioner | Patient | RelatedPerson | OrganizationonAnnotation.author), so a writer reusing the owner reference as the note author produces an invalid note.NexusEmrCoreCareTeam.subjectrelaxes1..1→0..1to make this usable, which is a change to a second profile and is called out separately here because it is easy to miss. The teams that hold pooled work — the nursing pool, the front desk, the referrals desk — are standing teams that serve the whole practice and have no single patient; leavingsubjectrequired would have made the new owner target unusable for exactly the population it was widened for. Two populations now live in that profile andsubjectis the discriminator: a team WITH a subject is that patient's care team (it backs the chart's Care Providers section), a team WITHOUT one is an operational work stream. A query listing a patient's care providers must now filter onsubjectrather than assume it, or it will show the nursing pool as one of the patient's clinicians.
-
Task.forrelaxes1..1→0..1, andTask.identifier1..*→0..*(PSD-949, adopted from PR #1148). BREAKING for readers: both may now be absent.forwas required "for clinical safety", and that is right for the population it was written for — a clinical task whose patient link is missing IS a safety problem, and every chart-created task carries one. But the profile also holds work that is genuinely about no patient (restock the fridge, complete the cold-chain log), and a requiredfordoes not make those safer, it makes them unrepresentable, which pushes them into some system the EMR cannot see.identifierfails the same way: a task born in the EMR has no external record to be identified against, the server-assigned id already answers "which task is this", and requiring a business identifier forces a writer to mint a meaningless one — which is worse than none, because it looks like a key into something.Consumer action: treat a task with no
foras a standalone work item, not a malformed record, and never infer the patient fromfocus. Tolerate tasks with no identifier at all. The expectation is now stated rather than enforced: chart-born tasks always setfor, and a task corresponding to a record in another system should still carry that system's identifier.
-
Composition.identifierrelaxes1..1→0..1. BREAKING for readers: it may now be absent. It was required when this profile modelled ONE document kind — the encounter note, the unit source EMRs key on and the gateway must map onto (OSCARcasemgmt_note), where an unidentified note cannot be reconciled across a sync boundary. That case is unchanged and is still the reason to populate it. What changed is the POPULATION: this release widens the profile to three document kinds, and a document born here with no external record to reconcile against has no identifier to carry. The server-assigned id already answers "which document is this", and forcing a writer to mint a business identifier produces a meaningless value that looks like a key into something — the identical failureTask.identifierwas relaxed for above. Letters had already shipped in exactly that shape, off-profile, because the requirement could not be met honestly; this is the profile catching up with what the widening had already made true.The obligation moves onto the element, stated per population rather than enforced profile-wide: a document synced from or reconciled to a source system SHOULD carry that system's identifier; a generated summary MUST carry its
nexus-harness-keybusiness key, or regeneration mints a second resource instead of updating in place; a document answering to neither MAY omit it.identifier.systemandidentifier.valueremain required when an identifier is present, and there is still exactly one slot — a prose invitation to carry a source 'raw code' as an additional identifier was wrong for a singular element and has been removed.Consumer action: tolerate a Composition with no identifier, and do not use its presence to tell document kinds apart —
categorydoes that. A letter or summary that will be revised should still mint one: a document with no identity cannot be updated in place, only duplicated.
-
Task.businessStatusis bound (extensible) to a published vocabulary, and the vocabulary is documented for what it actually means (PSD-949, adopted from PR #1148). NewCodeSystem/task-business-status(ok,needs-review) andValueSet/task-business-statuses, consolidating under the IG canonical base the codes previously carried on the legacyhttp://well.company/task-business-statussystem.needs-reviewships DEPRECATED, andbusinessStatusis reserved for what it is actually for. Settled 2026-08-19 with the chart-task author. The element is for CUSTOM LIFECYCLE SUB-STATES of the task itself -- a clinic that needs a nurse to pre-process incoming reports before a physician sees them transitions through its ownpre-reviewbefore the task becomes active in the physician's inbox.needs-reviewis not that. It is a flag on the chart element the task is FILED AGAINST, and the obvious reading is wrong in a way that makes the code look redundant -- the existence of a task already means work is needed.The workflow it was built for is an Ocean referral that was COMPLETED, then received a new message from the referring physician, and had to resurface. That is now modelled as a new Task on the same ServiceRequest, because once a task is completed it does not come back to life -- the same semantics as a user creating a task on a document for a colleague, only arriving from outside. Inbound data carrying the old code is transformed at the C3 gateway.
The code is RETAINED and marked
status = deprecatedrather than removed: roughly two years of stored data carry it and it must stay representable. Consumer action: do not write it. Do not confuse it with the AI/CDS suggestion review, which isintent = #proposalwith the decision onTask.status.okcarries no information an absentbusinessStatusdoes not; it exists so a writer can clear the flag in place, and readers should treat the two as the same fact.
- Guide correction — external systems as task owners. The guide showed
owner → Organizationfor external-system handoffs.Organizationis not a permitted target: a task waiting on a lab is owned by the human or team waiting for it. Also corrected: a lab-review workflow step that added its comment "via NexusEmrCommunication".
Added¶
-
New profile
NexusEmrTaskNote— the IG's first datatype profile, onAnnotation. A task turn carries enough load-bearing rules to deserve a name of its own rather than prose on the Task profile. It requiresauthorandtimeon every turn (both optional in base R4: an unattributed or undated turn is a statement in the record with nobody behind it, and turns are read as an ordered thread), and it is where machine authorship is defined and enforced.⚠
PractitionerRolecannot author a turn.Annotation.authorin R4 permitsPractitioner,Patient,RelatedPerson, andOrganizationonly — notPractitionerRole, even thoughTask.ownerdoes permit it. A task can therefore be owned by a role while every turn on it is authored by a person, and a writer reusing the same reference in both places produces an invalid note. R5 addsPractitionerRole; R4 does not. This is not hypothetical: it was caught by the reference-target test against the synthetic corpus, which had 80 role-authored turns.
-
Agent-authored note turns are now specified and enforced:
nexus-note-agent-author. An AI-harness graph can author a task turn, butAnnotation.author[x]in R4 permits onlyPractitioner | Patient | RelatedPerson | Organizationor a string —Deviceis not an allowed target, and a profile cannot add one, because profiling constrains and never widens (SUSHI refuses the attempt outright). So an agent author is written as an identifier-only logical reference:author.identifierunderNamingSystem/nexus-harness-graphnaming the graph,author.displaycarrying a readable name, and noauthor.reference.Reference.typeis omitted deliberately rather than set toDevice, which would assert a target the element does not permit; the identifier's system carries the identity instead.Chosen over an extension because an extension hides the author from every generic consumer, which is the wrong trade for a partner-facing spec: an identifier-only reference with a display degrades gracefully — a reader that has never seen this IG still gets a named author. The new error-severity invariant (on the Task Note profile) enforces exactly that shape and is silent on human authors, on
authorString, and on tasks with no notes. Verified by mutation: a literal reference fails, a missing display fails, and all three benign cases pass.
- The Guide gains a "Tasks, the inbox, and the AI harness" section, because implementers arrive
at this model from three directions and it only makes sense as a unit: one rail for human work,
agent-created proposals, and agent conversation; the inbox as a view over tasks rather than a
second store; assignment-to-agent as the resume signal; gap-keyed idempotency; and a ranked list of
what a consuming surface must get right. Agent identity is also now tabulated across the three
positions it appears in (
requester,owner,note.author) with thenote.authorexception and its R4 cause called out, plus why identity is the graph rather than the model, and why noDeviceresource is registered.
-
Machine-written tasks get an idempotency contract:
NamingSystem/nexus-harness-key.ClinicalTask.identifieris "any system" and (as of this release) not even required, which left the one population that genuinely needs a named system without one: a task the AI harness or a decision-support service writes on its own behalf. Nothing stopped a rule that runs nightly from filing the same proposal every night. The new system carries a business key whose value is derived from the gap the task addresses, never from the run that produced it -- a key containing a run id, thread id or execution timestamp changes on every execution and defeats the purpose it exists for. Patient-scoped keys must include the subject, since two patients can present the same gap; values are opaque and must not be parsed.For implementers: a producer searches
Task?identifier=https://fhir.apps.health/NamingSystem/nexus-harness-key|{key}before writing, updates the task it finds, and resolves that task when the gap no longer holds -- so a gap that closes on its own closes its own task. The same key travels onto the first-class resource when a suggestion is accepted, and that shared value is what makes traceback work across the promotion, since the accepted resource cannotbasedOnthe Task. Human-created tasks are unaffected — no system is mandated for them and no identifier is required at all;identifieris deliberately NOT sliced, so adding the key is additive for every existing writer and breaks nothing that ships today. Sibling ofNamingSystem/nexus-harness-graph, which names the actor rather than the work.
Deprecated¶
ValueSet/nexus-emr-note-classificationis retired, superseded byValueSet/nexus-emr-composition-classification(see the entry under Changed for why the classification widened). Retired rather than deleted: the page stays published withstatus: retiredand keeps the single code it held, so an old link resolves to an artifact that names its successor instead of returning 404. No profile binds it any longer. Rebind before the next release; a retired artifact is a redirect with an expiry, not a supported one.
task-business-status#needs-reviewships deprecated on arrival, in the same release that publishes the CodeSystem holding it. It is documented under Changed above; it is repeated here because a consumer scanning for what not to build on should not have to read a paragraph aboutTask.businessStatusbindings to find it. Use a new Task on the sameServiceRequestfor the resurfacing workflow it was built for.okis unaffected.
[1.18.0] - 2026-08-19¶
Generated by TypedFhir 0.12.0. Bumped from 0.11.0 (which generated 1.17.0): this release adds
the Python source-loader features (harness inboxes, the day-sheet cut, since= windowing and the
last-encounter anchor), the typed_fhir.subscriptions package, the Template profile on List
with its facades and armed invariants, and the three-port effective-date walker fix. All three
manifests move together -- ts/package.json, python/pyproject.toml and
dotnet/TypedFhir/TypedFhir.csproj.
Versions here are sequential, not SemVer -- see the note at the top of this file. Read these two before consuming this release:
- Effective dates CHANGE for Compositions that carry an
event. The walker fix below meansevent.period.startfinally wins overdate, as 1.17.0 declared it should -- so windowing, recency sorts and anything cached off an effective date will move for exactly the notes where the two dates differ. That is the fix landing, not a regression; recompute rather than reconcile. - A given seed does not reproduce the 1.17.0 dataset. The generator emits the letter template and the clinic-site Location, and profile identities moved. Regenerate and re-baseline; counts and shapes are stable, individual values and ids are not. 1.17.0 remains published and archived if byte-fidelity is needed.
Added¶
-
The Template profile's canonical is
nexus-template-list, renamed before first publication -- and the reason is worth every implementer's attention. The HL7 validator (and the publisher convention it follows) IGNORES any file whose name ends in-template.json, reading it as an authoring template rather than a resource. Measured with byte-identical files:StructureDefinition-nexus-template.jsonis "ignored due to error: Unsupported format"; any other name loads. Under the original id, every consumer runningvalidator_cliagainst this IG's package silently lost the profile -- including this IG's own conformance gate, which had been validating the template example against base R4 only. No released canonical moved: nothing template-related shipped before this release, and the NamingSystem canonical (NamingSystem/nexus-template) is a URL, not a filename, and is unchanged.With the profile actually loading, its invariants grew teeth AND a new one:
nexus-template-entries-resolvemakes a typo'd entry reference (#lettrfor#letter) a validation ERROR rather than a template that renders nothing. It uses full FHIRPath (substring/in/%resource), which TypedFhir's runtime subset cannot yet verify -- it is enforced by the HL7 validator and any conformant external validator, and the runtime gap is tracked as FHIR-58.
-
Templates are served, validated, and discoverable:
Listjoins the declared surface (FHIR-53, FHIR-52). Three things move together, because the profile's own gate said they must:- Declared.
Listenters the server CapabilityStatement (read, search-type, create, update -- no delete, matching the charting permission grant exactly) with the standardclinical-identifiersearch parameter, and the Template profile carriesSmartExposedNoDelete(list). The gate that had held this back asked for measurement first, and got it: the discovery queryList?identifier=https://fhir.apps.health/NamingSystem/nexus-template|was run against the live store -- served, and genuinely filtered (a bogus identifier returns zero, not an unfiltered list). - Validated. The Template profile's two error invariants were DEAD CODE: the facade set
admitted only
nexus-emr-core-*files, so noListvalidator existed and a nonconformant template passed every gate with an information-level "no profile" note. The facade set now admits the template profile by name;Listfacades generate in all three ports, and a template whose entry references a stored resource (or nothing) fails validation with the invariant's own message. Facade modules 43 -> 44. - Discoverable. The published NamingSystem and the instantiation recipe were corrected in the same release (the Bundle-era text and the missing identifier step -- see below).
For implementers:
list:read/list:writescopes now resolve, templates live in the patient compartment classification per R4 (CompartmentDefinition-patientlists List viasubject/source), and destroying a template is deliberately not grantable. - Declared.
-
typed_fhir.subscriptions: change notifications join the library (PSD-1039). ASubscriptionClientover a transport seam -- the Redis change stream (at-least-once, consumer groups, dead-letter stream) or a real FHIRSubscriptionrest-hook (FhirSubscriptionTransportNotificationSink) -- with criteria parsed and validated against the subscriptions CapabilityStatement at construction, so an unsupported criteria raises before the loop starts and never registers as a subscription that quietly matches nothing.
Hardened by four review rounds before this entry was written, and the last one matters to consumers: notification Bundles batching several events now deliver EVERY event (not just the first); a bare status ping is never delivered as a clinical event; on the rest-hook transport a failed handler dead-letters immediately (rest-hook cannot redeliver, so "retry by not acking" was a silent drop); criteria follow R4 escaping in full,
+is a literal, and identifier matching requires the:identifiermodifier (owner:identifier=system|value; keyword formowner__identifier=) -- the plain form is refused at construction because it matched over Redis and matched nothing on a conformant server. Dateeqis containment (date=2026-08-14does not match a resource dated2026) and string search is case-insensitive-prefix, both per R4, so client-side and server-side evaluation of the same criteria string cannot diverge.
- The reference loaders verify instead of trusting, measured against a live store (FHIR-52).
The cross-patient searches refuse to present a security refusal as an empty result (the
gateway answers a cross-tenant read with a 200 carrying an error OperationOutcome -- that
raises now), refuse truncation at the page cap, and gate the inbox loader's client-side
fallback on the exact status: 400 falls back, 404 raises, and a 403 -- both NativeFHIR's
answer to the unsupported
:identifiermodifier AND the gateway's insufficient-scope answer -- is disambiguated with a one-request probe before any scan is licensed.load_daysheettreats the provider as two identities (Practitioner AND PractitionerRole) on both the appointment and schedule legs, and takestz=(IANA zone) so a clinic's evening appointments survive UTC normalization.last_encounter_datereadscategoryORtypeagainst the IG's note codes, so OSCAR-converted charts (type-only, 11488-4) anchor correctly instead of answering "first visit" for every migrated patient; window comparisons normalize offsets first, so anAppointment.startanchor with a timezone offset windows correctly.
- The effective-date walker descends arrays, in all three ports (FHIR-52). The declared
precedence
event.period.start | date(1.17.0) named the one ARRAY-VALUED path in the table, and no port's path walker could traverse an array -- so the declaration was inert at runtime and every consumer silently windowed on the authoring date, the exact wrong answer it was written to prevent. Found live: a note carrying both dates returned the wrong one. Fixed with an identical first-present-wins array-descent rule in TypeScript, Python and C#, pinned by tests in each.
-
load_harness_inboxeson the Python source connectors: every AI-harness actor's inbox in one call (FHIR-52). All Task resources owned by a harness graph -- an identifier under thenexus-harness-graphNamingSystem -- plus each Task'sbasedOnclinical targets and itsforPatient, as one flat list of raw FHIR. Because harness actors are LOGICAL Device references (identifier-only, no Device resource on the server), the query is anowner:identifiersearch, and the connector VERIFIES the modifier was honoured rather than trusting it: a backend that ignored it would answer with every Task in the store, and presenting those as inboxes would be silently wrong, so an all-stray result refuses loudly instead.Only the WellServices FhirGateway connector implements it. The query is cross-patient, so it is not a compartment fetch; every other source raises
NotImplementedErrornaming the connector that can. The returned resources can be wrapped with TypedFhir facades (typed_fhir.facade.generated.wrap) for typed access -- but note that nothing yet takes this FHIR and sorts it into per-actor inboxes for the AI harness; that consumer has not been written.
-
load_daysheeton the Python source connectors: one practitioner's day, appointment cut only (FHIR-52, SD-201). Appointments for the day, the practitioner's Schedules, and those Schedules' Slots inside the day — three standard R4 searches, paged, results VERIFIED against the practitioner and the day window rather than trusted (a backend that ignores a search parameter answers with everyone's appointments; an all-stray result refuses rather than presenting someone else's day). WellServices FhirGateway only, like the inbox load — the query is cross-patient.This supersedes the TS day-sheet read that 1.17.0 said had "moved to 1.18.0." That prototype tried to make one loader answer both "who is coming" and "what is their story", and hunted an Appointment→Task edge this IG forbids; it will not ship. The corrected model splits the two questions:
load_daysheetanswers WHO is coming, and opening a patient is a separatefetch_resources(..., since=...)anchored bylast_encounter_date(below), per visit, when the chart is opened. Patient depth is deliberately absent from the day sheet.
-
sinceon Python patient loading: a history window over date-oriented elements (FHIR-52).fetch_resources/fetch_resources_lenientacceptsince=; a resource whose IG-declared effective date falls before it is dropped, and a resource whose profile declares NO effective date (Patient, Coverage, Device, ...) is always kept -- undated context is not history. The filter readseffective_date_of, the sameevent.period.start | dateprecedence every facade reads, so the window and the facade cannot disagree about when something happened. Precision is handled inclusively: a year-only date the anchor cannot place is kept, because a summary that silently dropped it would misinform where including it merely over-shares.Server-side date windowing was removed from this connector in an earlier release because no caller could supply the anchor. The anchor now exists (below); the filter stays CLIENT-SIDE, where one implementation serves every connector. Bandwidth is not saved; correctness is.
-
last_encounter_date: the anchor the "since you last saw" window opens from (FHIR-52, SD-189/SD-190). The clinical date of the patient's last SIGNED encounter note -- a Composition whosecategorycarries LOINC 34109-9, dated byevent.period.start | date-- strictly before a REQUIREDbeforeanchor.beforehas no default, deliberately: the window's upper bound is the CURRENT visit (the appointment start at 36h pre-generation, the encounter start on chart load), and "now" is the one anchor that is always wrong during a visit -- the moment this visit's note is signed it would become "the last encounter" and the window would collapse to zero, precisely in the walk-in and chart-load cases the requirements call out (SD-189 03.05/03.06). Ties are excluded: a note dated exactly at the anchor is this visit's note. ReturnsNoneon a first visit -- a real answer, not a failure. Feed the result tosince=.No TS or C# twins are owed for any of the three, and that is a statement, not an omission: both ports refused a sources layer by design (a host there already has a FHIR client and comes in through the in-memory ingest edge), so the fetch contract lives in Python, whose consumer -- the AI harness -- is the consumer of all three features. The primitive they lean on,
effectiveDateOf, already exists in all three ports.
- Templates are a first-class shape in the IG: the Template profile on
List. A template is aListwhose CONTAINED resources are prototypes of what it produces, cloned into ordinary resources when someone uses it. The first consumer is the clinical letter, which produces one Composition, so a one-entry template is the ordinary shape rather than a degenerate one; a template that later produces a letter and a follow-up task gains a prototype and an entry, and needs no new definitions. (FHIR-53)
- A naming system for template identity,
https://fhir.apps.health/NamingSystem/nexus-template. A template carries exactly one identifier under it -- both which template it is and the marker that it is one -- so templates are found with the standardListidentifier search parameter. No custom SearchParameter, and no code system minted to mark a template. The identifier is identity, not classification: what a template produces stays stated by its prototypes, so there is no second copy of that fact to drift. (FHIR-53)
- A Templates page in the handbook, a worked one-entry letter template example, and a letter template in the generated tenant corpus (clinic configuration, alongside the schedule and the EMR device).
Notes for implementers¶
- A prototype is valid base R4; the instantiated resource is valid Nexus. This is the
load-bearing rule and it is what keeps the clinical profiles unchanged. A prototype has no
patient, and base R4 already permits
Composition.subjectto be absent -- soNexusEmrCoreCompositionis not relaxed to0..1, and no consumer of real letters has to carry a rule about templates. Conformance is checked where it matters, on the resource that reaches the chart.
- Prototypes are
contained, and two of the rules come free. R4 defines a contained resource as one with no independent existence, no independent identity and no transaction scope of its own, which is what a prototype is.dom-3then requires every contained resource to be referenced from the container, so a prototype no entry points at is already invalid;dom-4forbidsmeta.versionIdandmeta.lastUpdatedon one, so a prototype cannot claim repository identity.
- Do not write sentinel values into prototypes. Where base R4 requires a value
(
Composition.status,.type,.date,.author,.title), supply the template's own real one -- the date it was written, whoever wrote it -- and overwrite it at use time. A placeholder like1900-01-01validates cleanly and saves cleanly, which is exactly why it is dangerous: a letter dated 1900 is a defect no validator will report.
List, not a collectionBundle. The first draft of this profile was a Bundle, which expresses the same graph.Bundlederives directly fromResourcerather thanDomainResource(withBinaryandParameters, one of only three that do), so it has noextension, notextand nocontained. A template needs a name, a lifecycle status and whatever stamp a deployment puts on stored resources, and a Bundle can carry none of them.Listcarries all three natively:List.title,List.status, and ordinary extensions.
- Substitution is not specified yet, and instantiation is currently the client's job. For a
one-resource template that is: read the List, take the contained resource its entry names, set
subject/date/author, drop the contained id, and save it as an ordinary Composition. A declaration of the substitutions, and a server-side operation that applies it, are additive -- templates written against this release are their input.
[1.17.0] - 2026-08-16¶
Generated by TypedFhir 0.11.0. Bumped from 0.10.0 (which generated 1.16.0): this release teaches
the FHIRPath subset ofType() so two IG rules start being checked at runtime, and folds extension
invariants into the Python registry, which had been reporting a rule set two short of the IG's
own. All three manifests move together --
ts/package.json, python/pyproject.toml and dotnet/TypedFhir/TypedFhir.csproj.
MINOR, and it contains breaking changes (Task.focus is typed, and a given seed no longer
reproduces the 1.16.0 dataset). Under SemVer the first of those is MAJOR. This specification does
not version by SemVer -- see the note at the top of this file -- because it is pre-release and
still being remodelled, and 2.0.0 would claim a stability 1.x never had. The obligation the version
number is not carrying is carried here instead: both breaks are named as breaks, in bold, with the
consumer action, under Changed below. Read them; do not infer safety from the MINOR bump.
Consumer action for the seed break. If you pin generated seed data, regenerate it and re-baseline rather than trying to reconcile the diff: the values moved for four independent reasons (per-domain RNG streams, hashed resource ids, the lab-tier reconciliation, and the triangular normal-band sampler), so a value-by-value comparison against 1.16.0 will not tell you anything useful. Resource COUNTS and shapes are stable; individual values, ids and dates are not. If you need the old dataset byte-for-byte, stay on 1.16.0 -- it is still published and archived.
Scope on the ticket: the CapabilityStatements and the subscription criteria surface, the Task remodel FHIR-31 asked for, and the review findings and generator reconciliation those turned up.
⚠ The live site can be ahead of what has been reviewed. This specification is published before
its PR is merged, deliberately, so readers are not blocked on review. The cost is that a version
served from the site may still change before it lands: 1.17.0 was live while review moved
Composition.type/.category from extensible to preferred and changed the declared effective
date. If you pulled a package during that window you hold different bytes under the same version
number. Splitting unreviewed releases onto a preview section of the site, leaving the main spec
on the last merged release, is tracked as FHIR-54. Until then: check the PR before building
against a freshly published version.
SD-201's day-sheet read is NOT in this release. It was built on this branch and moved to
sd-201-daysheet-1-18-0 for 1.18.0. It is a read over the profiles, not a change to them, so
holding it back costs the spec nothing, and it had open questions worth answering before it ships
rather than after.
Changed¶
-
BREAKING --
Task.focusis typed, andfocusvsinputnow mean different things (FHIR-31).focuswasReference(Resource), which documents nothing: a conformant writer could point it at an Organization and still pass. It is nowReference(QuestionnaireResponse | DocumentReference | Composition | Condition | DiagnosticReport | ServiceRequest | MedicationRequest | Observation), each constrained to this IG's profile of that type.Consumer action. If you write Tasks,
focusmust now name one of those eight and must satisfy the corresponding Nexus profile; anything else fails validation where it previously passed. If you read Tasks, you gain the thing that was missing: "which chart element is this task on" is answerable from the type alone, so a reader can switch on it rather than resolve the reference to find out what it is.The two elements were competing.
inputwas ALSO an open reference to the chart, so the same fact had two homes and neither was wrong. That is why FHIR-31's question -- where do forms and inbox documents live -- had no answer. The split is now stated on the elements themselves:focusis what the task is about,inputis what the task needs (chiefly the contained AI-suggested resource). Where a workflow genuinely spans several elements,focusnames the one the task is filed against and the rest ride ininput.Why forms are not stored inside the task. It reads as a storage question and is really a boundary question. A task carries the WORK -- who asked (
requester), who owns it now (owner), what has been said (note), what happened to it (relevantHistory) -- and not the clinical content. If the form lived inside the task, every consumer that wanted the form would have to understand task workflow to reach it, the form would disappear when the task completed, and a document filed against two tasks would exist twice. So the form is a chart resource in its own right and the task points at it.noteis documented as what it actually is: the conversation, oldest first.Annotationcarries author and time as well as text, so the repeats ARE the thread that makes a forwarded task intelligible to whoever receives it. It stays deliberately distinct fromrelevantHistory, which is Provenance and records what HAPPENED rather than what was SAID -- an audit trail must not depend on someone having chosen to comment.
-
BREAKING for anyone pinning seed data: a given seed no longer produces the 1.16.0 dataset (PSD-417/PSD-412). The synthetic-patient generator was reconciled after PSD-417 was implemented twice, independently, against two different bases. Every change below reshuffles the random stream, so this is stated once, loudly, rather than discovered: regenerate any pinned fixtures. It is deliberately taken as one break in one release rather than three.
Lab flags are now derived from the value, against clinical panic thresholds. Previously the flag was rolled and a value synthesized to fit, with "critical" derived from the reference SPAN -- so a sodium of 130 was labelled Critical low when the real panic bound is 120 or below, and a results viewer colouring criticals red cried wolf on it. Analytes now carry
critLow/critHighpanic values, the flag is computed from the same bounds printed inObservation.referenceRange(so the range and the flag cannot disagree), and the tier distribution is chosen rather than inherited from each analyte's sampling spread. Measured on the new engine: about 16% of lab results abnormal and 0.8% critical, against a previous mix in which six of the eight analytes with panic bounds could only ever read Normal or Critical -- never a mildly high potassium, which is the most common abnormal a GP actually looks at.Review state and the review Task are now one decision. The
nexus-emr-reviewedextension remains the authority for review STATE and the Task is the workflow that produces it, but the two were independent draws, which would have putcompletedTasks on unstamped reports. They now move together. Review Tasks on imaging reports also stop being coded182836005"Review of laboratory test result".Not every resulted report is final, and the unverified ones now carry review Tasks too. The corpus previously emitted
finalreports and nothing else, so its inbox contained only settled work -- which had it backwards, since an unverified result is the one a clinician most needs to see. Reports now arrivepreliminary(complete but unverified) andpartial(some analytes back, the rest still running, and the report carries only the analytes that are), and both are reviewable.Observation.statusagrees with its report by construction: an unverified report is not made of verified observations, and becausepartialhas no Observation equivalent, the analytes that are back readpreliminary. Unverified reports are deliberately less likely to be signed off, because reviewing a preliminary result often means deciding to wait for the confirmed one.Imaging is now an ordered flow, and the corpus contains no ImagingStudy. An imaging report gets a
ServiceRequestorder with abasedOnback-reference, can be pending or cancelled like a lab, and varies its impression and performing centre.DiagnosticReport.codefor imaging becomes SNOMED rather than LOINC -- a preference call, not a conformance one, but visible to every consumer. The ImagingStudy profile stays in the IG and is excluded from the corpus with a stated reason: it carries series, instance and SOP identifiers only a PACS can mint, and an invented study UID asserts an acquisition that never happened. No canonical is removed.
-
The generator is additive again: per-domain random streams (PSD-412). Every emitter used to draw from one shared stream, and
uid()consumed 30 values from it per resource, so ANY change moved every value downstream of it -- different medications, different appointment times, different ids on resources nobody touched. A one-line change produced a corpus-wide diff, which is why PSD-412's "same seed, same dataset, reviewable diff" kept failing on the third clause, and how PSD-417 came to be implemented twice without either author seeing the other's effect.Each domain now draws from its own sub-stream, addressed by NAME rather than by position, with the "does this patient get one at all" gate on a separate stream again. So adding a domain perturbs nothing, retuning a prevalence changes which patients have the thing without rewriting what it looks like for the ones that still do, and two people can add generators in separate commits without colliding. Resource ids are hashed from (chart, domain, counter) rather than drawn, which makes them unique by construction rather than by luck.
What is NOT promised: isolation is per domain, not per resource. Adding a lab still moves later labs.
test/seed-stability.test.tspins all of this, and prints a per-resource-type fingerprint table so a reviewer can check that an imaging change moved only imaging.
Added¶
-
ofType()joins the emittable FHIRPath subset, and two IG rules start being enforced on real data (FHIR-33).value.ofType(CodeableConcept)ISvalueCodeableConceptin FHIR JSON, so the type filter is a statically-known key rather than a runtime type test, and it compiles by collapsing two navigation steps into one -- exact, not an approximation.It was the only operator standing between the sociodemographic
*-absence-exclusiveinvariants and being checked; everything else in them was already supported. Those two rules -- "if a patient answered Asked-But-Declined, that must be their only answer" -- are now verified by TypedFhir at runtime in all three ports, rather than only by the HL7 validator against this IG's own examples. They are what stops a decline being counted as a demographic on a field whose stated purpose is reporting.The sibling
*-instances-cleanrules (duplicate detection) remain unverifiable: they needselect(),&andisDistinct(), which do not reduce to anything the compiler already has. No consumer action -- nothing about the published contract changes; two rules the IG already stated are now actually checked.
- The IG now publishes CapabilityStatements (FHIR-35).
nexus-emr-serverdeclares the 29 resource types a conformant endpoint exposes and the interactions each honours, derived from thesmart-exposedextension the profiles already carry — so "which types can an app reach, and to do what" is answerable from the artifacts instead of by reading 45 profiles. The 14 types this IG models but does not expose are deliberately absent from it: modelling a resource says the estate contains it, exposing it says an application may reach it. General search parameters are not declared yet — that surface varies by backing store, and this IG will not publish a list it has not measured against each one.nexus-emr-subscriptionsdeclares the closed set of types that may appear in aSubscription.criteriaand the exact search parameters each permits, so an unsupported criteria can be rejected when the subscription is created rather than matching nothing forever. It also states what the change feed cannot express — transition criteria ("changed into cancelled"), deletes, and unbounded replay — each of which an implementer would otherwise discover by not receiving a notification. Every parameter names a base R4SearchParameterthroughdefinition; none is invented here, and the element each one reads comes from that SearchParameter's ownexpressionrather than from anything this IG restates. That matters because a parameter name does not tell you which element it reads —patienton a DocumentReference readssubject,dateon an Appointment readsstart, anddateon an Observation readseffective[x]in every type the profile permits. The client libraries' criteria vocabulary is generated by resolving those definitions, so a criteria cannot read a different element here than the same query would read on a conformant FHIR server. - Patient self-identification fields for sociodemographic capture (PSD-765): the Patient profile slices REUSE CA Core+'s
patient-indigenousIdentity/patient-racializedGroupextensions (ca.infoway.io.core1.2.0-dft-ballot, newly pinned as a dependency) rather than minting Nexus canonicals — repeatable, one answer per instance. Slice bindings tighten CA Core+'s preferred Infoway ValueSets to pinned enumerated mirrors (extensible) of IndigenousIdentityCode/RacializedGroupCode 1.3.0 — absence concepts are first-class members per the source composition — plus warning-severity exclusivity/cleanliness invariants on the Patient profile.
-
Composition.eventis modelled: the note's CLINICAL date now has a home (FHIR-35). The profile previously offered onlydate, which R4 defines as the composition editing time -- it MOVES when a note is amended. So a timeline sorted bydatere-positions an amended note to the date of its amendment, and no element said when the documented care actually happened.event.periodis that element:event.period.startis required when the period is present,event.codesays what kind of care it was,event.detailmay reference it. Sort a clinical timeline byevent.period.start, not bydate. Both elements now carry a timing-relationships block and a worked example, matching the treatment DocumentReference already had.The seed corpus deliberately makes the two DISAGREE -- an amended note carries a
datea week after itsevent.period.start-- because a corpus in which they always agree teaches a consumer the opposite of what the profile says.Composition and DocumentReference disagree about where these facts live. That is inherited from base R4, not introduced here, and it is now written on both profiles rather than left to be discovered:
what Composition DocumentReference when the care happened event.periodcontext.periodwhat kind of care event.codecontext.eventthe visit encounter(top level)context.encounter(nested)The same two words nest the opposite way round, and the encounter link sits at a different depth in each. Consumer action: if you learned one of these profiles and assumed the other, check all three rows.
-
Composition.categorybecomes the preferred classifier, matching DocumentReference (FHIR-35). The two profiles gave OPPOSITE guidance on the same question: DocumentReference said "use category instead" oftype, while Composition saidtypewas the primary classifier and demoted category to optional. Composition now follows DocumentReference.Why category wins, and it is the reasoning DocumentReference already published: category supports multiple classifications per resource, admits clinical and administrative taxonomies side by side, and leaves
typefree to carry the source EMR's own note type without displacing the field a consumer keys off.Composition.typestays1..1because base R4 requires it, not because it is the right thing to classify on.That distinction is not academic. In converted OSCAR data every
casemgmt_notearrives withtypefixed to LOINC 11488-4 "Consult note" whatever the note actually is, so a consumer keying offtypethere learns nothing.The seed corpus previously emitted a
categorythat CONTRADICTED its owntype-- 11488-4 "Consult note" as the category of a note whose type said 11506-3 "Progress note", two document types competing rather than a general and a specific. Both now read LOINC 34109-9 "Note".
-
New value set
nexus-emr-note-classification, boundpreferredon BOTHComposition.typeandComposition.category(https://fhir.apps.health/ValueSet/nexus-emr-note-classification)One code: LOINC 34109-9 "Note". That is what the estate stores, and a larger enumeration would be a guess about note taxonomy published as a contract.
The same set is on both elements because there is nothing yet to distinguish.
categoryis the preferred classifier (see the entry above);typeis1..1because base R4 requires it, not because it carries a finer second fact. When note subtypes start being stored -- progress vs telephone vs discharge --typeis the element they belong on, and that is the moment to widen this binding back toward LOINC's document-type space. Thetypeelement's prose no longer lists four candidate LOINC codes, which a one-code set contradicted.preferred, notextensibleorrequired, and the reasoning changed late in review, so it is worth stating plainly.requiredwould make converted OSCAR data non-conformant on arrival: everycasemgmt_notecarries LOINC 11488-4 "Consult note" whatever the note is.extensiblelooked like the safe middle and is not, once the set is small -- an extensible binding over an enumerable set is a SHALL-unless-nothing-fits, and base R4 only avoided firing on that same data because its value set was a LOINC filter wide enough to contain 11488-4 already.preferredsays the thing actually meant: this is the code we store, and yours is still conformant.⚠ A
preferredbinding is not silent in TypedFhir today. All three ports grade any binding that is notexampleat WARNING severity, so a note carrying a document-type code outside this set will warn despite being conformant -- which converted OSCAR data does, on every note. The warning is a tooling artefact, not a conformance statement. Aligning the ports with FHIR's severity ladder (preferredas information) is a three-port behaviour change and is tracked separately rather than folded into a spec release. Carry an unmapped local type intype.textrather than mis-coding it.Two notes for implementers coming from elsewhere. First, base R4 binds
Composition.typeonly preferred; this IG has bound itextensiblesince before this release, so the tightening is not new here, only the value set is. Second, US Core makes the same split -- coarse classifier oncategory(a value set of exactly one code,clinical-note), fine slot ontype-- so an implementer who knows US Core will recognise the shape. The differences are that this IG uses the LOINC code that already means "Note" rather than minting a local one, and binds extensible rather than required.DocumentReference.categoryis deliberately NOT bound to this set. That resource holds arriving binary documents (scans, faxes, PDFs), a genuinely wider space than authored notes, and a note-classification set would be wrong there.
-
Published page URLs are now locked by a checked-in manifest (FHIR-35). Since the one-page-per-tab split in 1.16.0, a reference page's filename IS its public URL, and it is derived from the tab's DISPLAY LABEL. Editing a label therefore renamed published pages. Measured rather than hypothesised: renaming one tab label ("Salient fields" -> "Salient elements") moves 67 published URLs.
Nothing caught this, and the reason is the interesting part. mkdocs
strict: truechecks that links RESOLVE, and after such a rename they all still do, because every internal link is regenerated against the new name in the same pass. The site stays perfectly self-consistent while every inbound link from outside it dies. Internally coherent, externally wrong, and green.docs/published-urls.txtnow records all 901 page URLs and is compared on every pre-PR run. Additions pass silently -- a new profile or tab is ordinary work, and gating it would make the manifest a chore that gets rubber-stamped, which is how a lock stops being one. Only removals and renames fail, and accepting one is a separate named action (npm run urls:accept) so that moving a public URL is something someone can be seen doing in the diff.What this means for you: the canonical-URLs-are-forever promise this IG makes about its published pages now has something enforcing it. Bookmarks and inbound links to a reference page will not be moved by an editorial change to a tab label.
Fixed¶
-
Task.basedOnandTask.reasonReferenceare no longer must-support. Both elements remain, both remain typed (base R4 leaves each open to any resource; this profile narrowsbasedOnto the four request types andreasonReferenceto Condition / Observation / DiagnosticReport / DocumentReference). What changes is the CLAIM.Must-support says a consumer of this IG will do something meaningful with a populated element. This IG is not yet in a position to say that for either: nothing populates them, and a surface that displays a task reads
focus,owner,requester,status,priorityand the due date.Advertising support that does not exist has a specific cost, and this release is the demonstration of it. Because both were must-support, the seed generator had to populate them, so it reached for the nearest resource in scope:
basedOngot the Encounter andreasonReferencegot the Patient. "This task is because of the patient" says nothing -- every task is about the patient. Both are fixed above, but the fix people should take away is the demotion, not the patch: an element nobody reads is one producers fill with whatever is to hand.If you populate either today, nothing breaks -- they are still modelled, and the published StructureDefinition still narrows their targets, so a validator that checks reference target profiles will hold you to them.
⚠ TypedFhir is not such a validator. Its IG generator reads
type[].profileand nevertype[].targetProfile, so what reaches the ports for a narrowed reference is the bare datatype (types: ["Reference"]) and nothing enforces the target list in TypeScript, Python or C#. That is true ofTask.focusabove as well, and it is the reason FHIR-45 exists. Do not read "typed" as "checked by our client libraries" -- today it means "declared in the specification".What would make them must-support again: a consuming surface that reads them.
Note the overlap with
focus, which is the element to reach for first.focusis the chart element the task is filed AGAINST;reasonReferenceis why it exists. A task to review an abnormal potassium has that DiagnosticReport as itsfocus, and the Condition it relates to (if any) as itsreasonReference. Must-support coverage in the seed corpus moves 575 -> 573 accordingly.The seed corpus no longer emits either element. Populating them with the nearest resource to hand is what produced the wrong types in the first place, and synthesizing a plausible order or a plausible clinical justification for a task that has neither would put the same fiction back, one type-check better dressed. If you were reading
Task.basedOnorTask.reasonReferenceout of the seed data, they are gone -- they were never carrying real information.Wider question logged as FHIR-47: 20 of this profile's must-support elements are justified in published prose by the same source, and only these two have been examined.
-
Nearly half of all "normal" lab results sat exactly ON a reference-range boundary. The N tier drew from the wide near-normal sampler and CLAMPED into the reference interval. Because the sampler's
spreadis deliberately greater than 1 (the general lab path needs out-of-range values), clamping deposited every out-of-range draw exactly on a bound. Measured over 200,000 normal results: 22.4% landed on exactly 3.9 and 22.4% on exactly 5.5 for Glucose. Creatinine put ~17% on each of 60 and 110, about 9x what a flat distribution would give.The distribution was bimodal at the edges, which is the opposite of the mid-clustering the code claimed, and it is visible to anyone who looks at the seed data: a demo chart full of glucoses reading exactly 5.5 does not look like lab data.
The N tier now draws a triangular in-band value (mass concentrated mid-range, tapering toward the bounds, no spike on them); the abnormal tiers stay uniform, since there is no clinical reason to prefer the middle of "high" to its edges. Guarded in
interpretation-tiers.test.tsby two assertions -- no bound spike, and a modal value in the middle third -- both mutation-tested against the old clamping.This moves lab values for a given seed. It is part of the seed break already declared above.
Composition.event.period.startis now the DECLARED effective date, not just the documented one. The profile told readers in bold to sort a clinical timeline byevent.period.start, and the machine-readable declaration three hundred lines above it still saiddate. Everything that actually reads the effective date reads that declaration -- the facade accessor, the PatientCache recency windows, andeffective_date_ofover raw JSON -- so a note written on the 10th and amended on the 18th still sorted to the 18th in all three ports, which is exactly the failure the prose was added to prevent. Nowevent.period.start | date, a precedence list, so the clinical date wins and the authoring date remains the fallback for notes that carry noevent.
-
The reference-target guard was checking less than it claimed. Three holes, all found in review, all closed:
- It keyed constraints per RESOURCE TYPE while reading each profile's snapshot, so two profiles
of one type collided and whichever sorted last won.
nexus-emr-core-app-consentnarrowsConsent.provision.actor.referenceto a Device;item-consentdoes not narrow it at all, and its inherited eight-type list silently replaced the narrow one. Now keyed per PROFILE and selected by each resource's ownmeta.profile, the same rule the coverage tool uses, so both constraint sets coexist and each instance is judged against the profile it claims. - It filtered to
kind === "resource", dropping every extension, so all 191 extension-borne references in the corpus were unchecked. Extensions are now derived too, keyed by (canonical, slice) since that is how the constraint is actually addressed. - It compared JSON paths against FHIR element paths, so a constraint on a choice element such as
Consent.source[x]could never match an instance'ssourceReference. The choice form is now tried as a FALLBACK after the literal path -- never as a substitution, becauseTask.reasonReferenceis a real element name rather than a choice, and rewriting it unhooked a constraint that was being enforced.
Constraints derived: 290 -> 302. All four originally-fixed violations are now individually mutation-tested, so a future refactor that unhooks one fails here rather than in a release.
- It keyed constraints per RESOURCE TYPE while reading each profile's snapshot, so two profiles
of one type collided and whichever sorted last won.
-
Four seed-data references pointed at the wrong resource TYPE, and the validator did not notice.
Task.focus-- the element this very release types as a breaking change -- was itself pointing at an Encounter in the generated corpus. Found by auditing all 61,399 generated resources against the 290 typed-reference constraints the IG declares:Element Pointed at Allowed Task.focusEncounter ServiceRequest, DiagnosticReport, Composition, Condition, DocumentReference, MedicationRequest, Observation, QuestionnaireResponse Task.basedOnEncounter CarePlan, MedicationRequest, ServiceRequest, SupplyRequest Task.reasonReferencePatient Condition, DiagnosticReport, DocumentReference, Observation Location.partOfOrganization Location The last is wrong in base R4, not only under this IG:
Location.partOfis the location hierarchy, and the owning organization ismanagingOrganization. The corpus now carries a site Location that the clinic building is part of, which is the smallest honest hierarchy.Why nothing caught it, and what you should take from that. TypedFhir's validator does not enforce reference target types. It checks cardinality, bindings, invariants and datatype shape, but a
Referenceis only shape-checked, so{"reference": "Encounter/e1"}in afocustyped to exclude Encounter validates clean in all three ports. If you are migrating for theTask.focustyping above, do not expect the validator to find your violations for you yet. Tracked as FHIR-45, which carries the design and the acceptance criteria.In the meantime the generator has a standing guard (
fixtures/patient-generators/test/reference-targets.test.ts) that derives every typed-reference constraint from the IG's own StructureDefinitions and walks the whole corpus against them, so a new profile rule is covered the moment it is written. That guard covers the seed corpus only; it does nothing for your data.
-
"ICD-9 subset used by OHIP" was wrong, and it shipped. Read this one if you produce OHIP diagnostic codes (FHIR-32). The OHIP diagnostic code list was described in a published comment as an ICD-9 subset. It is not. OHIP diagnostic codes are Ontario's OWN three-digit set: ICD-9-like in shape and sharing many codes, but with a parallel Ontario space laid over ICD-9 numbers that are already occupied.
OHIP 970 = "Other Acceptable Diagnosis: Prenatal Care" ICD-9 970 = poisoning by CNS stimulants OHIP 967 = "Immunization: Influenza" ICD-9 967 = poisoning by sedativeEighteen codes carry two unrelated meanings inside OHIP itself (072 is both "Mumps" and "Gait Training"), so a code alone does not determine a concept.
Why this was dangerous rather than pedantic. A reader could reasonably conclude that SNOMED -> ICD-9 -> truncate is a safe route to an OHIP code. Measured against the Provincial Health Care Vocabulary Standard: 69.4% reach, and 124 rows truncate into the 895-971 range and come out VALID AND WRONG. "Poisoning caused by opioid receptor agonist" becomes 970 Prenatal Care; "Pica" becomes 307 Sleep-Wake disorders. Nothing downstream rejects either, and they go on a claim.
Consumer action: map explicitly from the concept list. Never compute an OHIP diagnostic code from ICD-9. The published comment now says what the set is, shows the collisions, and carries the measurement.
-
The
task-intentvalue set's description of R4 was wrong, and it ships (FHIR-35). The comment claimed R4 has notask-intentcode system and thatdirectivewas among the codes this IG declines. Neither is true: R4'stask-intentvalue set composes from TWO systems,http://hl7.org/fhir/task-intentincluded whole (it definesunknown) plus eightrequest-intentconcepts, anddirectiveis not among them at all. FSH comments compile into the StructureDefinitions and render on the published profile pages, so this was a wrong claim about the base specification on its way to implementers.The corrected text lists all nine base codes and gives a reason for each exclusion, so the narrowing to
order | proposalreads as a decision rather than an omission. One is worth stating here:unknownis excluded deliberately. A task whose intent is unknown cannot be triaged, because "is this mine to do, or mine to decide?" is exactly what a consumer reads this element to answer. A writer that does not know yet should not be minting the task. No consumer action -- the binding itself is unchanged from 1.16.0.
-
Published prose that contradicted this release's own artifacts (FHIR-35). Three partner-facing places still said the IG publishes no CapabilityStatement, after it began publishing two: the Subscription profile's
criteriacomment (which ships insidepackage.tgz), the "About this IG" page, and the Composition narrative. Corrected, and narrowed rather than simply deleted where only half the claim had gone stale -- the Composition warning now says what remains true, which is that the server statement declares Composition's interaction set but still declares no versioning policy, so whether prior versions are retrievable at all is server-specific. The "what this IG does not publish" list is now derived from the compiled artifacts instead of hand-written, so it cannot make that class of claim again.The Subscription
criteriacomment is the one to read if you consume subscriptions: the permitted resource types and search parameters are now DECLARED (innexus-emr-subscriptions, and the client libraries' criteria vocabulary is generated from it) but are not ENFORCED by the profile, which still constrainscriteriaonly to1..1. Validate against the CapabilityStatement when accepting a subscription from an untrusted writer.
- Five guards did not cover what they appeared to cover (FHIR-35), all found while closing out
1.16.0. Grouped because they share one signature: green, self-consistent, and blind.
* The FHIRPath subset drift gate iterated the default IG only, so a second registered IG could ship compiled invariants that nothing had ever checked could compile. It now iterates every registered IG, driven off the registry itself so the test cannot drift from it. *
isSupportednow fails closed on boolean literals.active = falsecompiled to a navigation into a member named "false" -- always empty, so the predicate could never fire while reporting itself as supported. No IG invariant uses that form today; it is closed while still theoretical, which is the only comfortable time to close it. *extensionAppliesAtRootreportsunanchorable-contextinstead of silently dropping a wildcard context it does not understand. *check-fragmentsgains a zero-guard (an empty run reported success), matchingcheck-extension-invariants. * The IG version lives in six places and only two were checked against each other. Cutting 1.16.0 found the rest one red gate at a time and still missed the Python registry, which shipped a package that could not be imported. One step now checks all six and names the fix.
- The server CapabilityStatement's "derived" claim is now enforced (FHIR-35). It states that
its resource list and interactions come from the
smart-exposedextension the profiles carry. They did, but only because someone had checked by hand: 29 entries of hand-written FSH with nothing connecting them back. Since a scope naming a type the statement does not list is not granted, a stale entry there is an access-control defect rather than a documentation one.check-capability-derivationrecomputes the list from the compiled profiles and fails on divergence in either direction -- an interaction the grant does not justify, a type listed that nothing exposes, a type exposed that is not listed, or two profiles of one type disagreeing.
- Four analytes were reported to the wrong precision, and one could fall outside its own
reference range (PSD-417). Decimal places were guessed from the unit string, which printed TSH
as
1, INR as2and WBC as7-- none a value a laboratory reports. Worse, TSH's reference range is 0.4 to 4.0 while its values rounded to integers, so a result the generator had decided was Normal could be emitted as0and read Low. Precision is now stated per analyte where the unit does not imply it, and normal values are snapped to the representable grid INSIDE the reference range rather than clamped before rounding.
- Duplicate resource ids in a generated tenant (PSD-412).
tenant.tshands one patient RNG to twogenerateChartcalls (a mother and her newborn), so once ids were drawn from position-independent per-domain streams, both charts minted the same ones. Ids are hashed from a per-chart scope now, and a tenant-wide uniqueness assertion guards it. Caught before release by the new stability test; no published corpus carried it.
-
The Python port under-reported the IG's own rule set (FHIR-35). 1.16.0 taught TypedFhir to compile invariants declared on EXTENSIONS, and the TypeScript registry folded them into the loaded invariant list. The Python registry did not, so
typed_fhir.ig.INVARIANTS-- exported public API -- was silently short by exactly the rules extensions declare (66 where TypeScript reported 68). Nothing on the Python side could go red for it: there was no twin of the TypeScript assertion, and the Python test fixture had no extensions at all. Both are added, so the two ports stay pinned to the same set.Consumer action: if you read
typed_fhir.ig.INVARIANTSto enumerate what the IG requires, it now returns the complete set.LoadedIGalso gains anextensionsfield, matching the TypeScript shape.
- A drift gate could no longer fail for ten common invariant keys (FHIR-35). The allowlist of
conformance rules TypedFhir cannot verify is keyed
ResourceType:key, but the check matched on the bare key, which blanket-exemptedstatus,type,intent,category,severity,mode,criticality,verificationStatus,confidentialityandlifecycleStatuson every profile in every registered IG. Nothing was being wrongly skipped when this was found -- the defect was that the gate could not go red for those keys, which reads as coverage. Now matched on the full key, with a mutation test that fails if the bare-key behaviour returns. No consumer action.
- Two gates were demanding incompatible things (FHIR-35). One asserted that EVERY invariant compiles to the emittable subset; the other froze a list of the ones that do not. Allowlisting in the second left the first red, and the only state satisfying both was "support every expression the IG could ever contain" -- a policy nobody chose, just what two half-overlapping gates added up to. The allowlist now lives once and both gates read it.
- Four invariants are newly declared unverifiable at runtime, and the cost is stated rather than
buried (FHIR-33/FHIR-35). The sociodemographic
*-instances-cleanrules needselect(),isDistinct()and&, which are not in the emittable subset. What this means in practice: they are enforced by the HL7 validator against the examples this IG publishes, so they hold for what we publish and NOT for what a writer sends us. Both are warning severity, so the effect is that a malformed sociodemographic answer can reach a store unremarked. Stated here because a consumer relying on TypedFhir's runtime validation should know which rules it is and is not checking on their behalf.
[1.16.0] - 2026-08-14¶
Generated by TypedFhir 0.10.0. Bumped from 0.9.0 (which generated 1.15.1): this release teaches
the library to compile and run invariants declared on EXTENSIONS, in all three ports, and adds two
FHIRPath forms (xor and the extension('url') shorthand) to the emittable subset so they can be
written the way an author would write them. It also closes the last blind spot in the IG chain,
where the vendored snapshot could go stale without any guard noticing. All three manifests move
together -- ts/package.json, python/pyproject.toml and dotnet/TypedFhir/TypedFhir.csproj.
MINOR, and it contains a breaking change (ClinicalTask.intent is no longer fixed to order).
That is the draft carve-out: while sushi-config.yaml says status: draft, a breaking change
bumps MINOR and the changelog carries the obligation to name the break and the consumer action.
Both are stated under Changed, below.
Scope on the ticket: the published prose surfaces and the one-page-per-tab reference split, attribution modelling for the AI harness (PSD-848), and the verification work those two turned up.
Added¶
-
The reasoning behind a constraint is now published (FHIR-32).
definition,commentandrequirementsare where the WHY of a constraint lives, most of it hand-written in FSH carets, and none of it reached this site: it compiled intopackage.tgzand rendered only on IG Publisher pages. Elements now have a Details view, every element name in the Differential / Key / Snapshot tables links into it, and an element carrying acommentgets a marker in the description column, becausecommentis often the most useful prose on an element and it renders in no table. Patient alone gains 169 prose blocks and 199 markers across 102 elements.Salient fields carry
short,definitionandcommentseparately rather than one lumped description, so each consumer takes the text written for its audience: the table cell, the generated docstring, and the pydantic schema an LLM reads. 94 of 95 salient fields inheritshortanddefinitionwith nobody writing a word.
-
One page per tab in the generated reference (FHIR-32). The tabs were one page carrying every view; each leaf view is now its own page and the tab bar is a strip of links. 273 generated pages become 890. Patient goes from a single 521 KB page to 14 pages, largest 184 KB, median 11 KB.
Existing URLs are preserved -- the first leaf keeps
StructureDefinition-<id>.md, so every link and bookmark to a profile page still resolves. The reason for the split is that hidden tabs were still in the DOM: a search hit or an inbound anchor could land on content the reader could not see. Every view is now separately addressable, and search results land visible.
Fixed¶
-
The vendored IG snapshot can no longer go stale unnoticed (PSD-848). The chain is
doc/fhir-> SUSHI -> vendored snapshot -> the generated tables, facades and patient-compartment map, and every guard sat at or below the snapshot: they regenerate from it, or compare against it. None could see the FIRST link break. Editing the FSH without re-runningrefresh:igleft the snapshot unmoved, so every guard agreed with every other guard about an IG that no longer matched its own source, and CI stayed green.refresh:ignow stamps anigSourceHashinto the manifest -- a content hash ofsrc/fsh,src/examplesandsushi-config.yaml, the inputs that determine the vendored artifacts -- and a new test recomputes it. A mismatch means the source has moved since the snapshot was taken, and says so with the command that fixes it. It runs in thenpm testchain, so the required CI check enforces it rather than only the owner-track gate.No consumer action. A build-integrity check; nothing about the published contract changes.
-
Invariants declared on EXTENSIONS are now compiled and enforced (PSD-848). They previously were not, by any mechanism: extension StructureDefinitions never entered TypedFhir's IG model (only
kind: "resource"was kept), so their constraints reached no table, no compiler and no gate. Such a rule rendered on its published profile page, read as enforced, and checked nothing -- and the gate that exists to flag an unverifiable invariant could not see it either, because it iterates the invariants the model collects.effective-date-declares-exactly-onewas in that state for its whole life; it is now verified against all 45 profiles that carry the extension.Two FHIRPath forms were added to the emittable subset to make it compile as written:
xor, andextension('url')as sugar forextension.where(url = '...'). The sugar matters beyond this rule -- it is what an author naturally writes (HL7's own invariants use it), and until now reaching for it silently produced an unverifiable invariant.Extension invariants are now also compiled into the RUNTIME validators. An extension's rules are not in the profile's snapshot -- a sliced extension element carries only
ele-1/ext-1-- so TypedFhir checked none of them on real data, even where the HL7 validator was checking them against our examples.gen-facadenow resolves them BY URL, matching how an extension is identified in the data, so a rule applies wherever its extension appears rather than only where a profile happens to slice it. All three ports.Mostly no consumer action -- a rule the IG already stated is now actually checked. The one addition is below.
nexus-attributed-factrequires substantive citations (PSD-848): new invariantattributed-fact-citations-are-substantive-- the statement, and every citation's verbatim quote, must be non-empty.1..1requires those elements to be PRESENT; it does not stop them being"", and an empty quote is worse than a missing one: it is a citation that cites no text, passing every structural check while leaving the fact unauditable, which is the one property the extension exists to provide. Error severity. The extension isexperimentaland unreleased, so no published contract tightens.
Changed¶
-
BREAKING --
ClinicalTask.intentis no longer fixed toorder(PSD-848). It now bindsnexus-emr-task-intent(required):orderfor work already authorized and assigned,proposalfor a suggestion that takes effect only once a human accepts it.Consumer action: code asserting
intent === 'order', or relying on it being present with that value, must handleproposal. If you were identifying AI suggestions bycode, theAIASTsecurity label, or the presence of a contained resource, readintentinstead -- those were proxies for it. Task instances already in a store are unaffected:orderremains valid and no stored value changes meaning.Why: the fixed value described the population the profile was written for -- inbox items, follow-ups, to-dos, which genuinely are orders -- and prevented a scatter of intents for identical work. Modelling AI suggestions onto the same profile made it wrong: a suggestion awaiting a clinician's approval is exactly what "not proposals or plans" excluded, so
intentcould not distinguish "assigned to you to do" from "awaiting your approval". The binding is narrowed to two codes rather than reopened to R4's nine, because the order-flavour refinements describe a fulfilment chain this model does not have, and a task whose intent isunknowncannot be triaged at all. The FSH records the accept-or-reject for each of the nine.
- Provenance leaves placeholder status (PSD-848): the profile now has a committed job -- attribution
for AI-mediated and cross-boundary writes, in two worked shapes (standalone, and contained in an
AI-suggestion review Task) -- and is marked
experimental = false. No cardinality, type, binding or slicing changed, so nothing that validated before stops validating. One invariant did get real teeth:provenance-ai-action-names-agentwas effectively vacuous (agent.exists()) and is now a genuine conditional check. Its severity is stillwarning, so it fails nothing -- but a producer relying on it never firing will start seeing it fire. Model/token accounting, themetaUpdatedByrelationship, and whether Provenance is written for ordinary human edits remain open and are documented as such. - Attributed facts on Provenance (PSD-848): new
nexus-attributed-factextension carrying, for AI-generated text, each factual statement with its backing verbatim quote(s) and the generating run's chunk label -- a lossless mapping of the harness's fact/citation wire schema, with an optionalsourcereference tying a citation to theProvenance.entityit was drawn from. Sliced into the Provenance profile asattributedFact 0..*, with a worked pre-visit-summary example. The AI-suggestion Task example now also carries a CONTAINED Provenance (linked fromrelevantHistory) citing the passage its contained suggestion was extracted from -- the suggestion and its citation trail travel and are approved or rejected as one unit. - A suggestion's review state belongs on the Task (PSD-848): the ClinicalTask handbook page
now states the rule and the contained-suggestion example follows it. A contained suggested
resource is written as the clinical fact its citations support, so the example's
Conditionmoves fromverificationStatus = unconfirmedtoconfirmedand gains the requiredcategory. Usingunconfirmedto mean "not yet approved" reads as clinical doubt to every consumer --verificationStatusis a modifier element -- and the wrong value survives into the chart on approval, because approving movesTask.statusand changes nothing inside the contained resource. The same rule keeps a suggestion promotable: it should already satisfy its profile.
[1.15.1] - 2026-08-12¶
Generated by TypedFhir 0.9.0. Bumped from 0.8.0 (which generated 1.14.1): this release adds
sibling-profile-aware validators, pydantic schema generation, seven profiles' worth of typed
accessors and facades in all three ports, and the since-checkpoint delta fetch. All three
manifests move together -- ts/package.json, python/pyproject.toml and
dotnet/TypedFhir/TypedFhir.csproj.
Scope on the ticket: AI-harness modelling (the suggestion/Task model), TypedFhir work the harness depends on, and terminology-server-related binding work.
Added¶
-
The tenant stamp is declared: the
tenant-idextension (FHIR-29). Every resource stored through the FHIR Gateway carrieshttp://services.well.com/fhir/auth/tenant-idonDomainResource, and the IG did not declare it -- so it appeared on every payload with nothing to resolve and no explanation. It is now a published extension, documented as what it is: the gateway stamps the caller's tenant on every create and update and strips any client-supplied value first, so tenancy follows the token and a client cannot choose it.Both this and the
userIdidentifier are private infrastructure and must not be depended on -- unresolvable URLs, opaque values, no breaking-change guarantee, and deliberately kept off the governedfhir.apps.healthhost to say so. If you need to know which clinic a resource belongs to, use the modelledOrganization/Locationreferences instead. See the Identifiers page in the handbook.
- Seven new first-pass profiles close the parallel-efforts modelling gaps (FHIR-29, from the 2026-08-10 gap triage; every one has a named consumer): ImagingStudy (the imaging half of results viewing, PSD-417; DiagnosticReport.imagingStudy now targets it), Coverage (payor context for billing/demo -- the Ontario shape, payor as a logical Organization reference), FamilyMemberHistory and MedicationStatement (the harness's Patient Summary Domains read list), Goal and CarePlan (CDM flowsheets and care gaps; CarePlan also resolves the corpus's no-model-but-generated inverse gap by profiling rather than un-emitting), and RiskAssessment (computed risk scores; replaces the chart-module registry's profileAbsentReason placeholder). ALL seven are CLOSED to SMART apps pending an exposure decision (the FHIR-28 policy for newly profiled types).
- Result review state: the
nexus-reviewedextension (FHIR-29) on DiagnosticReport and Observation -- who acknowledged a result and when; ABSENT means unreviewed. The Task inbox is the review workflow; this is the queryable state it stamps, so a results viewer needs no Task join. The seed corpus emits a realistic reviewed mix with unreviewed volume on recent results.
- ICD-10-CA and CCI get named slots, not content (FHIR-29): system-sliced secondary codings on Condition.code (icd10ca) and Procedure.code (cci). Codes are deliberately NOT enumerated -- both classifications are CIHI-licensed and the licence paperwork is in flight (the prevention-catalog pattern: the IG declares the slot and the system; content governance lives outside).
-
The seed corpus covers the grown surface: emitters for all seven new types (payor context incl. a cancelled prior plan, family history incl. a health-unknown relative, patient-reported OTC meds incl. a discontinued one, the CDM bundle tying Goal + CarePlan
- RiskAssessment to a live chronic Condition, imaging studies with their reports) plus the review-state stamp. Coverage guarantee after the growth: 43/43 profiles, 577/577 must-support, both directions clean with the CarePlan allowlist entry REMOVED.
- The seed corpus got the content the parallel demo efforts were missing (FHIR-29).
Abnormal/critical lab flags (H/L/HH/LL interpretations on a realistic minority of
results, deterministic on the anchors -- PSD-417's ask), ranked encounter diagnoses
spread across the record instead of one flat rank-1 (PSD-606), authored Composition
notes on ~85% of encounters INCLUDING the GMF follow-up majority, amended-note
snapshots (the version CHAIN stays a load-path concern -- a flat corpus cannot carry
_history), a
meta.tagsynthetic marker on every resource (corpus-local URN system; one_tagquery finds or purges fixture data), per-patient collection-Bundle output behind--bundles, and OHIP fee-code variety (six schedule entries, occasional add-on line item) instead of a single hardcoded A007.
- The coverage guarantee now fails in BOTH directions (FHIR-29). A new inverse check: any resource type the generator EMITS that the IG does not profile fails the guarantee unless it has a documented ALLOWED_UNMODELED entry (same visible-reasoned style as the existing exclusions). The motivating example -- 293 un-modelled CarePlans quietly shipping into benchmark stores -- is now the one allowlisted entry, explicitly flagged as pending the CarePlan modelling ruling rather than resolved.
-
bin/pre-pr.sh: the mandatory owner-track pre-PR gate (FHIR-29). One entry point chaining SUSHI build -> example conformance -> version-drift check (sushi-config vs ig-specs.ts, automating the hand-bump that trailed a release) -> IG vendoring + code generation for every generated consumer (TS, Python, C#, and the docs tree) ->npm run verify(the WHOLE TS suite, i.e. what CI runs, not a slice of it) -> the data-generator typecheck -> the bidirectional data-generator coverage guarantee -> a git-clean lockstep check on every generated tree. Documented as mandatory in the IG owner release process: regenerating code from the IG and proving data-generation coverage are now ONE event, so the corpus can never again trail the spec the way the note model did.The gate now covers EVERY generated consumer of this IG -- the TypeScript, Python and C# libraries and this documentation site -- and refuses to pass unless all of them regenerate clean from the same FSH. What that buys a consumer: the artifacts published under one version cannot silently be a release apart.
- The seed-data generator covers the whole 1.15.0 IG surface again (FHIR-29). The coverage guarantee is green across every dimension (profiles, must-support, bindings, invariants, extensions): the anchors gained the Composition encounter note (the 1.14.0 note switch), both consent shapes (app-consent grant + deny, item-consent provided + declined-with-evidence), the AI-suggestion Task (contained payload, thread-id, logical Device requester), the prevention-status Observation, the boolean app-state, and the heritage-inactive Patient (record-inactive-reason) -- plus a latent app-state extension URL bug fixed. TypedFhir's generated validators are now SIBLING-PROFILE AWARE: a resource declaring a sibling profile of its type via meta.profile (Consent app/item, Device app/emr) is validated against that profile's own rules instead of whichever profile sorted first -- previously every item-consent instance failed app-consent's pinned category, in every port that validates. The dispatch is emitted by all three facade generators, so TypeScript, Python and C# behave identically; a conformant item-consent and a conformant emrdevice are pinned in the cross-language parity corpus (vectors/facade-adversarial.json), so a port that loses the dispatch fails the parity gate rather than silently rejecting valid resources. nexus-sugg-1 was reformulated inside the compilable FHIRPath subset (plain choice access instead of ofType; same semantics), so it is machine-enforced rather than prose. The vendored IG, generated tables, facades (TS + Python + C#) and registries now stand at 1.15.1.
- AI suggestions are modelled as resources CONTAINED in their review Task (FHIR-29).
A suggested chart entry is never a first-class chart resource pre-approval: it rides
Task.contained, referenced from the new pattern-slicedsuggestioninput (input.type = nexus-suggestion#suggested-resource), withTask.code = ai-suggestion(enforced by the new nexus-sugg-1/2 invariants: the suggestion reference SHALL be local, and a suggestion Task SHALL declare itself). Type-level queries cannot see suggestions by construction -- confirmed empirically on both the WellServices store (MS FHIR Server for Azure) and Medplum. Lifecycle rides Task.status natively (requested -> accepted/completed, or rejected -- rejected Tasks and their payloads are RETAINED as the record of what was declined). New: thenexus-thread-idextension (harness thread traceback), thenexus-suggestionCodeSystem, and a full contained-suggestion example (AIAST-labelled Task carrying a suggested Condition).
- AI agent identity: logical Device references, no registration (FHIR-29). An AI
graph acting in the EMR is a Device referenced BY IDENTIFIER (the new
nexus-harness-graphNamingSystem -- the tree's first declared identifier-kind NamingSystem;Reference.type = "Device"); the identifier points into the harness's graph registry and need not resolve to a Device resource on the server. Granularity is deliberately graph/feature level.Task.requesterandTask.ownernow admit Device: an AI-created Task names its graph as requester, and REASSIGNING a Task to an agent Device is the approve/resume contract (the harness watches task-assignment changes and resumes the thread named by the thread-id extension).
- TypedFhir (Python): since-checkpoint delta fetch +
PatientCache.refresh(PSD-731).fetch_resources_deltaanswers "what changed since this opaque checkpoint" (_lastUpdated-windowed on the WellServices gateway, with an overlap so out-of-order commits are never lost; full-fetch fallback on every other source), andPatientCache.refreshmerges the delta in place by(resourceType, id). Checkpoints never advance past a window a covered type failed to sweep; a query built before a refresh raises rather than answering from stale bits. Hard deletes are invisible to FHIR search, so a refresh converges between full loads rather than replacing them. A cross-patient resource inside a delta is treated as an ERROR (backend corruption, dropped + error issue + held checkpoint, so it re-reports every refresh until fixed) -- never as a quiet removal.
- The C# port catches up: facades for all seven new profiles (FHIR-29).
CarePlan,Coverage,FamilyMemberHistory,Goal,ImagingStudy,MedicationStatementandRiskAssessmenthad TS and Python facades but no C# ones, so aWell.Services.TypedFhirconsumer could not touch any of the seven types this release added. Fourteen generated files were simply missing. The cause was the pre-PR gate not runninggen:facade-cs(above); the port had been a release behind with nothing reporting it.dotnet buildis clean on the backfill. bin/site.sh zippackages the human-readable IG for sending (FHIR-29).npm run site:zipbuilds the MkDocs site and zips it to~/Downloads(orZIP_DEST) as<date>-<version>-nexus-fhir-ig-mkdocs.zip, entries zipped from inside the site root so extracting dropsindex.htmlinto the cwd withassets/resolving, and the same self-containment proofpublish.sh --zipruns: every local assetindex.htmlreferences must be an entry in the zip.use_directory_urlsis off, so the extracted site works straight off disk with no server. This is the artifact to send someone who just wants to read the IG -- the-mkdocssuffix keeps it distinct from the publisher's-full/-slimrenders. Those two also drop-emr-from their filenames (<date>-<version>-nexus-fhir-ig-full.zip), so all three variants now share one naming scheme; anyone globbing the old name needs to update it.- The seed-data generator is type-checked (FHIR-29), with
npm run typecheckwired into the pre-PR gate alongside the existing coverage guarantee. -
IP statements are published, and the IG now declares who publishes it (FHIR-29). New IP statements page listing the licence terms of every code system this specification binds. Read it before implementing: SNOMED CT content requires an appropriate SNOMED International Affiliate licence in your jurisdiction, and that obligation passes through to anyone deploying against this IG. It was previously stated nowhere.
publisher,contact,jurisdiction(Canada) and acopyrightstatement are now set on the IG.copyrightcompiles into every StructureDefinition and ships insidepackage.tgz, so the licence terms now travel with the artifacts to a tool that never loads the site. The licence itself is unchanged: CC0-1.0 for this specification's own text, which is why the third-party terminology terms needed saying separately.The dependency table, cross-version analysis and globals table are also published for the first time, on the publisher render's new IG-metadata page. All four were being generated and discarded.
Changed¶
DocumentReferencenarrows to attached binary documents; encounter notes move toComposition(FHIR-29). A re-cut rather than an edit, so no constraint tightened in the diff and nothing fails validation -- but the BOUNDARY between the two profiles moved, and that is a consumer-facing change.Compositionis now the model for an authored encounter note;DocumentReferenceis for a document that arrives as a file (PDF, scan, Word, fax). If you write textual notes toDocumentReferencetoday, that shape is no longer the modelled one, and the same rule is restated on both profile pages. No migration is forced in this release: nothing rejects an existingDocumentReference-borne note, and both profiles remain valid to read.
-
Practitioner.identifier[userId]now names the authority the platform actually stamps (FHIR-29). The slice pinnedhttps://fhir.apps.health/NamingSystem/user-id; every real Practitioner carrieshttp://services.well.com/fhir/NamingSystem/user-id. Becauseidentifierslicing is open, that mismatch never failed validation -- the identifier fell into the open remainder and simply never matched the slice, so anything readingidentifier[userId](or a generated accessor for it) found nothing on every Practitioner in the estate. The slice is now pinned to the real system and matches data. If you looked for this identifier by the old system and found nothing, that is why.The authority stays on the internal host. Moving it to the governed one is a data migration across every stamped Practitioner plus its search parameter, not a spec edit, and it is not in this release.
appointment-participant-provideris renamed toappointment-participant-practitioner(FHIR-29). Consumers matching on the invariant key must update it -- for the second time in four releases. 1.12.0 renamed this key fromappointment-participant-practitionerroleto-providerwhile widening the rule underneath it.-providernames an abstraction with no resource type behind it, while every sibling key in the profile (appointment-participant-patient,appointment-participant-location) names the referenced FHIR resource type.Practitioneris this IG's primary provider identity, so the key now says so. The rule itself has not changed since 1.12.0 and no data or code change is needed beyond the key string: at most one participant whose actor references a provider, counted across both permitted spellings (PractitionerorPractitionerRole), which is what the Description still states. Raised in review -- in a published artifact, a key that makes the reader guess what it constrains is a defect rather than a matter of taste.
Removed¶
-
Dead internal tooling removed: 39 files, 20 npm scripts (FHIR-29). The seed-data generator package carried a lane of scripts built on the typed-fhir codec that was retired in July 2026 -- the MedPlum load harness, the Synthea patient source, the benchmark drivers and round-trip gates -- none of which could run any more. All of it is gone, along with the npm scripts that only pointed at it (
emit:*,bench:*,medplum:*,test:corpus,test:reconstruct,test:fidelis*,ref:roundtrip,generate:fidelis*).No effect on this IG or on anything generated from it. The seed corpus is unchanged -- still 51,155 resources at 43/43 profiles, 577/577 must-support, 48/48 bindings, 60/60 invariants, 22/22 extensions -- and
npm run generateremains the supported way to produce IG-conformant synthetic data. Previously published benchmark measurements are retained in the generator'sdocs/, marked as historical, and now point at that generator for any new measurement.
Fixed¶
-
AI-scribed note authorship had no conformant shape (FHIR-29).
Composition.authorandsection.authorconstrained their Device targets to the two PROFILED Device types (nexus-core-appdevice,nexus-core-emrdevice), while an AI agent is named by a LOGICAL Device reference --Reference.identifierunder thenexus-harness-graphNamingSystem, with no resource on the server. A logical reference cannot conform to a profiled target, because conforming to a profile presupposes a resource that exists to validate, so an AI-authored note could not name its author conformantly at all. Both elements now admit bareDevicealongside the profiled types, matchingClinicalTask.requester/.owner, which already did. The profiled targets stay for a Device genuinely registered in this store.The
authorprose was stale in the same way: it still described agent identity as an open question, which this release settled. It now states the mechanism, and says why bareDeviceis a requirement of that mechanism rather than laxity in the target list.
- Eight conformance rules on the new profiles are NOT machine-verified (FHIR-29) -- worth
knowing if you rely on the generated validators rather than a full FHIR validator.
CarePlan.intent,Coverage.status,FamilyMemberHistory.status,Goal.lifecycleStatus,ImagingStudy.statusandMedicationStatement.statuscarry required bindings whose R4 value sets cannot be expanded from the vendored IG, so the generated validators report them as unverifiable rather than checking against a partial expansion.RiskAssessmentras-2uses FHIRPath type operators (is/as) outside the compiled subset, andras-1is anchored below the resource root. All eight are recorded in the IG's frozen unverifiable-rule allowlist with their reason, so the set cannot grow silently; a full validator still enforces every one of them.
- Internal build tooling corrections (FHIR-29): a dead option removed from two internal validation calls (no change to what is enforced -- strictness is unconditional), the coverage report now naming the invariants it counts as uncovered, and a date defect in a generator diagnostic. None of these change the IG, the generated libraries, or the corpus.
[1.15.0] - 2026-08-11 -- WITHDRAWN¶
Do not use 1.15.0. It was published on 2026-08-11 and withdrawn the same day. Its content is
released unchanged as 1.15.1, minus one item: 1.15.0 renamed every identifier in this IG's
namespace, dropping the -emr- infix, which changed canonical URLs. Those URLs are not only
definitions -- CodeSystem URLs are Coding.system and NamingSystem identifier systems are
Identifier.system, so they exist in stored data. The rename is retracted and the identifiers
in 1.15.1 are the same as in 1.14.1.
If you fetched 1.15.0, move to 1.15.1 and revert any identifier constants you changed. Nothing else differs. If you are on 1.14.1, no identifier changed and there is nothing to do.
The archived package for 1.15.0 is deliberately still served, so a tool pinned to it keeps resolving; it is a real artifact that was really published.
[1.14.1] - 2026-08-09¶
Generated by TypedFhir 0.8.0.
Added¶
-
SMART on FHIR exposure is now declared per profile (the new
smart-exposedextension). Whether a resource type can be reached by a SMART app is an authorization decision, and it is now recorded in the IG rather than inferred from whether the resource happens to be profiled. A profile carrying the extension is reachable, and names the platform permission each operation costs (observation:read,observation:write,observation:delete). A profile without it is not reachable at all, so a newly profiled resource stays closed until someone decides otherwise. 29 resource types are exposed,Compositionamong them, since an encounter note is core clinical content for an app. The seven experimental profiles (Communication, DetectedIssue, MessageHeader, Provenance, QuestionnaireResponse, Specimen, Subscription) are deliberately left closed. Six of the 29 are exposed read-only —Practitioner,PractitionerRole,Organization,Location,ScheduleandSlot— because they are reference and directory data the EMR maintains rather than something a third-party app should rewrite, and a delete there is data loss rather than a workflow step.For app authors this is the answer to "what do I ask for": every resource page now carries a SMART tab listing the scope strings that work against that resource and the permission each one resolves to. The permission tokens are referenced here, not defined: they belong to the EMR's own role model (the same tokens an administrator sees when building a role), and code generation fails if the IG names one that does not exist there. SMART's scope grammar itself (the
crudsinteraction letters,.read/.writefolding, wildcard expansion) is deliberately not modelled in the IG: translating it is the gateway's job, and the IG answers only "is this exposed, and what does each operation cost". (FHIR-28)
- TypedFhir 0.8.0 surfaces SMART exposure in all three ports and now publishes as a NuGet
package.
SmartExposedResources,IsSmartExposed()andGetSmartScope()(snake_case in Python) are generated from thesmart-exposedextension, so a consumer asks the library what an app may reach rather than transcribing the answer. Generation refuses to emit a table naming a permission the platform does not define, and a test asserts the same on every CI build, which is what keeps the IG's published permission tokens and the EMR's role model from drifting apart. The C# port ships asWell.Services.TypedFhirwith an optionalWell.Services.TypedFhir.Firelyadapter for hosts that hold Firely POCOs; the core takes no Firely dependency. (PSD-721)
Fixed¶
effective-dateis now a declared extension. 38 profiles stampedhttps://fhir.apps.health/StructureDefinition/effective-datevia theEffectiveDate/NoEffectiveDateRuleSets while nothing defined it, so the canonical resolved to nothing and the IG Publisher objected on every single use. The declaration changes no stamped data; it only makes the URL resolve, and adds the constraint the RuleSets always implied but could not state: a profile declares either which paths carry its date (26 do) or that it deliberately has none (12 do), never both and never neither. Its siblingsalient-fieldwas declared all along, which is what made the gap visible. (FHIR-27's open item, closed here alongside the newsmart-exposed.)
- 18 CodeSystem and ValueSet ids no longer contradict their own canonical URLs. Each carried a
cs-/vs-prefix its URL did not (cs-nexus-emr-province-stateagainst.../CodeSystem/nexus-emr-province-state), which the IG Publisher rejected three times over per resource — 54 errors. The id was corrected to match the URL, never the reverse: a canonical URL is the published contract and every existing binding and reference points at it, while the id is local and the resource type already disambiguates a CodeSystem from a ValueSet of the same name. No canonical URL changed, so nothing downstream needs updating. (FHIR-28)
- A second, dead copy of four FSH files is gone.
doc/fhir/input/fsh/held duplicates of the province-state CodeSystem and the gender-identity, pronouns and province-state ValueSets. The build readssrc/fsh/(staged into.build/bybin/build.sh), so the copies shipped nothing and nothing referenced them — but they had already silently rotted: their titles were the pre-rename originals, so anyone reading or editing them was working from a stale file that looked authoritative. (FHIR-28)
- The Invoice example's practitioner reference resolves. It pointed at
Practitioner/dr-navarrowhere the example's actual id isdr-emil-navarro, so the reference dangled in every validation run. (FHIR-28)
[1.14.0] - 2026-08-09¶
Added¶
- Item consent (
nexus-emr-core-item-consent) — a patient-levelConsentprofile for the Patient Profile's Consent section: one Consent per (patient, item) for the fixed phase-1 catalogue (email, SMS, AI scribe — the newnexus-consent-itemCodeSystem/ValueSet), updated in place with the decision read fromstatusalone (active= provided,rejected= declined, no resource = not recorded) and the full decision history served by FHIR versioning (_history/vread); includes the optionalnexus-consent-status-reasonextension, evidence viasourceReferenceto aNexusEmrCoreDocumentReference(theAttachmentchoice is removed fromsource[x]so evidence stays behind DocumentReference read permission), examples, and the item-consent feature guide. Unlike its siblings the profile carries no raw-identifier invariant — an item consent is born in the EMR, so there is no source system to point back at and the rule could only ever be satisfied by inventing one — and instead warns (item-consent-status-is-defined,item-consent-performer-is-attributable) where the prose asks for more than cardinality and bindings can express: a status outside the three codes it defines, or a decision with no attributableperformer.display. The guide adds the migration caveats this model creates: version history does not survive a bulk export/import and must be replayed per version, and the deterministic identifier embeds the Patient's logical id, so re-keying patients means recomputing every one. (PSD-501, epic PSD-316)
-
Compositionis now the model for authored clinical notes — encounter notes, consult notes, telephone notes: anything an EMR user writes or an AI scribe drafts and a clinician signs. The narrative lives in FHIR as XHTML insection.text, so a note is readable, searchable and diffable without dereferencing a blob, andComposition.statuscarries the lifecycle (preliminarydraft →finalsigned →amended) instead of an invented extension. Amendments are version history over one resource — edit in place and setstatustoamended; the business identifier does not change andrelatesTois deliberately left unprofiled so it stays available if a true addendum workflow later appears. A signed note must name its signer (composition-signed-has-attester).authoradmits aDevice, so an AI-drafted note can say so on the resource, while attestation remains the clinician's alone. Two examples ship: a signed progress note, and a scribed telephone note that was signed and then corrected. Decided on the August 2026 SMART-launch review. (FHIR-27)This re-introduces a profile removed four days earlier, deliberately.
bde96cee4(1.11.0) deleted an experimentalNexusEmrCoreCompositionon the grounds that the surface was "experimental and unused, and publishing it invited implementers to build against a shape the IG was not committing to." That reasoning was sound and is now spent: there is a committed consumer. What returns is only the encounter-note subset — not the four-section-type (text/form/canvas/file) surface with recursive containment and coordinate annotations, whose removal also cleared two unevaluable-slicing defects.section.codeis left unbound for the same reason: no producer populates it yet.Known dependency, stated plainly. The amendment model rests on version history, and version history is server-local —
_historydoes not travel in bundles,$everything, or sync pipelines. This IG also publishes no CapabilityStatement, so no versioning policy is declared anywhere, andmeta.versionIdis server-opaque in R4 and unconstrained here: consumers must not sort or parse it to determine which version was signed. Reconstructing the as-signed note is reliable only on the authoring server today. Declaring the version-history contract is outstanding work and a prerequisite for relying on this across boundaries.
- IG HTML publishing is scripted —
bin/publish.shrenders the IG to browsable HTML and optionally zips it for sharing (-full/-slim, both labelled so neither can be mistaken for the other or overwrite it). This is release step 6 of the IG Owner Release Process, which until now said only "note the deferral". Also establishesbin/as the home for IG maintenance scripts, with the conventions written down. (FHIR-27)
Changed¶
DocumentReferencenarrows to attached binary documents — scans, faxes, external PDFs, Word files, consent forms: content that arrives as bytes. It is no longer the home for textual encounter notes (seeCompositionabove), and the profile now states the boundary rule explicitly: did we author it, and is the content text we control? A consult letter received as a PDF stays here; a consult note typed in the EMR does not. This is a deliberate divergence from US Core, which models clinical notes as DocumentReference; the rationale is recorded on both profiles. (FHIR-27)
DocumentReferencecontent documentation now matches what the gateway actually does. The profile described content as inline-base64-first and claimed external URLs were unsupported — both untrue since the gateway grew document storage, so implementers were reading a contract the runtime no longer honoured. Now documented: submit inline base64 with nourl; the gateway stores the bytes and rewrites the attachment (setsurl, computessizeand SHA-1hash, clearsdata), so readers get a URL and no inline data. The URL is an absolute File Storage Service address, not a FHIRBinaryreference and not a pre-signed/SAS link — fetching it needs the caller's own bearer token and resolves within the caller's tenant. It is stable for the life of the document and survives updates. Sendingdataandurltogether bypasses storage and persists the base64 inline permanently; the profile now warns against it. Client-supplied URLs are host-validated and rejected with a 400 when untrusted. (FHIR-27)
- Removed the
prosesalient accessor fromDocumentReference. It decodedcontent.first().attachment.dataand offered it to chart and model readers as long-form text. Wrong on two counts once the scope narrowed: the content is now a PDF or TIFF, so decoding it yields binary rather than prose; and the gateway clearsdataon write, so it resolved to empty for every gateway-ingested document anyway. Replaced withtitleandurl. The narrative reader surface isComposition.section.text. (FHIR-27)
- Dropped the boilerplate "Future Considerations" sections from 16 profile intro pages — speculative "potential enhancements being evaluated" lists that read as commitments the IG had not made. 156 lines removed; no profile, element, or binding changed. (FHIR-27)
- The tree is restructured around
src/and a contained build. FSH now lives insrc/fsh/(wasinput/fsh/), examples insrc/examples/, per-profile narrative insrc/pages/, and the IG configuration (sushi-config.yaml,ig.ini,ignoreWarnings.txt) insrc/ig/. The FHIR toolchain's hard-coded conventions no longer shape the tree:bin/build.shstagessrc/1:1 into the gitignored.build/and runs SUSHI (and, at publish time, the IG Publisher) in there, so all generated output and tool scratch (fsh-generated/,temp/,template/,input-cache/, the rendered site) is contained in one disposable directory.conformance/run-conformance.shfolded intobin/conformance.sh—bin/is the single home for tooling. No profile, example, or canonical URL changed — this is a layout-and-tooling change only. (FHIR-27)
- The IG is titled "Nexus IG" (was "Nexus EMR FHIR Implementation Guide") — nothing
else in the Nexus ecosystem defines an IG, and the spec serves more than the EMR. The
formal title, the site name (which now carries the version, e.g. "Nexus IG 1.14.0"),
and site prose all use the short form. Identity is untouched:
id: nexus-emr, the canonical base, and all profile slugs are unchanged. This CHANGELOG is now published on the site and linked from the front page. (FHIR-27)
- Artifact display titles normalized — three authoring generations of title styles
("X Profile Core", "X Core", "X (Minimal) Core", unprefixed extensions) collapse to one
pattern:
"<X> Profile"with qualifiers trailing ("Slot Profile (Minimal)"), and noNexus EMR/Nexusbrand prefix (the canonical URL carries the namespace). 53 titles changed. Display-only: no id, URL,name, or element change;meta.profilevalues and references are unaffected. (FHIR-27)
- The human-readable IG is now one MkDocs site, not the IG Publisher's HTML.
bin/site.shbuilds (or serves, with live reload) the guide plus a FHIR reference generated from the compiled IG — profile pages with differential tables, bindings, invariants and linked examples — by TypedFhir's newig-docsrenderer (@awaremd/typed-fhir/ig-docs). The IG Publisher survives as the package + QA factory (bin/publish.sh): validators and generators consume itspackage.tgz, published at the site root next topackage-list.json; its Jekyll HTML is not published anywhere. Profile pages present the resource as five tabbed views — Differential, Key elements, Snapshot, FSH source, Examples — and example JSON is hyperlinked (references to other examples and canonical URLs are clickable, with "Referenced by" backlinks), so instances browse as a graph. The site carries the Nexus EMR frontend's WELLSTAR Stardust branding (blue-on-clay light, navy-clay dark, IBM Plex, the Nexus mark). Profile pages also carry a Salient fields view (the synthesized-accessor contract: key, type, FHIRPath expression) and a TypedFhir API view with TypeScript / Python / C# sub-tabs showing the generated facade for the profile's resource type. Extensions used by exactly one profile nest under that profile in the sidebar; only shared/cross-cutting ones remain top-level. Also fixed en route:sushi --snapshot .silently dropped the flag (0/51 snapshots) — the staged build now runssushi build . --snapshot(51/51), which is what TypedFhir's vendored artifacts are regenerated from. (FHIR-27)
Fixed¶
- Organization references in three Patient examples used a NamingSystem the Organization
example does not own (
.../cedarbrook-clinic-organization-identifiervs theorganization-identifiersystem the Organization profile's invariant names) — the references were unresolvable as data. Normalized toorganization-identifier. (FHIR-27)
- Four declared-but-unwired extensions are now wired to their profiles. An extension
audit found extensions the IG declared but no profile referenced: the App State
string/boolean value carriers (the App State profile's entire payload surface — one was
already used by the shipped example), Prevention Status (carried by both worked
prevention Observation examples), and Patient Record Inactive Reason (declared intent
for heritage record-status migration, referenced nowhere). Each is now an explicit
optional slice on its profile — App State
stringValue/booleanValue, ObservationpreventionStatus, PatientrecordInactiveReason— so consumers can rely on the slice names and a reader can tell a decision from a leftover. None are must-support. (FHIR-27)
- Appointment Modality's Encounter context removed — nothing consumes modality on an Encounter; extension contexts follow observed consumption. Re-add with evidence. (FHIR-27)
- The IG had never been publishable as configured.
ig.ininamed./nexus-emr.template, a directory the fork/rename from Polaris never created, so any attempt to render HTML failed on a missing template before doing any work. It now usesfhir.base.template#current. (FHIR-27)
1.13.0 does not exist, and never will¶
There is no 1.13.0. It was skipped deliberately, going straight from 1.12.0 to 1.14.0, for exactly the reason buildings skip the thirteenth floor: none. There is no technical reason, no withdrawn release, no aborted branch, and nothing to go looking for.
To be plain about it, since a changelog is the wrong place to be coy: we have no interest in the superstition itself. But a version string is not private. It gets read aloud in meetings, quoted in vendor email, pasted into conformance statements and printed in RFP responses, and somewhere along that chain is a reader for whom the number is a small distraction. Skipping one integer costs nothing. Explaining it every time costs more.
Recorded here so nobody reading the history later assumes a release was pulled, hunts for a lost branch, or "corrects" the gap. If you need it as a rule: version numbers ending in 13 are not used. The successor to 1.12.0 is 1.14.0; the successor to 1.19.0 will be 1.20.0.
[1.12.0] - 2026-08-05¶
Never released as a standalone version. 1.12.0 was authored and reviewed (PR #858) but never merged: the merge window fell during a GitHub Actions outage on 2026-08-06 (workflow runs failed to resolve their own actions and could not be made green), and rather than fight the platform the work was folded forward into 1.14.0. Everything in this section ships as part of 1.14.0. The section is kept for the record so the modelling history reads continuously; there is no 1.12.0 tag or published artifact.
Version note. This release contains changes that are technically breaking — two renamed invariant keys and two new
requiredbindings — and is still numbered as a minor. That is deliberate and now written down: while the IG carriesstatus: draft, a breaking change bumps minor and the migration note rides in the CHANGELOG entry instead. See IG Owner Release Process §4. The carve-out ends whenstatus:leavesdraft. Every break below is called out explicitly with the consumer action; that is the obligation the carve-out comes with.
Added¶
-
Seven new draft profiles, for the resource types this stack demonstrably reads or writes with no profile behind them —
DetectedIssue,Communication,QuestionnaireResponse,MessageHeader,Subscription,Specimen, andProvenance. All are#draft+experimental = truefirst passes: each declares its identifier contract, its effective date (orNoEffectiveDate), its patient/subject reference, and the constraints load-bearing for the surface already consuming it — and each names inline what it deliberately does not settle, so a reader can tell a decision from a placeholder. Highest-consequence of the set:DetectedIssue(the prescribing override audit trail, previously ungoverned),QuestionnaireResponse(the chart's journey timeline could not date it), andProvenance(a deliberate placeholder for AI-Harness action attribution — token/model accounting and the gatewayUpdatedByreconciliation are explicitly left open).Scope note for IG consumers: profiles are added for observed consumption, not for reachability. Types that are merely granted a SMART app scope, or merely traversed as a containment field, do not get a profile on that basis — the app scope is the thing that should narrow.
Specimenqualifies on a narrower basis than the others and says so in its own notes: one real site resolvesServiceRequest.specimenreferences in eReferral bundles, and nothing reads a Specimen's own fields, so it is governed as a pass-through type. Still deliberately NOT profiled on the same rule:CarePlan,Goal,Coverage,MedicationStatement,ImagingStudy,Media,MedicationAdministration,Medication. (FHIR-25)
- Heritage-EMR situations are now documented on the affected element definitions, and flagged as
UNRESOLVED — so the caveat travels into the IG pages and the generated typed API rather than
living in a reviewer's head. Covered:
Patient.address.state/.country(heritage EMRs store province as unvalidated free text, so the newrequiredbindings are an open migration risk — the distribution has not been measured);Specimen.identifier/.accessionIdentifier/.subject(heritage lab results carry an accession and little else, with no cross-lab namespacing for it and a patient link that is inferred rather than asserted); andCommunication.identifier/.category(heritage message/inbox history is not FHIR Communication and whether to migrate it at all is undecided — every Communication in this IG is currently either WELL-native or external eReferral, never heritage-migrated). Each note says what is unknown and what would resolve it. (FHIR-25)
Provenance.agent.typeis1..1— tightened from the base0..1to match US Core 9.0.0us-core-provenance, which also sets itmin 1. It is the only element distinguishing a non-human generating agent from the human who accepted its output, which is the whole reason the Provenance placeholder exists; two untyped agents are an ambiguous record. Note this IG deliberately does not adopt US Core's must-support ontarget.reference— that would require literal references, and the Nexus logical-reference model permits identifier-only. (FHIR-25)
- New Patient example for the 1.12.0 identity surface —
Patient/rowan-mercier-no-health-cardexercisesreasonForNoHCN(on the MRN identifier),genderIdentityandpronounsin their correct complex form,name.prefix, and both requiredaddressbindings. Added because the new must-support elements previously shipped with no conforming instance, and the complex-extension shape (extension[value].value[x]; the extension root'svalue[x]is0..0) is the one contributors get wrong. (FHIR-25)
-
Reason for no health card number — Ontario's CA-eReC extension, ADOPTED not re-minted —
Patient.identifier.extension[reasonForNoHCN], usinghttp://fhir.infoway-inforoute.ca/io/CA-eReC/StructureDefinition/CA-eReC-reason-for-no-HCN-EXT. Ontario eReferral requiresPatient.identifierto carry either the health card number or a documented reason for its absence; this is the "or" half, and it completes the PHN identifier work begun in #768. New IG dependency:ca.on.erec.r4pinned at0.12.6-TU-snapshot1(exact pre-release pin — Ontario's own artifact isdraft0.10.1 and will move).Two things consumers should know. (1) The context is
Patient.identifier, notPatient— so it rides on an identifier the patient does have, in practice the MRN slice, because in exactly the case it describes there is no jurisdictional identifier to attach it to. SUSHI propagates the element to the baseidentifierand to both thejurisdictionalandmrnslices. (2) Ontario's value set holds one clinical code (#no-HC) plusdata-absent-reason#not-asked/#asked-declined, so it cannot distinguish newborn-awaiting-registration from refugee-on-IFHP from card-not-presented; carry those specifics in.text, or as an additional coding from a CodeSystem the IG declares. A Nexus-canonical equivalent was drafted and withdrawn in favour of the jurisdictional canonical, so later Ontario/Union-IG conformance is re-badging rather than remodelling. (FHIR-25)
Encounter.appointmentis now modelled — the Appointment↔Encounter link, with the primary-care 1:1 expressed as a warning invariant plus prose rather than as cardinality.appointmentstays0..*: R4 permits several Encounters to reference one Appointment (a visit that splits into multiple encounters — routine for specialists) and one Encounter to fulfil several Appointments, so tightening the cardinality would foreclose both and have to be unwound. Documents that Encounter is the association anchor for clinical resources, and that R4 has no reverse element on Appointment, so that direction requires a search. (FHIR-25)
Changed¶
-
Address.stateandAddress.countryare now bound — the only prior statement was non-computable prose ("valid province codes"), and the divergence it permitted is already realized in production: one writer emits ISO alpha-3 while the address UI emits alpha-2, consumers do positional string surgery on the result, and outbound jurisdictional HL7 has been observed carrying"Ca-io"and"CANio".statebinds required to the newNexusEmrProvinceStateVS(bare two-letter subdivision codes, 13 CA + 56 US, mirroring the frontend roster);countrybinds required toiso3166-1-2(alpha-2), with alpha-3 an edge form only. Both are deliberatelyrequiredagainst this IG's usually-lenient posture, because a closed roster is the only thing that makes the corruption fail loudly;patient-address-country-alpha2(warning) names the observed bug so the diagnostic survives a lenient validator. ⚠ Producers writing spelled-out province names or alpha-3 country codes become non-conformant.⚠ Migration caveat, carried in both elements' definitions so it reaches the IG pages and the generated typed API: these bindings were validated against the IG's own examples only — legacy inbound source-EMR data has not been surveyed, and source systems emit uncoded free-text on adjacent address fields, so spelled-out province names are plausible in existing records. Implementers migrating a corpus SHOULD profile their
state/countrydistribution before enforcing, and MAY treat the bindings asextensibleduring migration (the warning invariant still reports the alpha-3 case). The caveat comes out of the definitions once the survey is done. (FHIR-25)
genderIdentityandpronounsnow carry bindings — the IG adopted both HL7 extensions and bound neither, leaving real terminology governance in frontend TypeScript on must-support elements.genderIdentitybinds required toNexusEmrGenderIdentityVS, deliberately broader than the HL7 value set it inherits: 10 of its 13 SNOMED concepts are Canada Health Infoway namespace (pan-Canadian GSSO) and are not members ofhl7.org/fhir/ValueSet/gender-identity.pronounsbinds extensible toNexusEmrPronounsVS(LOINC answer list), since pronoun sets are open-ended. Note both are complex extensions: the bindings sit onextension[value].value[x], not on the extension root, whosevalue[x]is0..0. Absence reasons remain adata-absent-reasonextension on the value and are not value-set members. (FHIR-25)
- No generated directory is version-keyed any more, so a release renames nothing. The generated
registry subpackage moves from
src/ig/generated/nexus_emr_v<major>_<minor>_<patch>/tosrc/ig/generated/nexus_emr/(and the Python twin), finishing what 1.11.0 started when it de-versioned the vendoredig/nexus-emr/directory. Exactly one version is generated at a time, so the version in the path carried no information the artifacts did not already hold — and it forced agit mvon every release, which made git record a whole-directory delete-plus-add and drowned the real spec diff in a release PR. The registry selector key is unchanged (nexus-emr-v1.12.0); the version still travels in the generated file headers, inIG_META, and in that key. Consumers importing the generated modules by path must update the path once; consumers resolving through the registry are unaffected. (FHIR-25)
- Three profiles documented a PractitionerRole requirement their own constraints had already
dropped —
DocumentReference.authenticatorread "Reference must be to a NexusEmrCorePractitionerRole" two lines above a constraint permitting both;DocumentReference.authorsaid authors are "represented by NexusEmrCorePractitionerRole";Patient.generalPractitionersaid it "links to PractitionerRole". The same defect class 1.12.0 fixed inresource-relationships.md, and worse in effect, because these strings are baked into TypedFhir's generatedFieldDocand so mis-teach every consumer of the generated facade. Prose only — no constraint changed. (FHIR-25)
- Practitioner is the primary provider identity; the Appointment provider participant is
no longer PractitionerRole-shaped — this release relaxed every provider reference element
to accept
PractitioneralongsidePractitionerRole, butAppointmentwas missed. The invariantappointment-participant-practitionerroleis renamed toappointment-participant-providerand now counts the provider participant regardless of which resource names them. The old rule was vacuous — aPractitioner/-shaped participant scored zero and passed — while reading as though PractitionerRole were required.Appointment.participant's definition and comment, and thecreatedByextension's reference-target order and prose, now put Practitioner first. The walk-in example's provider changed fromPractitionerRole/dr-navarro-familymed-roletoPractitioner/dr-emil-navarro, so aPractitioner-shaped provider finally has a canonical example; the with-slot example deliberately keeps itsPractitionerRoleprovider so both permitted spellings are exemplified. Consumers validating against the old invariant key must update it. (FHIR-25)
Schedule.actornow accepts a raw Practitioner as the provider — Schedule was the only place a PractitionerRole reference was a hard requirement rather than a target-type constraint:schedule-actor-requireddemanded one of PractitionerRole / Location / HealthcareService, so a Practitioner-shaped Schedule — the shape a primary-care writer would naturally emit — failed outright. The invariant now accepts a provider under either spelling, andschedule-actor-practitionerrole-maxis renamed toschedule-actor-provider-max, capping the provider actor at one whichever resource names them.actor's definition documents both. Consumers validating against the old invariant key must update it. (FHIR-25)
- The "PractitionerRole (Primary) / Practitioner (Secondary)" guidance is corrected —
guide/patterns/resource-relationships.mdsaid PractitionerRole was primary and that Practitioner was "always accessed through a PractitionerRole." That was the origin of the PractitionerRole-primary reading in this IG, and it kept being re-derived from that page after the model changed. Practitioner is now documented as the primary identity (it carriesidentifier[userId], per the SMARTfhirUserprecedent) and PractitionerRole as the tenant-scoped role binding — still required where the clinic/role relationship is the assertion, sincePractitionerhas noorganizationelement. (FHIR-25)
Fixed¶
-
The validator rejected two shapes the IG itself models. Both were FALSE POSITIVES on conformant data — the worse failure direction, since a validator that rejects valid instances gets routed around. Both are fixed in all three ports (TS / Python / C#), and both were found by putting the new 1.12.0 profiles through the synthetic corpus.
- A partially-expanded value set was enforced as if it were complete. The facade generator
baked whatever codes it could enumerate and checked against them, so any code reachable only
through an include it cannot expand read as "not in the value set". Concretely:
Consent.categorybindsrequiredtoconsent-category, which unionsv3-ActConsentType— a filter-based (concept is-a _ActConsentType) include — sov3-ActCode#INFAwas dropped from the enumeration even though it carriessubsumedBy: _ActConsentTypeand IS a member. The IG pins exactly that code, so every conformant app-consent instance was reported as violating its own profile. A partial expansion is now treated like no expansion: the binding is reported as unverifiable rather than enforced against an incomplete list, matching what the runtime registry already did. Five bindings moved into the unverifiable set as a result (seeinvariant-skip-allowlist.test.ts) — the enforcement was never real, only the reporting changes.
- A required primitive carrying only a
data-absent-reasonextension read as absent. The cardinality check counted values, not element presence, so the standard FHIR form —_occurrenceDateTime: { extension: [ data-absent-reason ] }with no value — failedrequired (min=1). That is the exact escape hatch this IG models onImmunization.occurrence[x], so the validator was rejecting the pattern the profile provides. Presence now counts the_-prefixed primitive-extension sibling (choice forms and repeating primitives included); value-shape and binding checks still see no value, which is correct. Deliberately scoped to the cardinality check — the shared FHIRPath navigator is unchanged, soexists()/count()semantics inside invariants are untouched. (FHIR-25)
- A partially-expanded value set was enforced as if it were complete. The facade generator
baked whatever codes it could enumerate and checked against them, so any code reachable only
through an include it cannot expand read as "not in the value set". Concretely:
- Strict validation now checks datatype shapes — it previously checked none — the
generated per-profile
validate()(what the fixture gate, CI and harness ingest all call) verified cardinality, value-set bindings, fixed/pattern equality and the compiled invariants, and nothing about datatypes. So a string insideQuantity.valuereported zero issues:valueQuantitywas present soobs-6was satisfied, a Quantity carries no bindable code so binding rules stayed silent, and nothing onvalue[x]was fixed. Shapes were checked in the accessor layer, but bulk validation never calls it.ELEMENT_RULESnow carries each element's declared FHIR type (choice elements resolve theirs from the instance key,valueQuantity→ Quantity) and the loop checks JSON type plus the primitive members of the common complex types, soQuantity.value,Period.startand friends are reached. New issue code:datatype-shape. Implemented once per port (datatype-shape.ts/datatype_shape.py/DatatypeShape.cs) and held in parity by the facade-validation vectors. Consumers should expect new errors on data that was always malformed — this reports it rather than introducing it. (FHIR-25)
- FHIR primitive lexical forms are validated —
"banana"was a validdateTime— no ISO-format check existed anywhere in the validation stack: the accessor layer'sdateTimetest was "is it a string", and the bulk validator had no test at all. The R4 spec regexes fordate,dateTime,instant,time,id,code,oid,uuidandbase64Binaryare now applied by both layers from one shared table, and integer-family types are range-checked (positiveInt >= 1,unsignedInt >= 0). This immediately caught two long-standing defects in the frozen vector corpus:DocumentReference.dateis aninstant, and two fixtures carried a date-only value — corrected to full timestamps. (FHIR-25)
[1.11.0] - 2026-08-02¶
Fixed¶
- 12 examples were never validated against their profiles — their
meta.profilenamed a canonical that does not exist (nexus-emr-emrdevice,nexus-emr-allergyintolerance,nexus-emr-appdevice,nexus-emr-appstate,nexus-emr-clinicalimpression,nexus-emr-diagnosticreport,nexus-emr-documentreference,nexus-emr-medicationdispense,nexus-emr-servicerequest,nexus-emr-document-bundle-core), all missing the-coreinfix the profile Ids actually carry. The validator cannot resolve an unknown profile, so it silently falls back to base FHIR: those examples reported "0 errors" while never being checked against the profile they advertise. Corrected to the declared Ids, so every example is now checked against the profile it claims. (FHIR-25)
- CareTeam, Flag and RelatedPerson now declare an effective date — the
EffectiveDate/NoEffectiveDatesweep covered 26 profiles but not the three added alongside it, so consumers fell back to guessing. All three declareNoEffectiveDate: they are parties and administrative assertions, not dated events. Without it, RelatedPerson derivedbirthDateas its effective date — the exact invented-date failure the ruleset exists to prevent. (FHIR-25)
identifierdocumentation no longer claims "required" on optional profiles — the FHIR-22 relaxation droppedidentifierto0..*on 26 profiles, but 21 still carried^short/^definitiontext saying "at least one business identifier is required" (and DocumentReference documented the deleted invariant as normative: "At least one identifier MUST follow the Nexus EMR naming pattern"). Those strings render on the IG profile pages and travel inside the StructureDefinitions, so implementers saw "required" on an optional element. Reworded to "recommended but not required". ClinicalTask (1..*) keeps a floor, so its wording is unchanged. (FHIR-25)
- Patient identifier slicing is now evaluable — the
jurisdictionalandmrnidentifier slices now carry apatternCodeableConceptonidentifier.type. The slice used atypepattern-discriminator but fixed onlytype.coding.*(grandchildren the matcher does not traverse), so every Patient instance failed slice evaluation in the HL7 validator. No change to what conforms; the slicing now actually validates. (FHIR-25)
- Example instances corrected to validate cleanly — fixed pre-existing errors
across the example corpus: Task
reasonReferencecardinality (0..1) + input/outputtypecode systems + a task-code display; MedicationRequest identifier/timing/dose; Consent attachment size/hash; a stray Invoice line-item extension; a double-LOINC BP Observation; and the ROUTINE/WALKIN appointment display names. The full example set now passes the newconformance/run-conformance.shgate: 50/50 instances, 0 errors. (FHIR-25)
Removed¶
- The experimental clinical-document surface is gone —
NexusEmrCoreCompositionandNexusEmrCoreDocumentBundleare removed from the IG, along with the extensions and value sets that existed only to serve them:nexus-emr-markdown-content,nexus-emr-embedded-section,nexus-emr-embedded-section-answer,nexus-emr-embedded-section-response,nexus-emr-canvas-annotation,nexus-emr-canvas-metadata,nexus-emr-file-metadata, and thenexus-emr-embedded-section-types/nexus-emr-canvas-annotation-types/nexus-emr-coordinate-systems/nexus-emr-file-categoriesvalue sets. The clinical-document guide pages and the document-bundle example go with them. The surface was experimental and unused; keeping it published invited implementers to build against a shape the IG was not committing to. Clinical documents remain representable throughDocumentReference. Profiles drop 31 → 29, extensions 22 → 15, value sets 17 → 11. (FHIR-25)
NexusEmrCoreRosterPanel(the FHIRGroup-based panel profile) and its CPAR-specific panel/submission extensions (PanelFacilities,PanelConfirmationDate,PrimaryProviders,SubmitDuringNextWindow,AutomatedSubmission) — superseded by the primary-care-attachment model above.
- The deprecated contained-Medication pattern: the
NexusEmrContainedMedicationCoreprofile and themedicationReference/ mandatorycontainedwiring on MedicationRequest.
- The CPAR and CII feature areas (mapping guides + CPAR-specific FSH). These were dependent on the old Polaris estate; they can be reintroduced against the new model if needed.
Changed¶
- ClinicalTask input/output remodelled —
Task.outputis removed;Task.inputis now the reference(s) to the chart element(s) the task is about (a report, requisition, prescription, problem, ...), withinput.value[x]constrained toReference(any resource). Non-reference input parameters are no longer carried (usenote/descriptionfor free text).input.typeis left as a free-text label for now (no controlled vocabulary bound). This resolves the undefined task-input/output-type code systems the examples had referenced. (FHIR-25)
- MedicationRequest: medication is now represented inline via
medicationCodeableConcept(text required, DIN coding slice,medicationStrengthextension), replacing the mandated contained-Medication pattern.
- Encounter:
classbinding corrected to the canonical ValueSethttp://terminology.hl7.org/ValueSet/v3-ActEncounterCode(was a documentation HTML URL).
- Identifier requirements relaxed across all profiles, in two ways: dropped the
requirement for a Nexus EMR canonical identifier (removed the
*-has-nexus-emr-idinvariants), and dropped the cardinality floor.identifieris0..* MSon 26 of the 28 profiles that carry it: a business identifier is not required, and where one is supplied any system is accepted (no specific canonical is mandated). Two profiles deliberately keep a floor and are the exceptions to this bullet:ClinicalTask(1..*, since a task is tracked and audited by its identifier). (The experimentalCompositionwas the other exception; it has since been removed from the IG.) Carrying a source/raw identifier is now recommended (the*-has-raw-identifierinvariants softened from#errorto#warning; use the most appropriate system URI for the source system).
Added¶
- Per-profile effective-date declaration — every profile now declares, via the new
EffectiveDate(paths)/NoEffectiveDateRuleSets, which element carries the one effective date for that resource type (onsetDateTime | recordedDatefor Condition,period.startfor Encounter, and so on), or that it deliberately has none. It rides on the StructureDefinition as aneffective-dateextension. A resource has exactly one date concept and which element that is differs per resource type, so it is declared in the IG rather than guessed by each consumer; readers that previously inferred a date from whatever date-typed element they found should read the declaration instead. (PSD-322)
- Examples exercising the release's new must-support elements —
name.prefix/name.suffixon the Patient and Practitioner examples, and two new Patient examples for the new attachment-status codes:nexus-emr-patient-not-enrolled-exampleandnexus-emr-patient-fee-for-service-example. (FHIR-25, FHIR-26)
- Primary-care attachment status: new codes + extensible binding — added
not-enrolled(NE, a decided "not rostered", distinct from pending and from an absent attachment) andfee-for-service(FS, seen FFS / not rostered) toNexusEmrAttachmentStatus, and relaxed the status binding from required to extensible so source-EMR clinic-custom statuses (e.g. OSCAR customizations) can be carried without a breaking change. Nootherescape code is defined: the extensible binding lets a source system carry its own code directly, which preserves the original value instead of flattening it toother.attachedretained (Alberta shared panels). Consumer guidance in the rostering guide updated for the expanded enum. (FHIR-26)
- Provider references accept
Practitioner, not onlyPractitionerRole— every IG element previously constrained toonly Reference(NexusEmrCorePractitionerRole)(Patient.generalPractitioner, Immunization.performer, Encounter.participant, MedicationRequest.requester, DocumentReference.author/authenticator, ClinicalTask.owner/requester, primaryCareAttachment provider) now also permitsNexusEmrCorePractitioner, and the multi-targetAppState.subjectandMedicationDispense.performer.actorlists gainedPractitionertoo — realigning with base R4 (which allows a bare Practitioner) and matching how source EMRs reference providers by person. (FHIR-25)
- Appointment authoring (createdBy / created) — a new
createdByextension onAppointmentrecords the authoring agent (a deliberately broad target set: PractitionerRole | Practitioner | Device | Patient | RelatedPerson | Organization, so user-created, self-booked, and system-created appointments are all representable), and nativeAppointment.createdis now must-support for the creation timestamp. Participants continue to model attendance, not authorship. (FHIR-19)
- Name prefix/suffix on
PatientandPractitioner—name.prefixandname.suffixare now explicit must-support subfields (standard R4HumanName), so honorifics (Dr., Mr., Ms.) and post-nominal credentials (MD, RN, NP) have a first-class home rather than being folded intogiven/familyortext. (FHIR-25)
- Contribution guidelines — a two-track contributor / IG-owner model is now
documented (
docs/guide/contributing/), referenced fromREADME.mdandconventions.md, and encoded for agents indoc/fhir/CLAUDE.md: contributors edit FSH only; derived artifacts (compiled resources, TypedFhir types, synthetic corpus) and version bumps are regenerated once by the owners at release. (FHIR-25)
- Prevention status extension (
nexus-emr-prevention-status-extension) onObservationonly — carries the Nexus Preventions product status (completed | completed-externally | refused) whereObservation.status's required binding cannot distinguish completed-externally from completed nor represent refusal-as-declined; Immunization is deliberately out of context (natively representable viastatus+primarySource/reportOrigin/statusReason), and nativeentered-in-erroroverrides the extension. Includes the status CodeSystem/ValueSet (draft/experimental). The FHIR-25 review round added worked prevention examples (PAP completed-externally, smoking-cessation refused, and a catalog-coded COVID Immunization) and clarified that a prevention is marked oncode.codingunder the prevention-catalog NamingSystem, not bycategory. (PSD-465, epic PSD-303; modelling review FHIR-23)
- NamingSystem
nexus-emr-prevention-catalog— declares the EMR-local code system identifying prevention catalog entries (the Preventions module's grouping key onImmunization.vaccineCode/Observation.coderawCode codings). Codes are app-catalog-owned and deliberately not enumerated in the IG; no Health Canada governance implied. SNOMED CT CA / DIN slices are a phase-2 commitment. (PSD-465; FHIR-23)
- Appointment modality extension (
nexus-emr-appointment-modality-extension) onNexusEmrCoreAppointment(andEncounter) — carries the delivery channel (in-person | video | telephone | secure message) as a distinct axis fromappointmentType(booking style), since one CodeableConcept cannot hold both. Value is CodeableConcept (raw source code + normalized Nexus code co-exist); binding is extensible so a pan-Canadian/Ontario code can be added as a parallel coding without a breaking change. Includes the modality CodeSystem/ValueSet and a telephone example. Draft/experimental. (FHIR-21)
- Primary Care Rostering / Attachment modeling proposal + FSH
(
docs/guide/features/rostering/primary-care-attachment.md) — a Patient-levelprimaryCareAttachmentmodel chosen as the IG's rostering direction. The FSH has shipped (PrimaryCareAttachmentExtension.fsh); the status enum is expanded in FHIR-26.
- Standards Alignment guides (
docs/guide/alignment/): HALO IG alignment, the Union IG (what it is + the pan-Canadian stack it consolidates), and a WellServices R4 FHIR-consumption audit — 29 findings across R1-R6 shape mismatches plus a new R7 "un-modeled surface" pass (off-canonical WELL extensions/code systems). Point-in-time references; profiles may have changed since.
- Care Providers & Relationships modeling: two new profiles —
NexusEmrCoreCareTeam(a patient's care providers: physician/MRP, nurse, midwife, referring physician) andNexusEmrCoreRelatedPerson(an alternative contact — a person the practice can reach on the patient's behalf, with a personal relationship, contact purposes, contact details, a note, and an optional primary/call-first flag). The publishednexus-emr-primary-care-attachmentextension is extended in place (nothing renamed or removed): the attachment-status code system gainsnot-enrolledandfee-for-service(OSCAR roster_status coverage), andterminationReasonnow binds (extensible) the newnexus-emr-roster-termination-reasonvalue set. Official codings are used wherever an official set covers the need:CareTeam.participant.rolebinds (extensible) to the FHIRparticipant-rolevalue set (SNOMED CT), with v3 ParticipationTypeREFfor the referring physician the set has no concept for, and the alternative contact's relationship draws from the officialrelatedperson-relationshiptypevalue set (v3PersonalRelationshipRoleTypeplus v2-0131Ofor Other, with v3 RoleCodeGUARDcovering legal guardian). Local terminology is limited to sets with no official equivalent —nexus-emr-roster-termination-reason(Ontario MoHLTC enrolment-report reasons, ministry numeric codes recorded per concept),nexus-emr-contact-purpose(dual-coded with v2-0131 / v3 RoleCode / v3 RoleClass where an official concept exists), andnexus-emr-patient-record-inactive-reason(moved / transferred / duplicate, bound extensible; a death stays on the nativePatient.deceased[x]) — plus extensions (nexus-emr-contact-primary/nexus-emr-contact-noteonRelatedPerson,nexus-emr-contact-point-phone-extensiononContactPoint, andnexus-emr-patient-record-inactive-reasononPatient). Backs the Patient Profile Care Providers and Relationships sections.
- Notes modeling: new
NexusEmrCoreFlagprofile — an administrative "sticky note" about a patient, Flag being FHIR's official home for patient notes. The Patient Profile Notes section stores its two singleton free-text fields (booking alert, patient note) as one active Flag each, discriminated by the newnexus-emr-patient-flag-typecode system onFlag.codewith the free text onFlag.code.text; the category is the officialflag-category#admin. Flags are never edited in place or deleted: a text change retires the current Flag (status = inactive, text kept) and creates a replacement, a cleared field retires without one, and readers filter to active flags — a field's history is the chain of its retired flags. No extensions.
- Must-Support / terminology gap analysis vs the Ontario / pan-Canadian stack (CA Core+, PS-CA, OPS, CA:eReC) — the dual-conformance edge obligations (meta.profile, Ontario identity, tightened coded-element bindings). Point-in-time; target specs are draft/ballot. (The analysis itself moved out of this repo in 1.15.0 — it described what this IG would need in order to claim dual conformance, which is internal planning material rather than spec guidance.)
primaryCareAttachmentextension on Patient (nexus-emr-primary-care-attachment) — the FSH for the rostering model:providers[provider, confirmedAt],status,effectivePeriod,consentStatus,terminationReason,externalIdentifier,jurisdictionalStatus, plus the attachment-status CodeSystem/ValueSet and an example. Supersedes the removed Group-based RosterPanel.
- Patient
mrnidentifier slice — Medical Record Number / chart number (identifier typeMR, HL7 v2-0203; system.../NamingSystem/patient-mrn).
- Promoted
ClinicalTaskfrom experimental to a first-class profile (trial-use), with an example and intro page. Straight promotion — all clinical-safety and MERT-compatibility constraints retained (owner/for/requester/authoredOn/descriptionrequired,intentfixed toorder, status/priority bindings, and the full MERT Must-Support set); the identifier requirement is relaxed in line with this round's IG-wide identifier relaxation (no mandated canonical Nexus system), but unlike most profiles it keeps the1..*floor — a task still needs at least one identifier, from any system.
[1.10.0] – 2026-07-23¶
Inception as a fork of the Polaris FHIR IG
(Well-Polaris/fhir-polaris-spec
@ 12f75cd8 — the dev/stardust-integration tip, i.e. Polaris 1.10.0 plus the
unreleased constraint work below). Canonical base https://fhir.apps.health
retained (WELL owns apps.health). Content inherited unchanged: 30 profiles,
18 extensions, 9 value sets, 3 code systems.
Added¶
conventions.md— PR review checklist / Definition-of-Done, seeded from Ryan Watson's Polarisconventions.md.
Changed¶
- Renamed
polaris→nexus-emrthroughout: profile names (PolarisCore*→NexusEmrCore*), canonical slugs (polaris-core-*→nexus-emr-core-*), FSH aliases, invariant ids, file names, and example instances.
- Reworked identifier/code enforcement from unevaluable slices to portable
FHIRPath invariants (the "evaluable-slicing" convention, inherited from
dev/stardust-integrationand never released in Polaris):has-raw-identifierrequired (#error) on all top-level profiles; Appointment raw-code shape invariants forcancelationReason,reasonCode,serviceCategory, andserviceType(provide-what-you-have — codings optional, butcode+displayrequired when present).
Fixed¶
- app-consent: corrected the
Consent.categorypin from the invalidconsentcategorycodes#infa(a code that does not exist) to the validv3-ActCode#INFA.
Removed¶
- Inherited Polaris contributing/governance docs and the "Ask Fira" nav entry, in favour of the lighter maintainer-review + file-a-ticket model.