Skip to content

Questionnaire Profile

Canonical../StructureDefinition/nexus-emr-core-questionnaire
Statusdraft (experimental) · 1.26.0
BaseQuestionnaire (constraint)
SourceFSH · JSON

A Nexus EMR form definition: what it asks, what it produces, and where its answers land in the chart.

FSH

The authored source of truth: resources/StructureDefinition-nexus-emr-core-questionnaire.fsh (edit on GitHub). One FSH file may define several related artifacts.

// NexusEmrCoreQuestionnaire.fsh
// The FORM DEFINITION -- the other half of QuestionnaireResponse, and the resource that says what
// a form asks, what it produces, and where its answers land in the chart.
//
// WHY THIS EXISTS. The IG has profiled the answer set since 1.16.0 and never the form, so it could
// describe a filled-in response but not the thing that produced it. The QuestionnaireResponse
// profile says so in two places: its `questionnaire` element is "a canonical reference to a
// definition the IG does not govern -- for external instruments that is correct and permanent; for
// WELL-authored forms it is a gap", and its header lists "how questionnaire answers that are also
// clinical facts get promoted to Observation" as deliberately unsettled. This closes both.
//
// THE POSITION THIS TAKES, AND IT REVERSES THE RESPONSE PROFILE'S. `QuestionnaireResponse.item` is
// deliberately unconstrained because external instruments define their own item trees and
// constraining them would break pass-through. That reasoning holds for responses we INGEST and
// stops holding the moment we AUTHOR the form. Nexus does not accept arbitrary foreign form
// definitions into the chart: external forms are translated inward at the boundary into this
// shape. One generic format, ours.
//
// WHERE THE REQUIREMENTS COME FROM. Not from FHIR, and not from any one implementation. The
// capability set is taken from an established primary-care form model -- twenty item types, a
// seventy-value EMR-field binding vocabulary, eight scripted hooks, and note generation as a
// first-class output. It is a requirements source, not an authority: its flattening compromises
// are resolved here rather than carried over.
//
// The item-type-by-item-type mapping from that model to R4 `item.type` is NOT published. It is the
// single largest thing a form implementer still has to reconstruct, and saying so is more use than
// a pointer at a page that does not exist. `item.type` is left unconstrained here for that reason:
// the profile does not pretend to a mapping it has not written down. What would settle it is the
// inbound translation contract -- see "What is not settled" on this profile's page -- since the
// same decision governs what happens to the item types that have no R4 equivalent. The MECHANISM,
// which is the part that IS settled, is on the handbook's Templates page.
//
// THE MECHANISM IS THE TEMPLATE MECHANISM. This is the important structural fact and it is why
// this profile is short. A form and a letter template are the same thing: reusable content that
// carries CONTAINED PROTOTYPES of what it produces, and is instantiated by cloning them, filling in
// what the definition could not know, and rewriting the references between them. NexusTemplate says
// that for letters with `List`; this says it for forms with `Questionnaire`. The two are documented
// once, in the handbook's Templates page, and neither invents its own vocabulary for it.
//
// The difference is only what drives the fill. A template is filled from an instantiation context;
// a form is filled from a QuestionnaireResponse. That is precisely the split SDC draws, so the
// SDC vocabulary is adopted here natively rather than mirrored.
//
// THE CONTAINER IS SHARED TOO, and a form uses it EXCLUSIVELY as of 1.21.0. Write down how this
// landed, because the specification said three different things in two days and only the third is
// both true and ours.
//
//   1. 1.20.0 said a form carries its prototypes in a contained Bundle via `templateExtractBundle`.
//      The profile did not have the extension, so the sentence described nothing.
//   2. 1.21.0 corrected it to say a form does NOT use a Bundle -- prototypes contained
//      individually, named per item -- on the reasoning that "a repeating item produces one
//      resource per answer and none if unanswered, which a static transaction Bundle cannot
//      express". THAT REASONING IS FALSE, and it is false in the extension SDC provides for
//      exactly this: `templateExtractContext` (context `Element`, so legal on `Bundle.entry`) is a
//      FHIRPath expression naming what fills an entry, and its own definition says "if the
//      expression evaluates to no results, then this element in the templated resource will be
//      REMOVED". An entry whose context selects a repeating item is instantiated per match and
//      dropped when there are none. SDC's `extract-complex-template2` does precisely that: one
//      authored `RelatedPerson` entry, context `item.where(linkId = 'contacts')`, over a repeating
//      group.
//   3. This profile permits ONLY the Bundle. SDC ships both spellings -- and ships THE SAME
//      demonstration form authored each way, `extract-complex-template` and
//      `-template2`, identical sixteen-linkId trees over the same five output resources, which is
//      as clear a statement as a specification makes that they are one capability. We take one.
//
// WHY NARROW RATHER THAN PERMIT BOTH. Not because the item spelling is weaker; because MIXING them
// leaves a form with two prototype pools that cannot reference each other. A `#id` resolves against
// `Questionnaire.contained`. Prototypes in the Bundle are `entry.resource`, so an item's
// `templateExtract.template` -- which SDC defines as a contained reference -- cannot name one; 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 cite a fixed one
// would have no legal way to author it. One container has no seam to document.
//
// This is a NARROWING and is recorded as one: everything written against this profile stays valid
// if the item spelling is admitted later.
//
// WHAT THE BUNDLE COSTS, stated so nobody rediscovers it. `dom-2` does NOT force the Bundle here
// the way it forces it on a `Task` -- a form's prototypes are perfectly legal loose in `contained`,
// which is what 1.20.0 shipped. The Bundle buys `entry.request` (create versus update, per
// resource), `entry.fullUrl` (prototypes citing one another the way an ordinary transaction does),
// atomicity, and one shape across all three wrappers. It does not buy legality.

