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.
Overview
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 is reusable content a user picks from a list and turns into real chart content: a clinical letter, and in time an encounter note, a request, or any of these together with the work items that accompany them.
One idea, three wrappers, one container
A template, a form and a suggested chart entry are the same idea: reusable content
carrying contained prototypes of what it produces, instantiated by cloning them and filling in
what the definition could not know. All three carry those prototypes in a contained transaction
Bundle; what differs is the wrapper that names it and what drives the fill. This profile names
it with entry.item, a Task with
input[suggestion], and a
Questionnaire with SDC's
templateExtractBundle. The mechanism is documented
once on the handbook's
Templates page.
This profile stores a template as a List that names and identifies it, carrying the resources it
produces as prototypes inside a contained transaction Bundle. Instantiating it fills the
prototypes in, resolves the references between them, and writes the Bundle -- which is an ordinary
FHIR transaction rather than an operation this specification has to define. Nothing about the output
is special: a letter made from a template is the same Composition as a letter typed from scratch.
A template that produces one resource has a one-entry Bundle. That is the ordinary shape today, not a degenerate one -- it becomes a two-resource template by gaining a Bundle entry, and no definition changes.
List.entry is exactly one item, pointing at that Bundle. The membership of the graph is the
Bundle's entries, not the List's.
Why the prototypes are contained, and why in a Bundle
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, exactly: it is not a letter, it is the shape a letter will have.
Containment supplies two guarantees from base FHIR rather than from rules invented here:
dom-3requires every contained resource to be referenced from the container. A stray contained resource that nothing points at is already invalid.dom-4forbidsmeta.versionIdandmeta.lastUpdatedon a contained resource, so a prototype cannot carry repository identity it has no right to.
And a third rule decides the shape. dom-2 says a contained resource SHALL NOT contain nested
resources (contained.contained.empty()). A prototype graph is not only used standalone -- a Task
carries one contained -- and a contained List can hold nothing at all. Bundle is the one
container exempt, because it derives from Resource rather than DomainResource and has no
contained element for the rule to find. So the graph travels as a Bundle and keeps its prototypes
wherever it sits.
A Bundle cannot carry identity, though: no extension, no text, no identifier, no title, no
status. Hence the wrapper. Each resource does the half it is good at -- the Bundle holds the
graph, the List names the template.
Why a transaction rather than a collection
Instantiating a template has always meant write these resources, and type = transaction says so
in the model instead of in prose:
entry.request.methodis per entry, so one graph can create one resource and update another.entry.request.ifNoneExist/ifMatchcarry conditional writes, which is where idempotency belongs.entry.fullUrl(aurn:uuid) is how prototypes refer to one another. A template producing a letter and a task carries aTaskwhosefocusis that letter'sfullUrl. This is ordinary transaction mechanics, and it sidesteps#id-- which resolves against the ROOT resource and would be ambiguous once a graph can sit inside a Task.
The rule that matters when authoring one
A prototype is valid base R4. The instantiated resource is valid Nexus.
A prototype has no patient, because a template is about no one until it is used. Base R4 already
allows Composition.subject to be absent, so nothing needs relaxing: the Nexus Composition profile
keeps saying exactly what it says about real letters, and the prototype is simply not held to it.
Where base R4 does require a value -- Composition.status, .type, .date, .author, .title
-- supply a real one. The honest values are the template's own: the date it was written, whoever
wrote it. They are overwritten when the template is used.
Do not invent sentinel values
A placeholder like 1900-01-01 or Patient/placeholder is syntactically valid, which is
exactly the problem: if the step that was supposed to replace it is missing, it survives into a
real chart and nothing downstream can distinguish it from a value someone meant. Leave the
element out where base R4 permits, and use a truthful value where it does not.
Identity, and how a template is found
A template carries exactly one identifier under the template naming system
(../NamingSystem/nexus-template), which is both which template it is and the
marker that it is one. List.identifier is 0..* in base R4, so a deployment may carry its own
alongside; the rule is stated as an invariant rather than a slice, because an identifier slice
discriminated by system is not evaluable by a generic validator.
The standard List search parameter therefore finds templates with no custom SearchParameter:
GET [base]/List?identifier=../NamingSystem/nexus-template|specialist-referral-letter
The identifier value is a stable slug naming which template it is. It is not a classification: what a template produces is stated by its prototypes, so there is no second copy of that fact to drift.
Why there is no url and no version
A form carries canonical identity and a business version; a template carries an identifier and a lifecycle status. The reason is one sentence:
A Questionnaire stays relevant to the rendering of a response. A template's result is self-contained.
A response cannot be rendered without its form -- question text, item types and answer options all
live there. 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
-- the usual way to give canonical metadata to a non-canonical resource -- do not list List among
their contexts. 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 of a
stored template. What is deliberately not done is linking produced output back to the revision
that made it -- that would end the self-containment this design rests on. Reconsiderable as a
decision, not a missing field.
One current template per identifier
nexus-template-has-identity requires exactly one identifier per template. It does not require
at most one current template per identifier value, and nothing else does either.
The discovery search below assumes it does. Author a revision without retiring the one it replaces,
and it returns two current templates with the same identifier and no way to choose -- the picker
shows the same name twice.
The rule: at most one List with status = current may carry a given template identifier value.
Superseding means retiring the old one in the same transaction that creates the new one.
It is a rule rather than an invariant because it constrains resources against each other, and a FHIRPath invariant only sees the resource it runs on. Enforcement needs a server-side check on create and update.
More than one is a corrupt state. Fail, do not choose.
If the discovery query returns more than one current template for a single identifier value,
the system is in a corrupt state and the consumer MUST raise an error naming the identifier
and the ids found.
Not the newest by date, not the first result, not both shown to the user. Each of those is a
heuristic over corrupt data, and each makes the defect permanent and invisible: the picker keeps
working, someone eventually instantiates the wrong template, and nothing reports that two
templates claimed one identity. It is a software defect, not a variation to tolerate.
What List gives the template for free
| Need | Element |
|---|---|
| Name shown in a picker | List.title (required here) |
| Lifecycle | List.status -- current, retired, entered-in-error |
| When it was authored | List.date |
| Membership | the contained Bundle's entry (List.entry points at the Bundle) |
| Description / authoring notes | List.note |
List.mode is fixed to working: a template's prototype set is maintained in place rather than
captured at a moment, which is what R4 means by that code.
List.subject is 1..1 and is a placeholder: present, carrying a population expression as an
extension, with no reference and no identifier, and a display saying the value arrives on use.
That is SDC's template-extraction pattern, and Reference has no required children, so an
extension-only subject is valid base R4.
A template still enters no patient compartment, because compartment membership is computed from
the reference and a placeholder has none -- the same guarantee the previous 0..0 bought, kept
while giving instantiation a declared place to write the answer.
The substitution language
This profile defines the container and the prototype graph. Which elements a consumer fills in, and the language for expressing that, was left open when this profile shipped in 1.18.0 -- the letter composer knew what a letter needed and set it itself.
As of 1.20.0 that language exists, and it is not a Nexus invention. SDC's template extraction is contained-prototypes-cloned-and-filled, the same shape this profile arrived at independently, so Nexus converges on SDC's vocabulary rather than minting a parallel one:
sdc-questionnaire-templateExtractContextdeclares what a prototype is filled FROM. Its context isElement, so it is usable here as well as on a Questionnaire.sdc-questionnaire-extractAllocateIdanswers, for a form, how a REPEATING entry gets an id: such an entry is instantiated once per repeat, so it cannot carry an authoredfullUrland stay distinguishable. A template has no repeating entries and does not need it -- its prototypes refer to one another byentry.fullUrllike any other transaction, and the server resolves those on write. (The extension could not be used here in any case -- SDC declares its context asQuestionnaire,Questionnaire.itemandElementDefinition, notElement, so a validator rejects it anywhere on aList.) What was an open question when the prototypes were loose contained resources is answered by the Bundle.
Convergence went further in 1.21.0: the prototypes now ride in a contained transaction Bundle, which
is exactly what sdc-questionnaire-templateExtractBundle points at, and the form profile carries
that extension natively. So the container is shared as well as the vocabulary, and what stays
ours is only the wrapper that supplies identity.
What Nexus still does not adopt is the dependency in the other direction.
sdc-questionnaire-templateExtract is scoped to Questionnaire and Questionnaire.item, and an SDC
extraction is driven by a QuestionnaireResponse. A letter template has no Questionnaire and must
not require one, so the wrapper stays a List.
A server-side instantiation operation remains additive: templates written against this profile today are its input. The Templates page in the handbook states what is settled and what is not, for templates and forms together.
Exposure
Declared as of 1.18.0, and measured before it was declared. List is in the server
CapabilityStatement with read, search-type, create and update -- no delete, and the profile
carries SmartExposedNoDelete(list): list:read grants read and search, list:write grants create
and update, and destroying a template is deliberately not grantable, because retiring one is
List.status = retired rather than a delete.
Discovery uses the standard List identifier search parameter, so finding every template needs no
custom SearchParameter:
GET [base]/List?identifier=../NamingSystem/nexus-template|
That query was run against the live store before this profile declared the interaction: it is served, and genuinely filtered -- a bogus identifier returns zero matches rather than an unfiltered list.