Skip to content

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.method is 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 on Task.focus, which is 0..1, so a mixed graph was unrepresentable.
  • Atomicity is stated, not asserted. The graph is instantiated or it is not.
  • ifNoneExist / ifMatch give conditional writes per entry, which is where idempotency belongs.
  • entry.fullUrl is how prototypes reference each other. 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.

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:

  1. The identifier marks it. Exactly one identifier under the template naming system. The value is a stable slug naming which template it is.
  2. The prototypes live in the contained Bundle, as entry.resource. List.entry.item points at the Bundle -- one entry, always, because the graph is the Bundle.
  3. Each Bundle entry says what instantiating does to it. request.method POST to create, PUT to update; fullUrl is how prototypes reference one another.
  4. List.title is 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 what List.entry does: one entry, pointing at the Bundle.
  • dom-4 -- a contained resource must not carry meta.versionId or meta.lastUpdated, so the Bundle cannot claim repository identity, and nothing inside it can either.
  • dom-2 -- and this is why it is a Bundle at all: a contained resource may not contain nested resources, and Bundle is the one container exempt (it derives from Resource, not DomainResource, so it has no contained element 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:

  1. Read the template List.
  2. Take the contained Bundle named by entry[0].item, and the prototype in its first entry.resource.
  3. Fill in what the template could not know: subject (the patient), date (now), author (the current provider).
  4. Mint the letter's identifier if you intend to re-instantiate it. The Composition profile allows identifier at 0..1 (relaxed from 1..1 in 1.19.0) with system and value both 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 own identifier element documents.
  5. 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 the fullUrl references 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-templateExtractContext says what a prototype is filled from, and sdc-questionnaire-extractAllocateId answers, 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 by fullUrl, which the server resolves on write. Convergence also went one level deeper than the vocabulary -- the contained transaction Bundle is what sdc-questionnaire-templateExtractBundle points 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 extractAllocateId and a templateExtractValue expression on _fullUrl; what is unsettled is whether SDC agrees, since the ballot does not say what an authored entry.fullUrl becomes 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 fullUrl resolution. 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:write today. 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.