// EXPOSURE. `SmartExposedNoDelete(questionnaire)`: read + write, delete withheld. The grant is
// wider than a definitional artifact usually warrants, so it is stated rather than assumed.
//
// READ is what an app renders a form from. A response is only interpretable against the definition
// that was answered, so an app holding a QuestionnaireResponse and no Questionnaire has the
// answers and not the questions.
//
// WRITE is form authoring, and it is a form-authoring app that this exposes the EMR to. Say the
// consequence plainly, because it is not obvious from the resource type: this profile's thesis is
// that a form carries CONTAINED PROTOTYPES of the resources it extracts into, so `questionnaire:write`
// is an indirect path to writing the chart. A grantor should read it as such and not as the
// permission to store a document.
//
// DELETE is withheld for the reason the CapabilityStatement gives beside the same decision:
// retiring a form is a status change, and removing a definition would strand every
// QuestionnaireResponse pointing at its canonical.
//
// NOT YET MEASURED, and NexusTemplate's precedent is that it should be: the discovery query an app
// would stand on has not been run against the live NativeFHIR store the way
// `List?identifier=...|` was on 2026-08-19. Until it is, an implementer should confirm that
// `Questionnaire?url=` is served and genuinely FILTERED rather than silently dropped. That is what
// would let this paragraph say "measured, then declared" the way the template one does.

Alias: $NexusEmrCoreQuestionnaire = ../StructureDefinition/nexus-emr-core-questionnaire
Alias: $sdc-templateExtract = http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-templateExtract
Alias: $sdc-templateExtractBundle = http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-templateExtractBundle
Alias: $sdc-extractAllocateId = http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-extractAllocateId

// A Nexus-authored form is identified by its canonical, the same way every other definitional
// artifact in FHIR is. Stated as an invariant rather than a cardinality change so the message can
// say WHY: without it a response's `questionnaire` reference has nothing stable to point at, and
// the form cannot be versioned, cached or shared.
Invariant:     questionnaire-has-canonical
Description:   "A Nexus-authored form MUST carry `url` -- it is the form's identity, the target of every QuestionnaireResponse.questionnaire reference, and the only thing that makes a form versionable."
Severity:      #error
Expression:    "url.exists()"

