Suggested-Resource Tasks¶
A suggested-resource task is a task carrying a suggested chart entry that somebody has to accept before it becomes part of the record. Clinical extraction reads an encounter note and suggests adding type 2 diabetes to the problem list. A decision-support rule notices an A1c above target and suggests a medication. A consult letter refutes a recorded allergy, and something suggests retiring it.
There is no separate profile, and that is the design. It is an ordinary
Task, and one part of its shape is the whole definition:
A task is a suggested-resource task when it carries its suggested resources in a contained transaction Bundle.
Nothing else says so. Not Task.code, not Task.intent. Read the containment.
First: this is not the same thing as a proposal¶
Two nearby ideas share a neighbourhood, and it is worth thirty seconds to keep them apart.
A proposal is Task.intent = proposal: a task offered for a
decision rather than an instruction, which a user sees in a Suggestions list rather than a Tasks
list. That is about what is being asked.
A suggested-resource task -- this page -- is about what the task contains.
They are independent, and an authorized order carrying a suggested chart entry is a real and
ordinary shape: the entry is already agreed and the task exists to get it recorded rather than
decided. So read the containment, not Task.intent, to answer "does this carry a suggested
chart entry?".
The Proposals page has the full treatment, including the table of all four combinations, approving and declining, alternatives and decision-support cards.
suggestion is not a code anywhere in this specification -- there is no such Task.code, and no
code system defines one. (The English word still appears throughout: what went away is the term of
art that stood for two different ideas.)
The payload is a template¶
The suggested resources ride in a contained transaction Bundle -- the same prototype-graph
mechanism a reusable letter template and a form use:
| wrapper | points at the Bundle with |
|---|---|
List (a named template) |
entry.item |
Task (this page) |
input[suggestion] |
Questionnaire (a form) |
SDC's templateExtractBundle |
(A form's entries can additionally carry a templateExtractContext, so that an entry is produced
once per answer, or not at all. A Task's suggested resources are already filled in, so its entries
are simply the ones authored. See that page.)
"contained": [{
"resourceType": "Bundle", "id": "suggested", "type": "transaction",
"entry": [
{ "fullUrl": "urn:uuid:...01",
"resource": { "resourceType": "Condition", ... },
"request": { "method": "POST", "url": "Condition" } },
{ "fullUrl": "urn:uuid:...02",
"resource": { "resourceType": "MedicationRequest", ... },
"request": { "method": "PUT", "url": "MedicationRequest/abc" } }
]
}],
"input": [{ "type": { "coding": [{ "code": "suggested-resource" }] },
"valueReference": { "reference": "#suggested" } }]
One pointer, one thing. input[suggestion] is 0..1: the Bundle holds every suggested
resource, as its entries. A task that also contains its Provenance or an attachment has no
ambiguity about which resources are being suggested.
Why a Bundle and not a List¶
dom-2: a contained resource SHALL NOT contain nested resources (contained.contained.empty(),
error severity, unchanged in R5). Anything contained in a Task is forbidden from containing
anything itself, so a contained List could not hold the prototypes -- they would have to scatter as
its siblings with the List reduced to a manifest over them.
Bundle is the one container exempt, because it derives from Resource rather than
DomainResource and has no contained element for the rule to find. That is not a loophole we
invented: it is what SDC does.
What the transaction buys¶
entry.request.methodis per entry. One suggested-resource task can create aConditionand update aMedicationRequest. An earlier revision put add-versus-update onTask.focus, which is0..1, so a mixed suggestion was unrepresentable.- Atomicity is in the model.
type = transaction: instantiated, or not. ifNoneExist/ifMatchcarry conditional writes, which is where idempotency belongs.fullUrlis how prototypes reference each other -- and it sidesteps#id, which resolves against the root resource and would be ambiguous for a graph inside a Task.- Instantiating is just a transaction. Fill the prototypes in and POST the Bundle. Not an algorithm this specification defines -- an operation every FHIR server already implements.
Why containment at all¶
A suggested resource is never written to the chart as a first-class resource before someone accepts it, and
the consequence is the point: type-level queries cannot see it. Not "should not" -- cannot. A
Condition?patient= search returns asserted problems and nothing else, because a contained resource
has no independent existence to be returned. Confirmed against both stores in use.
So no consumer anywhere needs a "filter out suggestions" rule. A team that has never read this page cannot accidentally show a patient an unapproved diagnosis, because the query they wrote was never capable of returning one.
It is also what the base specification asks for. Containment is for content that "cannot be identified independently" (R4 §2.3.0.8), and a suggested resource has no independent identity because nobody has asserted it. Identity is what acceptance mints.
Add, update, retire -- told apart by request.method¶
One shape, not three, and the distinction is per entry:
| the suggestion | entry.request |
the prototype is |
|---|---|---|
| add an entry | POST to the type's endpoint |
the new resource |
| update an entry | PUT to the existing resource |
its suggested new state |
| retire an entry | PUT, as above |
its suggested new state, carrying entered-in-error |
Retiring is a kind of update, because there is no delete in this chart. Nothing is removed; an entry that should not have been made is marked entered-in-error, which its own resource type already knows how to say.
So Task.code carries no vocabulary of actions, and neither does anything else on the Task. FHIR
publishes one -- R4 binds Task.code at example strength to
approve | fulfill | abort | replace | change | suspend | resume -- and this specification
deliberately does not use it. The action is already in the Bundle, once per entry, in the element
FHIR designed to carry it.
focus does not say what a suggested-resource task writes
focus names the chart element the task is filed against, and it always points outward: never
a contained resource, never a local # reference (enforced by nexus-task-focus-not-contained).
A suggested change to a medication may well name that medication on focus, because that is what
the task is about -- but it is entry.request that makes it an update, and a reader must not
infer one from the other.
Three verbs¶
Conflating any two of these is the failure this section exists to prevent.
| verb | changes | what it does |
|---|---|---|
| approve | Task.intent proposal → order |
moves the task from the Suggestions list to the Tasks list. One-way. Writes nothing to the chart. |
| accept | Task.status → accepted |
claims the work. Where the owner was a CareTeam, owner becomes the claiming person |
| instantiate | the chart | fills the prototypes in and POSTs the Bundle. This is the one that writes. |
They are independent. A task can be approved without being instantiated -- the order still has to be
carried out. A template can be instantiated with no task anywhere near it. And instantiate is
deliberately the same verb, and the same operation, as instantiating a template a user picked from a
list.
What instantiating does¶
- Clone the prototypes named by the graph's entries.
- Fill in what the graph could not know (see the evaluation context).
- Rewrite the references between them.
- Write them to the chart -- creating, or updating whatever
focusnames. - Carry the shared harness business key onto what was written, and complete the task.
The written resources cannot basedOn the task -- most basedOn value sets exclude Task --
so traceback rides supportingInformation / Provenance plus that shared key, which is the
one value present on both sides.
Declining¶
Task.status = cancelled, with statusReason saying a human declined it. The task and its
contained graph are retained: what was offered and refused is part of the record.
There is deliberately no rejected status. cancelled covers both a proposal someone declined and
one that became moot when its alternative was approved -- which is exactly why statusReason
matters more here than anywhere else on the profile. Without it the record cannot say whether a
clinician considered this and said no.
The evaluation context¶
A template names nobody until it is used, so its List.subject is a placeholder: the element is
present and carries a population expression, with no reference and no identifier. A
suggested-resource Bundle, by contrast, is already about one patient.
The expressions are written against a set of 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 | on a suggested-resource task |
|---|---|---|
%patient |
the patient in context | Task.for |
%user |
who is acting | whoever instantiated |
%encounter |
the encounter in context | Task.encounter |
%location |
the location in context | Task.location |
%clinical |
the resource being reviewed or manipulated | Task.focus |
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.
Cards, alternatives and who raised the task¶
One decision-support evaluation may raise several proposals at once, grouped by a shared
groupIdentifier, and they are mutually exclusive alternatives only when they carry
Task.intent = option. Those are properties of the task rather than of what it contains, so
they live on the Proposals page along with what
Task.requester says about the producer.
The AI label rides the task¶
Where the producer is a machine, the task carries the
AIAST security label -- the same label every machine-produced
resource in this specification carries, whatever its type -- on the task, never on the payload. That is not a stylistic choice: per dom-5 a contained resource cannot carry
meta.security at all, which is one of the reasons the labelling lives where it does.
Review state never goes inside the payload¶
A prototype is written as the clinical fact its citations support, not as a draft.
So a suggested Condition extracted from a note documenting an established diagnosis carries
verificationStatus = confirmed. Reaching for unconfirmed to mean "not yet approved" is the
tempting mistake, and a damaging one: verificationStatus is a modifier element, so it changes
how every downstream consumer reads the condition. A consumer that honours it treats a well-evidenced
diagnosis as clinically doubtful -- and the wrong value survives into the chart on acceptance,
because instantiating touches nothing inside the prototype.
The same rule keeps a suggestion instantiable: a prototype should already satisfy the profile
its instantiated form will have, including required elements such as Condition.category. A
suggestion that cannot be accepted without a reviewer filling in missing elements is not a complete
one.
Citations travel with the suggestion¶
The Provenance is an entry in the Bundle, targeting the suggested resource by its
fullUrl. It carries the nexus-attributed-fact extension: for each statement, the verbatim
passage it rests on and the document that passage came from.
It has to be inside rather than beside, and the reason is a plain FHIR one: a urn:uuid resolves
only within its own Bundle. A Provenance sitting next to the Bundle and pointing at an entry inside
it is a dangling reference, and the IG Publisher says so.
So the citations are written too, on acceptance. That is the right outcome rather than a side effect: an accepted chart entry whose provenance was dropped is machine-produced content with no trail back to the passage it came from, which is exactly what the AI attribution rules exist to prevent. Decline instead and nothing is written -- the Bundle, Provenance included, stays contained on the retained task.
Do not file the same suggestion twice¶
A rule that runs nightly against an open care gap will file the same task every night unless
something stops it. Every task the harness writes carries an identifier under
https://fhir.apps.health/NamingSystem/nexus-harness-key, and a producer searches for it before
writing:
GET [base]/Task?identifier=https://fhir.apps.health/NamingSystem/nexus-harness-key|dm-review:Patient/4021:a1c-overdue
The value is derived from what the task is about -- the gap being addressed -- and never from the run that produced it. A key containing a run id, thread id or execution timestamp is a new key on every execution, which is precisely the duplicate it was meant to prevent. Because two patients can present the same gap, a patient-scoped key must include the subject.
The payoff: a producer re-running against a gap that no longer holds finds its own task and resolves it, rather than leaving a stale suggestion in somebody's worklist. A gap that closes on its own closes its own task.
If this ever needs to change¶
Containment is a decision, not a foreclosure. If a real consumer needs first-class suggested
resources -- CPOE, e-prescribing, an external interoperability contract -- request-pattern suggestions
can graduate to first-class intent = proposal resources additively. The task spine, the business identifiers
and the review lifecycle do not change.