Template Profile¶
| Canonical | ../StructureDefinition/nexus-template-list |
|---|---|
| Status | draft · 1.26.0 |
| Base | List (constraint) |
| Source | FSH · JSON |
A reusable template: a List that names and identifies the template, carrying the resources it
produces as prototypes inside a contained transaction Bundle.
FSH
The authored source of truth: resources/StructureDefinition-nexus-template-list.fsh (edit on GitHub). One FSH file may define several related artifacts.
// StructureDefinition-nexus-template.fsh
//
// A TEMPLATE IS A PROTOTYPE RESOURCE GRAPH, not a flag on a clinical resource.
//
// Nexus EMR needs reusable templates that a user picks from a list and turns into real chart
// content. The first of them is the clinical letter, which produces one Composition. The ones
// after it will not stop at one resource: a letter with a follow-up Task, an encounter note with
// a Communication, a request with the resources that accompany it.
//
// Marking each resource type as "this one is a template" answers only the first case, and answers
// it once per resource type -- a template extension on Composition, then another on
// MedicationRequest, then a list extension on each to name the resources created alongside it. A
// template that is a LIST OF PROTOTYPES answers all of them with one profile: membership is
// `List.entry`, and a template that grows from one resource to three changes no definitions.
//
// A one-entry template is therefore the normal shape, not a degenerate one. The letter template is
// a List with a single contained Composition, and it becomes a letter-plus-task template by gaining
// an entry.
//
// WHY THE PROTOTYPES ARE `contained`. R4 defines a contained resource as one that has no
// independent existence apart from its container, cannot be identified independently, and has no
// transaction scope of its own. That is a prototype, precisely -- it is not a letter, it is the
// shape a letter will have. Containment also buys two guarantees from base FHIR rather than from
// rules of our own: `dom-3` requires every contained resource to be referenced from the container,
// so a stray prototype nobody points at is already invalid, and `dom-4` forbids `meta.versionId` and
// `meta.lastUpdated` on one, so a prototype cannot carry repository identity it has no right to.
//
// WHY THE PROTOTYPES ARE NOT PROFILED HERE, and it is the load-bearing decision on this page.
// A prototype has no patient, because a template is about no one until it is used. Requiring it to
// satisfy the profile of the resource it will eventually become would mean relaxing that profile --
// `Composition.subject` to `0..1` for everyone, guarded by an invariant that has to know what a
// template is. That weakens a published clinical profile to serve an authoring concern.
//
// Instead: A PROTOTYPE IS VALID BASE R4, AND THE INSTANTIATED RESOURCE IS VALID NEXUS. Base R4
// already permits `Composition.subject` to be absent, so the prototype needs no relaxation and the
// Nexus Composition profile needs no change. Every Nexus profile keeps saying exactly what it said
// before about real clinical resources.
//
// The corollary, and the thing to get right when authoring a template: WHERE BASE R4 REQUIRES A
// VALUE, PUT A REAL ONE. `Composition.date`, `.author`, `.title`, `.status` and `.type` are all
// 1..1 in base R4. The honest values are the template's own -- the date it was written, whoever
// wrote it -- and they are overwritten when the template is used. Do not invent a sentinel like
// `1900-01-01`; a sentinel that is syntactically valid is one that survives into a real chart when
// the rule that was supposed to replace it is missing, and nothing downstream can tell it apart
// from a date someone meant.
//
// WHY BOTH: A `List` WRAPPING A `Bundle`, and why the prototypes are not loose in `contained`.
//
// The prototypes ride in a CONTAINED TRANSACTION BUNDLE, and the List wraps it. Two separate
// forces produced that, and neither is a preference:
//
// 1. dom-2: "If the resource is contained in another resource, it SHALL NOT contain nested
// Resources" (`contained.contained.empty()`, error severity, unchanged in R5). A prototype
// graph is not only used standalone -- a Task carries one too, CONTAINED -- and a contained
// List cannot hold anything. `Bundle` is exempt, because it derives from `Resource` rather
// than `DomainResource` and has no `contained` element for that rule to find. So the graph
// travels as a Bundle and holds its prototypes wherever it sits.
// 2. `Bundle` carries no `extension`, no `text`, no `identifier`, no `title` and no `status`. A
// template needs a stable slug to be found by, a name to be picked by, a lifecycle so a
// retired one stops being offered, and a deployment's tenancy stamp. A Bundle can carry none
// of them.
//
// Each resource does the half it is good at. The Bundle holds the prototypes; the List names the
// template, identifies it and gives it a lifecycle. `List.entry` is exactly one item and points at
// the Bundle.
//
// This is SDC's shape, not an invention here: `sdc-questionnaire-templateExtractBundle` points at a
// contained transaction Bundle, and it ships in SDC's own examples. A Task carrying a suggested
// chart entry is the same shape with a different wrapper.
//
// A FORM IS THE SAME SHAPE, and carries this same container and no other. NexusEmrCoreQuestionnaire
// declares `templateExtractBundle` and points it at a contained transaction Bundle exactly as this
// profile points `entry.item` at one; SDC's per-item `templateExtract` is prohibited there. (An
// earlier revision of this comment claimed a form COULD NOT use a Bundle, because a repeating
// item's output is answer-dependent. That is wrong: `templateExtractContext` is legal on
// `Bundle.entry`, an entry whose context finds nothing is removed, and SDC's own
// `extract-complex-template2` runs one authored entry over a repeating group.) The one thing a form
// needs that a template does not is an id for a REPEATING entry, which is what
// `extractAllocateId` supplies; a template has no item tree and so has no repeating entries.
//
// WHY A TRANSACTION BUNDLE RATHER THAN A COLLECTION. Instantiating a template has always meant
// "write these resources", and `type = transaction` says that in the model instead of in prose:
//
// * `entry.request.method` is PER ENTRY, so one graph can create one resource and update
// another. Nothing else in this design could express that -- an earlier revision put the
// add-versus-update distinction on `Task.focus`, which is 0..1, so a mixed graph was
// unrepresentable.
// * `type = transaction` states atomicity: the graph is instantiated, or it is not.
// * `entry.request.ifNoneExist` / `ifMatch` give conditional writes, which is where idempotency
// belongs.
// * `entry.fullUrl` (a `urn:uuid`) is how prototypes reference each other. That is ordinary
// transaction mechanics, and it sidesteps `#id` entirely -- which matters, because `#`
// references resolve against the ROOT resource and would be ambiguous once a graph can sit
// inside a Task.
// * Instantiating stops being an algorithm this specification has to describe and becomes an
// operation every FHIR server already implements: fill the prototypes in and POST the Bundle.
//
// WHAT THIS PROFILE DOES NOT YET DEFINE. There is no declaration here of WHICH elements a
// consumer must fill in, and no expression language for filling them. The first consumer knows
// what a letter needs (a patient, a date, an author) and sets them itself. A declaration of the
// substitutions, and a server-side operation that applies them, are the next step; both are
// additive to this profile, and templates written against it today are the input to them. See the
// Templates page in the handbook for what is settled and what is not.
//
// EXPOSURE. Declared as of 1.18.0, and the gate that held it back is satisfied the way it asked
// to be: MEASURED, then declared. The discovery query this feature stands on --
// `List?identifier=../NamingSystem/nexus-template|` -- was run against the
// live NativeFHIR store (2026-08-19): the standard `List-identifier` parameter is served and
// genuinely FILTERED (the template-NS query returned exactly the seeded template; a bogus
// identifier returned zero, not the silent-drop failure mode this store exhibits for unknown
// parameters). Exposure is `SmartExposedNoDelete`, matching the permission grant exactly
// (charting: list:read + list:write; delete withheld -- destroying a template is an
// administrative act, not an app workflow step), and `List` joins the server CapabilityStatement
// with read + search-type and the identifier search parameter.
Alias: $NexusTemplate = ../StructureDefinition/nexus-template-list
Alias: $NexusTemplateNS = ../NamingSystem/nexus-template
// Identity is one identifier under one system, and `List.identifier` is 0..* so a deployment may
// carry its own alongside. An INVARIANT rather than a slice, per this IG's evaluable-slicing
// convention: an identifier slice discriminated by system is not evaluable by a generic validator,
// and this states the same rule in portable FHIRPath.
// IDENTITY, AND WHAT THIS INVARIANT DELIBERATELY DOES NOT SAY.
//
// It requires exactly one identifier PER TEMPLATE. It does NOT require at most one CURRENT template
// per identifier VALUE, and nothing else does either -- that is a constraint across resources, and
// a FHIRPath invariant only sees the resource it runs on. Author a revision without retiring the
// one it replaces and the documented discovery search returns two `current` templates with the same
// identifier and no way to choose. The rule ("at most one current List per template identifier
// value; supersede by retiring the old one in the same transaction") needs a server-side check on
// create/update, which is a gateway concern. Stated in full on the handbook's Templates page.
//
// AND WHEN IT HAPPENS ANYWAY: more than one `current` template for one identifier value is a
// CORRUPT STATE, and a consumer MUST raise an error rather than choose between them. Not newest by
// date, not first result, not both shown to the user -- each of those is a heuristic over corrupt
// data that makes the defect permanent and invisible, because the picker keeps working and nobody
// is ever told two templates claimed one identity. It is a software defect, not a variation to
// tolerate. The gateway check is the prevention; the error is the detection, and both are needed:
// prevention can be bypassed by a direct write, a migration or a restore.
//
// NO `url` AND NO `version`, deliberately. A Questionnaire stays relevant to the RENDERING of a
// response -- question text, item types and answer options all live in the form, so a response is
// uninterpretable without knowing which version it answered. A template's result is SELF-CONTAINED:
// a letter made from a template is a complete Composition and rendering it needs no template at
// all. R4 List has no `version` element, and the standard `artifact-url`/`artifact-version`
// extensions do not list List among their contexts (checked 2026-08-21), so carrying one would mean
// minting a Nexus extension for a value no reader could use. FHIR version history
// (`GET [base]/List/{id}/_history/{vid}`) already retrieves prior revisions if they are ever wanted;
// what is NOT done is linking produced output back to the revision that made it, because that would
// end the self-containment this design rests on. Reconsiderable as a decision, not a missing field.
Invariant: nexus-template-has-identity
Description: "A template carries exactly one identifier under the Nexus template naming system -- that identifier is which template this is, and it is what the picker searches for."
Severity: #error
Expression: "identifier.where(system = '../NamingSystem/nexus-template').count() = 1"
// A template is about nobody until it is used, and `List.subject` says so by being PRESENT and
// UNRESOLVED: the element carries a population expression as an extension, with no `reference` and
// no `identifier`. That is SDC's template-extraction pattern (`templateExtractValue` holds a
// FHIRPath expression on any element, and an element whose expression yields nothing is dropped
// from the output), and `Reference` has no required children, so an extension-only subject is
// valid base R4 and satisfies `ele-1`.
//
// It also keeps the guarantee the previous `subject 0..0` bought: a template enters NO patient
// compartment, because compartment membership is computed from the reference and a placeholder has
// none. What the element adds is a declared place for instantiation to write the answer.
Invariant: nexus-template-subject-is-placeholder
Description: "List.subject SHALL be a placeholder -- present, but carrying neither `reference` nor `identifier`. A template is about nobody until it is instantiated; the value is supplied then, from the population expression the element carries."
Severity: #error
Expression: "subject.reference.empty() and subject.identifier.empty()"
// The entry names the contained Bundle, and nothing else. A template pointing at a STORED Bundle
// would be a template whose content can change under it; one pointing at a loose contained
// resource would be the pre-Bundle shape, which cannot survive being carried inside a Task.
Invariant: nexus-template-entry-is-contained-bundle
Description: "List.entry.item SHALL be a local reference (#id) that resolves to a Bundle contained in this template. The prototypes are that Bundle's entries."
Severity: #error
Expression: "entry.item.reference.exists() and entry.item.reference.startsWith('#') and entry.item.reference.substring(1) in %resource.contained.where($this is Bundle).id"
// The Bundle is a TRANSACTION, because instantiating it is a write. See the header note.
Invariant: nexus-template-bundle-is-transaction
Description: "The contained prototype Bundle SHALL be type `transaction`: instantiating a template writes its prototypes, and the per-entry request that describes each write is only meaningful on a transaction."
Severity: #error
Expression: "contained.where($this is Bundle).all(type = 'transaction')"
// WHY `nexus-template-list` AND NOT `nexus-template`: the HL7 validator (and the publisher
// convention it follows) IGNORES any file whose name ends in `-template.json` -- it reads such
// files as authoring templates, not resources. Measured directly (2026-08-19): byte-identical
// SDs, `StructureDefinition-nexus-template.json` is "ignored due to error: Unsupported format"
// while any other name loads and validates. Under the original id, every consumer running
// validator_cli against this IG's package silently lost this profile -- including our own
// conformance gate, which had been validating the template example against base R4 only.
// The id changed BEFORE first publication, so no released canonical moved. The NamingSystem
// canonical (NamingSystem/nexus-template) is a URL, not a filename, and is unaffected.
// @intro
// A named, reusable prototype graph: a List that identifies the template and carries its
// prototypes as a contained transaction Bundle, instantiated by filling the prototypes in and
// writing the Bundle.
Profile: NexusTemplate
Parent: List
Id: nexus-template-list
Title: "Template Profile"
Description: """
A reusable template: a `List` that names and identifies the template, carrying the resources it
produces as prototypes inside a **contained transaction Bundle**.
**Instantiating** it fills the prototypes in from the instantiation context, resolves the references
between them, and writes the Bundle -- an ordinary FHIR transaction rather than an operation this
specification has to define. A template producing one resource has a one-entry Bundle; one producing
several has several, referring to one another by `fullUrl`.
The same contained-Bundle mechanism carries a suggested chart entry on a `Task` and a form on a
`Questionnaire`, differing only in the wrapper that supplies identity and in what drives the fill.
"""
// A template has no effective date. It is configuration, not a clinical event, and `List.date` is
// the date the template itself was authored rather than anything that happened to a patient.
* insert NoEffectiveDate
* insert SmartExposedNoDelete(list)
* ^url = $NexusTemplate
* ^status = #draft
* insert NoResourceNoteExtension
* ^publisher = "WELLSTAR Technologies Corp."
* obeys nexus-template-has-identity and nexus-template-subject-is-placeholder and nexus-template-entry-is-contained-bundle and nexus-template-bundle-is-transaction
// --- Element Constraints ---
// The prototype Bundle. Exactly one: the graph is the Bundle, and a second one would be a second
// graph with no way to say which the template produces.
* contained 1..1
* contained only Bundle
* contained ^short = "The prototype Bundle -- a transaction whose entries are the resources this template produces"
* contained ^definition = """
A transaction Bundle whose `entry.resource` values are the PROTOTYPES: the resources this template
produces, stored in the shape they will have.
A prototype is valid BASE R4 and is NOT required to satisfy the Nexus profile its instantiated form
will satisfy. A prototype has no patient, because a template is about no one until it is used, and
requiring it to satisfy a clinical profile would mean relaxing that profile for everyone. So: a
prototype is valid base R4, and the instantiated resource is valid Nexus.
Where base R4 requires a value, put a REAL one. `Composition.date`, `.author`, `.title`, `.status`
and `.type` are all 1..1 in base R4; the honest values are the template's own, and they are
overwritten on use. Do not invent a sentinel like `1900-01-01` -- a sentinel that is syntactically
valid is one that survives into a real chart when the rule meant to replace it is missing, and
nothing downstream can tell it from a date someone meant.
Each entry carries `fullUrl` (a `urn:uuid`, which is how prototypes reference one another) and
`request` (the write that instantiating performs: `method`, `url`, and any conditional header).
"""
// Identity, and the marker. Fixing the system on the identifier means a template is findable with
// the standard `List` identifier search parameter and no custom SearchParameter:
// GET [base]/List?identifier=../NamingSystem/nexus-template|<value>
// The value is a stable slug for the template (`specialist-referral-letter`), not a
// classification. What a template PRODUCES is stated by its prototypes, so the two cannot drift.
* identifier 1..* MS
* identifier ^short = "Stable business identity of the template, and the marker that this List is one"
// `List.status` IS the template lifecycle, natively: a retired template stays readable by the
// letters that were made from it while dropping out of any picker. Nothing extra needs defining.
* status MS
* status ^short = "current (selectable) | retired (no longer offered) | entered-in-error"
// A template's prototype set is maintained in place rather than captured at a moment, which is what
// R4 means by `working`. Fixed so the element carries meaning rather than an arbitrary choice.
* mode = #working (exactly)
// The name shown in a template picker. Required: a template nobody can name is unpickable.
* title 1..1 MS
* title ^short = "The template's name, as shown in a picker"
// WHO THE TEMPLATE IS ABOUT: nobody, stated as a placeholder rather than an absence. See the
// subject invariant above for why the element is present at all.
* subject 1..1 MS
* subject only Reference(NexusEmrCorePatient)
* subject ^short = "Placeholder for the patient, carrying the expression that supplies one on use"
* subject ^definition = "Who the instantiated resources will be about. On a template this is a PLACEHOLDER: the element is present but carries neither `reference` nor `identifier`, holding instead an expression that supplies the value at instantiation (SDC's `templateExtractValue` pattern). A template is about nobody until it is used, and an element that is present-but-unresolved says that far better than an absent one, which says only that nobody thought about it."
* subject ^comment = "SHOULD carry a `display` saying the value arrives on use (\"Filled in when the template is used\"): base FHIR warns on a Reference with neither a reference nor an identifier, and a placeholder is exactly that shape, so the display is what tells a reader the emptiness is deliberate. A placeholder subject places a template in NO patient compartment, because compartment membership is computed from the reference and a placeholder has none. That is the same guarantee the previous `0..0` bought, kept while giving instantiation a declared place to write the answer."
// Exactly one entry, naming the contained Bundle. The graph's membership is the BUNDLE's entries;
// this element exists to point at the Bundle, and dom-3 requires that pointer to exist anyway.
* entry 1..1 MS
* entry ^short = "Points at the contained prototype Bundle"
* entry.item ^short = "Local reference (#id) to the contained transaction Bundle"
* . ^short = "Nexus EMR Template"