// EXTRACTION IS BUNDLE-ONLY, and this is the resource-level statement of it. `templateExtract` --
// the per-item spelling, prototype loose in `contained` -- is refused. The two `0..0` slices below
// say so in the element table, where an implementer reads; this rule says it once, for the whole
// resource, without depending on how a validator handles the item tree's recursion.
//
// WHY BOTH, and what was MEASURED rather than assumed. `Questionnaire.item.item` is a
// `contentReference` to `#Questionnaire.item` in base R4 (hl7.fhir.r4.core#4.0.1), and the snapshot
// therefore materialises no `item.item.extension` slice for a profile to constrain -- from which it
// is natural to conclude that a `0..0` slice bites at depth one and nowhere below, which matters
// because this profile's own `item` comment records real intake forms six levels deep. THAT
// CONCLUSION IS WRONG, and it was wrong here first: a templateExtract planted on a depth-two item
// is caught by the SLICE as well, at `Questionnaire.item[1].item[0]`. The HL7 validator walks the
// contentReference and applies the profile's `item` constraints recursively. So the slice is not
// the depth-one half of a pair -- it is a second, independent lock, and the invariant's value is
// that it is stated once at the root and does not rest on that recursion behaviour being universal.
//
// The traversal below still has to include the root, and that WAS got wrong once, in the rules this
// one replaces. They read `descendants().extension...`, which never reaches `Questionnaire.extension`
// -- `descendants()` excludes the node it is called on -- so a root-level templateExtract went
// unexamined and the constraint passed vacuously. `descendants().ofType(Extension)` collects the
// extension NODES at every depth, the root's own included.
Invariant:     questionnaire-extract-is-bundle-only
Description:   "Prototypes are carried as entries of the contained transaction Bundle named by `templateExtractBundle`. The per-item `templateExtract` spelling is not used in this specification and SHALL NOT appear, at any depth of the item tree -- a prototype loose in `contained` cannot be cited by a Bundle entry, and one carried as an entry cannot be cited by an item."
Severity:      #error
Expression:    "descendants().ofType(Extension).where(url = 'http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-templateExtract').empty()"

// THE CONTAINER, and the same two rules `NexusTemplate` states for `List.entry.item`. A form's
// prototypes ride in a contained transaction Bundle exactly as a template's and a
// suggested-resource task's do. `templateExtractBundle` is how SDC names that Bundle, and its
// context is `Questionnaire` -- a root-level extension, so this expression reads `extension`
// rather than `descendants()`.
//
// A STORED Bundle is refused for the reason `NexusTemplate` refuses one: a form whose output graph
// can change under it is not a form definition, it is a pointer at somebody else's.
Invariant:     questionnaire-extract-bundle-is-contained
Description:   "`templateExtractBundle` SHALL be a local reference (#id) that resolves to a Bundle contained in this Questionnaire. The prototypes are that Bundle's entries."
Severity:      #error
Expression:    "extension.where(url = 'http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-templateExtractBundle').all(value.ofType(Reference).reference.exists() and value.ofType(Reference).reference.startsWith('#') and value.ofType(Reference).reference.substring(1) in %resource.contained.where($this is Bundle).id)"

// Word-for-word the rule on `NexusTemplate`, and deliberately so: extraction WRITES the prototypes,
// and `entry.request` -- the per-entry method that says whether an entry is a create or an update --
// is only meaningful on a transaction. Anchored on `contained` rather than on the extension target so
// a Bundle that nothing points at cannot slip through as a collection; dom-3 already forbids the
// unreferenced case, which makes this a second lock on the same door rather than a new rule.
Invariant:     questionnaire-extract-bundle-is-transaction
Description:   "A contained prototype Bundle SHALL be type `transaction`: extracting a form 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')"

