Templates¶
A template is reusable content a user picks from a list and turns into real chart content. The first one is the clinical letter. The ones after it will not stop at a single resource: a letter with a follow-up task, an encounter note with a message to a colleague, a request with the work items that accompany it.
Nexus EMR models a template as a prototype resource graph: a List whose contained resources
are the ones the template will produce, stored in the shape they will have.
A form is the same thing. An intake Questionnaire is reusable content that produces
chart resources -- Observations, a note as a Composition, an update to
the Patient record -- by cloning prototypes and filling them in. So is a
suggested-resource task: when a decision-support rule or
an extraction graph offers a chart entry for someone to accept, what it offers is a prototype graph,
carried inside a Task and instantiated exactly the same way once accepted.
One idea, three wrappers, one container. In each, reusable content carries contained
prototypes of what it produces in a contained transaction Bundle, and instantiating clones them,
fills in what the definition could not know, and writes the Bundle.
What differs is the wrapper that supplies identity, and what drives the fill:
| wrapper | names the Bundle with | filled from | |
|---|---|---|---|
| Letter template | List |
entry.item |
instantiation context |
| Suggested-resource task | Task |
input[suggestion] |
already filled in |
| Form | Questionnaire |
SDC's templateExtractBundle |
a QuestionnaireResponse |
A form is the only one of the three whose output can depend on what a user answers, and that is expressed inside the Bundle rather than outside it -- see How a form's output depends on the answers below.
Everything below about prototypes, the authoring rule and sentinel values applies to all three.
Why the prototypes live in a Bundle¶
Two forces, and the first is a hard rule rather than a preference.
dom-2: a contained resource 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 contained -- and a contained List cannot hold anything at all. Bundle is the one container
exempt from the rule: it derives from Resource rather than DomainResource and has no contained
element for dom-2 to find. So the graph travels as a Bundle and keeps its prototypes wherever it
sits.
But a Bundle carries no identity. No extension, no text, no identifier, no title, no
status. A template needs a 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.
So each resource does the half it is good at. The Bundle holds the prototypes; the wrapper names the
thing. This is SDC's shape rather than an invention here -- sdc-questionnaire-templateExtractBundle
points at a contained transaction Bundle, and it ships in SDC's own examples.
How a form's output depends on the answers¶
A letter template and a suggested-resource task produce a known set of resources. A form does not: a repeating item produces one resource per answer, and none if it goes unanswered. That is expressed inside the Bundle, which is why a form needs no second mechanism.
Each Bundle.entry carries an sdc-questionnaire-templateExtractContext expression naming the part
of the response that fills it. The extension's own definition settles the empty case -- "if the
expression evaluates to no results, then this element in the templated resource will be removed" --
and SDC's extract-complex-template2 settles the repeating one: a single authored RelatedPerson
entry whose context is item.where(linkId = 'contacts'), over a repeating group.
{ "extension": [{ "url": ".../sdc-questionnaire-templateExtractContext",
"valueString": "item.where(linkId = 'hypoglycaemia')" }],
"_fullUrl": { "extension": [{ "url": ".../sdc-questionnaire-templateExtractValue",
"valueString": "%episodeId" }] },
"resource": { "resourceType": "Observation", "...": "the prototype" },
"request": { "method": "POST", "url": "Observation" } }
A repeating entry names its fullUrl by expression, because a single authored urn:uuid cannot
distinguish iterations. sdc-questionnaire-extractAllocateId on the repeating item mints a fresh
uuid per iteration, and templateExtractValue writes it into entry.fullUrl -- legal because that
extension is declared with context Element, and spelled _fullUrl because fullUrl is a uri.
A non-repeating entry needs none of this: it authors its urn:uuid and its siblings cite it, exactly
as a template's prototypes do. Worked through in the
Diabetes Annual Review
example.
This is a reading of SDC rather than a pattern SDC demonstrates. extract-complex-template2
leaves one authored urn:uuid on its repeating entry, which is under-specified rather than wrong.
The reading here is the one that makes the model coherent, and it validates; if SDC lands a different
answer, repeating entries are the part of a stored form that would need revisiting.
Why a form uses only this spelling¶
SDC ships a second one. sdc-questionnaire-templateExtract names a prototype on an item, with the
prototype sitting loose in contained and the item as its context -- and SDC ships the same
demonstration form authored both ways, extract-complex-template and extract-complex-template2,
identical sixteen-linkId trees over the same five output resources. They are one capability with
two spellings, and this specification takes one.
The reason is that mixing them leaves a form with two prototype pools that cannot cite each other.
A #id reference resolves against Questionnaire.contained. A prototype in the Bundle is
entry.resource, so an item's templateExtract.template -- which SDC defines as a contained
reference -- cannot name it; and inside a Bundle entry a #id resolves against that entry's own
contained list, so an entry cannot name a loose prototype either. A form needing an answer-driven
output to reference a fixed one would have no legal way to author the link. One container has no
seam.
This is a narrowing, and a reversible one. Admitting the item spelling later invalidates nothing written against the profile today.
Why a transaction, not a collection¶
Instantiating a template has always meant write these resources. type = transaction says that in
the model instead of in prose, and four things fall out of it:
entry.request.methodis per entry. One graph can create one resource and update another. Nothing else in this design could express that: an earlier revision put add-versus-update onTask.focus, which is0..1, so a mixed graph was unrepresentable.- Atomicity is stated, not asserted. The graph is instantiated or it is not.
ifNoneExist/ifMatchgive conditional writes per entry, which is where idempotency belongs.entry.fullUrlis how prototypes reference each other. Ordinary transaction mechanics, and it sidesteps#identirely -- which matters, because#references resolve against the root resource and would be ambiguous once a graph can sit inside aTask.
And instantiating stops being an algorithm this specification has to describe. It becomes an operation every FHIR server already implements: fill the prototypes in and POST the Bundle.
"contained": [{
"resourceType": "Bundle", "id": "bundle", "type": "transaction",
"entry": [{
"fullUrl": "urn:uuid:3d41b6c7-...-0001",
"resource": { "resourceType": "Composition", ... }, // the prototype
"request": { "method": "POST", "url": "Composition" } // what instantiating does
}]
}],
"entry": [{ "item": { "reference": "#bundle" } }]
A template's subject is a placeholder¶
List.subject is 1..1, and on a template it is a placeholder: present but naming
nobody -- it carries a population expression as an extension, with no reference and no
identifier. (A suggested-resource task has no List at all, and Task has no subject
element; its patient is Task.for, and it is already about one patient, so there is
nothing to defer.)
"subject": {
"extension": [{
"url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-templateExtractValue",
"valueString": "%patient.reference"
}]
}
This is SDC's template-extraction pattern rather than an invention here, 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. That is the same guarantee the previous subject 0..0
bought, kept while giving instantiation a declared place to write the answer.
The evaluation context¶
The expressions that fill a template's placeholders are written against named context variables
borrowed from SDC's launchContext, which is itself modelled on SMART's launch parameters, so the
same names travel from a SMART app launch through form population to template instantiation:
| name | is |
|---|---|
%patient |
the patient in context |
%user |
who is acting |
%encounter |
the encounter in context |
%location |
the location in context |
%clinical |
the resource being reviewed or manipulated |
An element whose expression yields nothing is removed from the instantiated resource, which is how an optional element stays optional.
Borrowed notation, not borrowed machinery
SDC evaluates these expressions against a QuestionnaireResponse. This specification uses the
same notation with the context above, which it defines itself. The syntax is shared; the
extraction engine is not.
stored template List
|
| clone, fill in, rewrite references
v
ordinary FHIR resources
The output is not special. A letter made from a template is the same Composition as a letter typed
from scratch, and nothing in it records that a template was involved.
The one-sentence model¶
A template is a stored List containing prototype resources. Instantiating it clones the prototypes, fills in what the template could not know, rewrites the references between them, and saves the result as ordinary resources.
The shape¶
A letter template today produces one resource, so its Bundle has one entry.
{
"resourceType": "List",
"identifier": [
{
"system": "https://fhir.apps.health/NamingSystem/nexus-template",
"value": "specialist-referral-letter"
}
],
"status": "current",
"mode": "working",
"title": "Specialist Referral Letter",
"subject": {
"extension": [
{
"url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-templateExtractValue",
"valueString": "%patient.reference"
}
],
"display": "Filled in when the template is used"
},
"date": "2026-08-18T09:00:00Z",
"contained": [
{
"resourceType": "Bundle",
"id": "bundle",
"type": "transaction",
"entry": [
{
"fullUrl": "urn:uuid:3d41b6c7-0000-4000-8000-000000000001",
"resource": {
"resourceType": "Composition",
"status": "preliminary",
"type": {
"coding": [
{ "system": "http://loinc.org", "code": "51852-2", "display": "Letter" }
]
},
"date": "2026-08-18T09:00:00Z",
"author": [{ "display": "Cedarbrook Family Health" }],
"title": "Specialist Referral Letter",
"section": [
{
"text": {
"status": "additional",
"div": "<div xmlns=\"http://www.w3.org/1999/xhtml\"><p>Dear Colleague,</p><p>...</p></div>"
}
}
]
},
"request": { "method": "POST", "url": "Composition" }
}
]
}
],
"entry": [{ "item": { "reference": "#bundle" } }]
}
Four things carry the whole model:
- The identifier marks it. Exactly one identifier under the template naming system. The value is a stable slug naming which template it is.
- The prototypes live in the contained Bundle, as
entry.resource.List.entry.itempoints at the Bundle -- one entry, always, because the graph is the Bundle. - Each Bundle entry says what instantiating does to it.
request.methodPOST to create, PUT to update;fullUrlis how prototypes reference one another. List.titleis the name. It is what a picker shows, and it is a real element rather than something borrowed or bolted on.
Why the graph is contained¶
R4's definition of a contained resource is the definition of a prototype:
These resources do not have an independent existence apart from the resource that contains them - they cannot be identified independently, and nor can they have their own independent transaction scope.
A prototype is not a letter. It is the shape a letter will have. It should not be findable by a chart search, should not have a version history of its own, and should not survive its template. Containment gives all three.
The contained thing is the Bundle, and this is the distinction the release turns on: a
prototype is a Bundle.entry.resource, not a contained resource in its own right. So the base
rules apply to the Bundle:
dom-3-- a contained resource must be referenced from its container, which is whatList.entrydoes: one entry, pointing at the Bundle.dom-4-- a contained resource must not carrymeta.versionIdormeta.lastUpdated, so the Bundle cannot claim repository identity, and nothing inside it can either.dom-2-- and this is why it is aBundleat all: a contained resource may not contain nested resources, andBundleis the one container exempt (it derives fromResource, notDomainResource, so it has nocontainedelement for the rule to find).
The authoring rule¶
A prototype is valid base R4. The instantiated resource is valid Nexus.
This is the decision the whole design rests on, so it is worth stating plainly.
A prototype has no patient, because a template is about no one until someone uses it. The obvious
way to allow that is to relax Composition.subject to 0..1 and add an invariant saying "unless it
is a template" -- which weakens a published clinical profile to serve an authoring concern, and
makes every consumer of real letters carry a rule about templates.
Base R4 already permits Composition.subject to be absent. So the prototype needs no relaxation and
the Nexus Composition profile is unchanged: it goes on saying exactly what it said about real
letters, and the prototype is simply not held to it. Conformance is checked where it matters, on the
resource that reaches the chart.
Where base R4 does require a value, supply a real one. For Composition that is status, type,
date, author and title. 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.
Never write a sentinel value
A placeholder such as 1900-01-01 or Patient/placeholder is syntactically valid, and that is
precisely the danger. If the step that was supposed to replace it is missing, it validates
cleanly, saves cleanly, and lands in a patient's chart, where nothing distinguishes it from a
value someone meant. A letter dated 1900 is a defect no validator will report.
Leave the element out where base R4 permits it. Use a truthful value where it does not.
Finding templates¶
The standard List identifier search parameter finds templates -- no custom SearchParameter, and no
new code system:
GET [base]/List?identifier=https://fhir.apps.health/NamingSystem/nexus-template|
and one template by name:
GET [base]/List?identifier=https://fhir.apps.health/NamingSystem/nexus-template|specialist-referral-letter
The identifier is identity, not classification. What a template produces is already stated by
its prototypes -- a letter template contains a Composition typed as a letter -- so restating it on
the List would create a second copy of the same fact, free to drift from the first.
A retired template stays readable by the letters made from it while dropping out of any picker:
that is List.status, and nothing extra had to be defined for it.
Using a template today¶
Instantiation is currently the client's job, and for a one-resource template it is five steps:
- Read the template List.
- Take the contained Bundle named by
entry[0].item, and the prototype in its firstentry.resource. - Fill in what the template could not know:
subject(the patient),date(now),author(the current provider). - Mint the letter's
identifierif you intend to re-instantiate it. The Composition profile allowsidentifierat 0..1 (relaxed from 1..1 in 1.19.0) withsystemandvalueboth required when present, and the prototype deliberately carries none -- a template is not an instance, and instances must not share an identity. A letter saved without one is a valid document, but it is an identity-less one: nothing distinguishes it from the next letter off the same template, so re-instantiation duplicates instead of updating. Mint one whenever the letter will be revised or reconciled, using the source-system identifier scheme the profile's ownidentifierelement documents. - POST the Bundle. For a one-resource template that is the same as saving an ordinary
Composition; for a multi-resource one it is what resolves thefullUrlreferences between the prototypes, which is why the shape is a transaction.
Merge tokens inside the narrative ({{patient.fullName}} and friends) are rendered by the letter
composer, which has the chart and the formatting rules. They are text inside XHTML, not a FHIR
mechanism, and this specification says nothing about them beyond noting that they exist and survive
into the composer unchanged.
Composition.section.text.div is XHTML, not arbitrary HTML
It must be a well-formed fragment in the XHTML namespace, and FHIR restricts which markup and which active content are permitted. An editor that emits browser-tolerant HTML will produce resources that fail validation.
Growing past one resource¶
When a template needs to produce a second resource, add a second Bundle entry. Prototypes refer
to each other by fullUrl, exactly as they would in any other transaction:
{
"fullUrl": "urn:uuid:3d41b6c7-0000-4000-8000-000000000002",
"resource": {
"resourceType": "Task",
"status": "ready",
"intent": "order",
"focus": { "reference": "urn:uuid:3d41b6c7-0000-4000-8000-000000000001" },
"description": "Send specialist referral letter"
},
"request": { "method": "POST", "url": "Task" }
}
The server resolves those urn:uuid references when the transaction is written, so the Task ends up
pointing at the real Composition. This is ordinary transaction behaviour, not something this
specification defines -- which is the main practical dividend of the Bundle: the hardest part of
instantiation, rewriting inter-resource references, is already implemented by every FHIR server.
Nothing about the stored shape changes, and nothing about this profile changes. That is the point of choosing a graph container over a per-resource marker: a template that grows from one resource to three needs no new extension, and neither does the next resource type that becomes templatable.
Identity, versioning, and why a template has neither url nor version¶
A form carries canonical identity and a business version; a template carries an identifier and a lifecycle status. That asymmetry is deliberate, and the reason is a single sentence:
A Questionnaire stays relevant to the rendering of a response. A template's result is self-contained.
A QuestionnaireResponse cannot be rendered without its Questionnaire -- the question text, the
item types and the answer options all live in the form, so a response is uninterpretable without
knowing which version it was answered against. Hence url and version are both required on
NexusEmrCoreQuestionnaire.
A letter made from a template is a complete Composition. Nothing about rendering it requires the
template to exist, let alone the revision that produced it. So a template needs identity and
lifecycle, and versioned canonical identity would buy nothing any reader could use.
This is reconsiderable, not accidental. FHIR's own version history already retrieves prior revisions of a stored template:
GET [base]/List/{id}/_history/{vid}
so the capability exists at the repository level whenever it is wanted. What is deliberately not done today is linking a produced resource back to the template revision that produced it. That linkage would make the output no longer self-contained, which is the property this whole design rests on. If a future requirement needs letter provenance -- an audit asking "which version of the template wrote this?" -- that is the decision to revisit, and it should be revisited as a decision rather than added as a field.
The obvious fix does not apply, and this is recorded so nobody re-derives it
artifact-url and artifact-version are FHIR's standard way to give canonical metadata to a
resource that is not a canonical resource, and they are active rather than draft. Neither
lists List among its contexts -- 14 and 15 contexts respectively, checked 2026-08-21. R4
List has no version element of its own either. So carrying a business version on a template
would mean minting a Nexus extension, and the paragraph above is why that is not worth doing.
One current template per identifier -- a rule the profile cannot enforce¶
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.
That matters because the documented way to find a template assumes uniqueness:
GET [base]/List?identifier=https://fhir.apps.health/NamingSystem/nexus-template|specialist-referral-letter
Author a revision without retiring the one it replaces and that query returns two current
templates with the same identifier and no way to choose between them. The picker shows the same
name twice; which one a user gets is arbitrary.
The rule: at most one List with status = current may carry a given template identifier
value. Superseding a template means setting the old one to retired in the same transaction that
creates the new one.
Why it is a rule and not an invariant: it is a constraint across resources, and a FHIRPath invariant can only see the resource it is evaluated on. Enforcing it needs a server-side check on create and update, which is a gateway concern this profile cannot answer -- the same shape as the authoring-rights question below. Until that exists it is an authoring discipline, and this paragraph is where it is written down.
More than one is a corrupt state. Fail, do not choose.
If that 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.
It must not pick the newest by date. It must not take the first result. It must not show both
and let the user decide. Every one of those is a heuristic over corrupt data, and each makes
the defect permanent and invisible -- the picker keeps working, somebody eventually
instantiates the wrong template, and nothing ever reports that two templates were claiming the
same identity. A loud failure naming the identifier value and the List ids found is the only
response that gets the data fixed.
This is a software defect when it happens, not a data-entry variation to be tolerated. The gateway check above is the prevention; this error is the detection, and both are needed -- prevention can be bypassed by a direct write, a migration or a restore, and the detector is what notices.
What is settled, and what is not¶
Settled. The container (a wrapper plus a contained transaction Bundle), the prototype graph,
template identity, the base-R4-prototype rule, and intra-template references by fullUrl. Templates
written against this today are the input to everything below.
Not settled yet, and deliberately not specified here:
- ~~A declaration of the substitutions.~~ Settled in 1.20.0, and not by inventing one. SDC's
template extraction is contained-prototypes-cloned-and-filled -- the same shape this profile
reached independently -- so Nexus converges on its vocabulary:
sdc-questionnaire-templateExtractContextsays what a prototype is filled from, andsdc-questionnaire-extractAllocateIdanswers, for a form, how prototypes that cite each other get their ids.Extended in 1.21.0. A template and a suggested-resource task no longer need
extractAllocateId: their prototypes ride in a transaction Bundle and cite one another byfullUrl, which the server resolves on write. Convergence also went one level deeper than the vocabulary -- the contained transaction Bundle is whatsdc-questionnaire-templateExtractBundlepoints at, and a form now carries that extension and only that one. One vocabulary, one container, three wrappers.A form's repeating entries answer the id question with
extractAllocateIdand atemplateExtractValueexpression on_fullUrl; what is unsettled is whether SDC agrees, since the ballot does not say what an authoredentry.fullUrlbecomes on a second iteration. See How a form's output depends on the answers.
- A server-side instantiation operation. Cloning and validation are the same work for every
template and do not belong in each client. The seam this was aiming at is now the stored shape:
the template already is a transaction Bundle, so persistence is an ordinary FHIR transaction and
reference rewriting is the server's existing
fullUrlresolution. What remains unspecified is the filling step -- an operation that takes a template and a context and returns the filled Bundle, so expansion can be tested without writing anything.
- Authoring rights for templates. A template is tenant-owned configuration, not patient data,
and a template List has no subject and so sits in no patient compartment. Which interactions the
server offers is settled -- read, search-type, create and update, no delete -- but who may author
a template, as opposed to use one, is not. Both are
list:writetoday. Separating them needs a distinct grant, which is a gateway concern this profile does not answer.
Each of those is additive. None requires a template stored today to be rewritten.
Why not the alternatives¶
A template flag on each resource type. This answers the one-resource case and answers it once
per resource type. The moment a template produces a letter and a task, the flag needs a companion
extension listing the other resources, on every type that can start a template. The List says the
same thing once.
A bare Bundle, with no wrapper. The Bundle holds the prototypes -- that part is settled, and
dom-2 leaves no alternative once a graph has to sit inside a Task. What a Bundle cannot do is
carry identity. It derives directly from Resource rather than DomainResource -- with Binary
and Parameters, one of only three that do -- so it has no extension, no text, no identifier,
no title and no status. A template needs a slug to be found by, a name to be picked by, a
lifecycle so a retired one stops being offered, and whatever stamp a deployment puts on stored
resources. Hence the wrapper, and hence List, which carries all of them natively.
Basic as the wrapper. Honest about being a house concept, and Basic.code would be a natural
marker. But it has no title and no status, so both would become extensions -- two invented
elements for things List already has -- and server support for Basic is patchy.
Discovering the graph by reverse reference. A template could be a set of independently stored
resources reassembled with _include / _revinclude. Membership would then depend on which
reference search parameters the server indexes, its traversal limits, and the direction each
relationship happens to point -- and the prototypes would be real resources sitting in the chart.
Structured Data Capture. SDC's template extraction is real precedent for prototype resources carrying expressions, and the shape here is deliberately close to it.
Updated in 1.21.0. The one objection that still stands is narrow: an SDC extraction is driven by a
QuestionnaireResponse, and there is no reason a letter template should require a Questionnaire to
exist. Everything else has converged. Nexus adopts SDC's substitution vocabulary
(templateExtractValue, templateExtractContext) and its container -- the prototypes ride in a
contained transaction Bundle exactly as templateExtractBundle does, and a form declares that
extension natively. Of SDC's two extraction spellings this specification takes one, the Bundle;
Why a form uses only this spelling gives the reason. What
remains ours is only the wrapper that supplies identity, which differs per use: a List for a named
template, a Questionnaire for a form, a Task for a suggested chart entry.
ActivityDefinition and PlanDefinition. R4's definitional resources are the closest standard
fit, and ActivityDefinition.dynamicValue (an element path plus a FHIRPath expression) is the
pattern a future substitution declaration should borrow rather than reinvent. They cannot carry this
use case, though: ActivityDefinition.kind is bound, required, to the request resource types, and
Composition is not one of them. A definitional resource in R4 cannot produce a document.