Skip to content

Template Profile

Canonical../StructureDefinition/nexus-template-list
Statusdraft · 1.26.0
BaseList (constraint)
SourceFSH · 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-3 requires every contained resource to be referenced from the container. A stray contained resource that nothing points at is already invalid.
  • dom-4 forbids meta.versionId and meta.lastUpdated on 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.method is per entry, so one graph can create one resource and update another.
  • entry.request.ifNoneExist / ifMatch carry conditional writes, which is where idempotency belongs.
  • entry.fullUrl (a urn:uuid) is how prototypes refer to one another. A template producing a letter and a task carries a Task whose focus is that letter's fullUrl. 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-templateExtractContext declares what a prototype is filled FROM. Its context is Element, so it is usable here as well as on a Questionnaire.
  • sdc-questionnaire-extractAllocateId answers, for a form, how a REPEATING entry gets an id: such an entry is instantiated once per repeat, so it cannot carry an authored fullUrl and stay distinguishable. A template has no repeating entries and does not need it -- its prototypes refer to one another by entry.fullUrl like any other transaction, and the server resolves those on write. (The extension could not be used here in any case -- SDC declares its context as Questionnaire, Questionnaire.item and ElementDefinition, not Element, so a validator rejects it anywhere on a List.) 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.