Profile:        NexusEmrCoreQuestionnaire
Parent:         Questionnaire
Id:             nexus-emr-core-questionnaire
Title:          "Questionnaire Profile"
Description:    """
A Nexus EMR form definition: what it asks, what it produces, and where its answers land in the
chart.

Forms carry contained prototypes of the resources they extract into, and are instantiated by cloning
those prototypes and filling them from the `QuestionnaireResponse` -- the same idea `NexusTemplate`
uses for letter templates and a `Task` uses for a suggested chart entry.

A form declares what it produces the way every other prototype-carrying resource in this
specification does: `templateExtractBundle` names a **contained transaction Bundle**, and its entries
are the prototypes. That is the same container `NexusTemplate` names with `List.entry.item` and a
suggested-resource `Task` names with `input[suggestion]`.

**Output that depends on the answers is expressed inside the Bundle, not outside it.** Each
`Bundle.entry` carries a `templateExtractContext` expression naming the part of the response that
fills it. An entry whose context selects a repeating item is instantiated once per repeat; an entry
whose context matches nothing is removed. A repeating entry names its `fullUrl` by expression over
an `extractAllocateId` variable, because a single authored `urn:uuid` cannot distinguish iterations.

**SDC's per-item `templateExtract` is deliberately not used here**, though SDC blesses it and ships
the same demonstration form authored both ways. This is a narrowing, and it can be widened later
without invalidating anything written against this profile. The reason is that mixing the two leaves
a form with two prototype pools that cannot cite each other: a `#id` resolves against
`Questionnaire.contained`, so an item cannot name a prototype that lives as a Bundle entry, and an
entry cannot name one that lives loose in `contained`. One container removes the seam rather than
documenting it.
"""

* insert NoEffectiveDate
* insert SmartExposedNoDelete(questionnaire)
* ^url        = $NexusEmrCoreQuestionnaire
* ^status     = #draft
* ^experimental = true
* ^publisher  = "WELLSTAR Technologies Corp."
* obeys questionnaire-has-canonical and questionnaire-extract-is-bundle-only and questionnaire-extract-bundle-is-contained and questionnaire-extract-bundle-is-transaction

* . ^short      = "Nexus EMR Questionnaire"
* . ^definition = """
A form definition. Three things distinguish it from a bare R4 Questionnaire, and all three come from
what a medical form system actually has to do rather than from FHIR:

1. **It has canonical identity.** `url` is required. A form is versioned, cached and referenced by
   every response to it.
2. **It declares what it produces.** Contained prototypes plus SDC template extraction, in the
   same contained transaction Bundle `NexusTemplate` uses for letters and `Task` uses for a
   suggested chart entry. One container, three wrappers.
3. **It says where answers land.** `item.definition` names the element an answer populates and
   `item.code` names the concept, so a "do you smoke" question is not free text that happens to
   be about smoking.
"""

//---------------------------------------------------------------------
// Identity
//---------------------------------------------------------------------
* url 1..1 MS
* url ^short = "Canonical identity of this form"
* url ^comment = "Required here though 0..1 in R4. This is the target of `QuestionnaireResponse.questionnaire`; a response to a form with no canonical cannot be interpreted beyond its own display text."

* version 1..1 MS
* version ^short = "Business version of the form"
* version ^comment = "Required here though 0..1 in R4. Forms change, and a response is only interpretable against the version that was answered. Form authoring systems already version their definitions; nothing is being asked for that they do not already track."

* name MS
* name ^short = "Machine-readable name"
* title 1..1 MS
* title ^short = "The form's name, as shown in a picker"
* title ^comment = "Required for the same reason `List.title` is required on a template: a form nobody can name is a form nobody can choose."

* identifier 0..* MS
* identifier.system 1..1
* identifier.value 1..1
* identifier ^short = "Business identifier from the authoring system, where one exists"
* identifier ^comment = "Distinct from `url`. A form translated inward from an external system SHOULD carry the source system's identifier here, so the translation can be traced back. `url` is what this form IS in Nexus; `identifier` is what it was called where it came from. A source system's own identifier SHOULD be carried alongside this one, flagged `use = secondary`. See [Carrying the raw code](../guide/patterns/raw-codes.md)."

* status 1..1 MS
* status ^short = "draft | active | retired | unknown"
* status ^comment = "`retired` is how a form stops being offered. As with templates, retiring is a status change and not a delete -- responses to a retired form must stay interpretable."

* subjectType MS
* subjectType ^short = "The resource type(s) a response is about -- normally Patient"

* date MS
* publisher MS
* description MS
* useContext MS
* effectivePeriod MS

//---------------------------------------------------------------------
// The prototypes
//---------------------------------------------------------------------
* contained ^short = "Prototypes of the resources this form produces, and the transaction Bundle that may carry them -- valid base R4, NOT required to satisfy their eventual Nexus profile"
* contained ^comment = """
The same rule the Templates page states for `NexusTemplate`, and for the same reasons: **a prototype
is valid base R4; the extracted resource is valid Nexus.**

A prototype has no patient, because a form is about no one until it is answered. Where base R4 does
require a value, supply a truthful one -- the form's own date, its author -- and expect it to be
overwritten. Do NOT invent sentinel values: a placeholder like `1900-01-01` is syntactically valid,
which is exactly the problem, because if the step that was meant to replace it is missing it
survives into a real chart indistinguishable from a value someone meant.

`dom-3` and `dom-4` supply two guarantees from base FHIR rather than from rules invented here: a
stray prototype nothing points at is already invalid, and a prototype cannot carry repository
identity it has no right to.

**One place a prototype sits: as an entry of the contained transaction Bundle**, with its own
`entry.request`, `entry.fullUrl` and `templateExtractContext`. So `contained` on a conformant form
is normally a single `Bundle`. A prototype loose in `contained` is the shape 1.20.0 shipped and this
profile no longer admits -- see `questionnaire-extract-is-bundle-only` and the profile description
for why.

`dom-2` is why the graph travels as a Bundle rather than as some other container -- a contained
resource may not contain nested resources, and `Bundle` is the one exempt, because it derives from
`Resource` rather than `DomainResource`. The Templates page states that once, for all three wrappers.
"""

//---------------------------------------------------------------------
// Extraction -- the shared mechanism
//---------------------------------------------------------------------
* extension contains
    $sdc-templateExtractBundle named templateExtractBundle 0..1 MS and
    $sdc-templateExtract named templateExtract 0..0
* extension[templateExtractBundle] ^short = "The contained transaction Bundle whose entries are the prototypes this form produces"
* extension[templateExtractBundle] ^comment = "A local reference to a contained transaction Bundle whose entries are the prototypes -- the SAME container `NexusTemplate` names with `List.entry.item` and a suggested-resource `Task` names with `input[suggestion]`. Each entry carries a `templateExtractContext` expression naming the part of the response that fills it: an entry is instantiated once per match and removed when the expression finds nothing, which is how a form whose output depends on the answers is expressed without leaving the Bundle. `0..1` because a form has one output Bundle; a form producing several resources gives it several entries. Optional rather than required because a form need not write to the chart at all."
* extension[templateExtract] ^short = "NOT USED -- prototypes are Bundle entries in this specification"
* extension[templateExtract] ^comment = "Prohibited here, though SDC blesses it, and prohibited twice over: by this slice and by `questionnaire-extract-is-bundle-only`, which states the same rule once for the whole resource. Prototypes are entries of the Bundle that `templateExtractBundle` names."

* item.extension contains
    $sdc-extractAllocateId named allocateId 0..* MS and
    $sdc-templateExtract named itemTemplateExtract 0..0
* item.extension[allocateId] ^short = "A uuid allocated before extraction, for an entry whose `fullUrl` cannot be authored"
* item.extension[allocateId] ^comment = "The answer to a REPEATING entry's identity, and the one place a form needs something a template does not. A `Bundle.entry` whose `templateExtractContext` selects a repeating item is instantiated once per repeat, but `entry.fullUrl` is a single authored value -- so a repeating entry cannot carry an authored `urn:uuid` and stay distinguishable. `extractAllocateId` on the repeating item names a fresh uuid per iteration, and the entry writes its `fullUrl` as an expression over it: `templateExtractValue` carrying `%episodeId`, on `_fullUrl`. That is legal because `templateExtractValue` is declared with context `Element` and `fullUrl` is a `uri`, so it takes the primitive-extension form. NOTE that this is a READING of SDC rather than a pattern SDC demonstrates -- `extract-complex-template2` leaves one authored `urn:uuid` on an entry whose context is a repeating group, which is under-specified rather than wrong. 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."
* item.extension[itemTemplateExtract] ^short = "NOT USED -- prototypes are Bundle entries in this specification"

//---------------------------------------------------------------------
// Items
//---------------------------------------------------------------------
* item MS
* item ^short = "The questions, and where their answers go"
* item ^comment = """
Constrained here, unlike `QuestionnaireResponse.item`, and the difference is deliberate. A RESPONSE
may arrive from an external instrument whose tree we do not govern, so constraining it would make
conformant partner data non-conformant. A form DEFINITION in Nexus is one we authored or one we
translated inward, so it is held to this shape.

Item trees nest arbitrarily deep -- a real intake form in production reaches six levels -- so
consumers MUST recurse rather than index positionally.
"""

* item.linkId MS
* item.type MS
* item.text MS
* item.required MS
* item.repeats MS
* item.readOnly MS
* item.answerOption MS
* item.answerValueSet MS
* item.enableWhen MS
* item.enableBehavior MS
* item.initial MS

* item.definition MS
* item.definition ^short = "The element an answer populates -- the form-to-chart binding"
* item.definition ^comment = """
The generic replacement for a flat EMR-field enum, and the reason this profile does not mint one.

The source model names seventy EMR fields an answer can be bound to -- demographics,
problem list, allergies, medications, social history, and a granular tobacco and substance
vocabulary. Every one of them is an element of a resource we already profile, so the binding is a
path, not a code: `Patient.birthDate`, `Observation.value[x]` on a smoking-status prototype,
`Condition.code`. Expressed this way the model's flattening compromises dissolve -- a parallel `addr2*`
enum is just `Patient.address[1]`.

R4 defines this element as a reference to an ElementDefinition, so a binding may name a Nexus
profile's element and inherit that profile's constraints rather than restating them.
"""

* item.code MS
* item.code ^short = "The concept this question asks about"
* item.code ^comment = "What the answer MEANS, as distinct from where it goes. A question coded with the LOINC tobacco-smoking-status concept is recognisable as that question whichever form it appears on, which is what makes an answer promotable to a clinical fact rather than form-specific text."

//---------------------------------------------------------------------
// What is NOT settled
//---------------------------------------------------------------------
* ^purpose = """
**Settled by this profile:** identity, the contained-prototype mechanism and its four invariants,
extraction through a contained transaction Bundle via `templateExtractBundle` and through nothing
else, the form-to-chart binding via `item.definition` / `item.code`, and the alignment with
`NexusTemplate`.

**NOT settled, and deliberately not specified here:**

- **Whether SDC agrees about a repeating entry's `fullUrl`.** The mechanism is settled here -- an
  `extractAllocateId` variable and a `templateExtractValue` expression on `_fullUrl`, which the
  `allocateId` comment sets out -- and it validates. What is not settled is upstream agreement:
  SDC 4.0.0-ballot does not state what an authored `entry.fullUrl` becomes on a second iteration,
  and its own `extract-complex-template2` leaves a single `urn:uuid` on an entry whose context is a
  repeating group. Ours is the coherent reading rather than a demonstrated one. If SDC lands a
  different answer, forms written against this profile need their repeating entries revisited --
  which is the only part of this mechanism carrying that risk.
- **Approximate date and duration.** "About three years ago" is what patients actually answer, and
  R4 `date` cannot express it. A real clinical requirement with no FHIR datatype, and it needs an
  extension this profile does not yet mint.
- **Body diagrams.** Annotating an image has no FHIR item type. `attachment` carries the result but
  not the interaction.
- **Note generation.** Producing clinical text is a first-class output of a medical form system: in
  the source model the note formatter is roughly the size of the scripting engine, and sections
  carry `QUESTIONNAIRE | OBJECTIVE | ASSESSMENT | PLAN` subcategories deciding what the patient
  sees versus what reaches the note. Nothing in FHIR Questionnaire addresses this. It is the same
  ruling as structured note sections and should be decided with it.
- **Expressions.** Show/hide, formulas, prefill from prior answers, progress and validation are
  script in the source model. SDC expresses most of the same behaviours in FHIRPath, which is
  specifiable where arbitrary script is not. Adopting them is additive; this pass does not.
- **The inbound translation contract.** The ruling is that external forms are translated inward
  rather than stored as-is. What is lossy in that translation -- embedded script, diagrams -- needs
  a declared fate rather than silent dropping.
"""