Skip to content

NexusEmrCoreItemConsent Feature Overview

Introduction

The NexusEmrCoreItemConsent feature records, per patient, the consent status for a small catalogue of consent items — the things a clinic needs a yes or no on before it does them, such as contacting the patient by email, contacting them by SMS, or running an AI scribe during a visit.

Clinic staff record these decisions in a Consent section on the Patient Profile. Each item shows one standing answer, staff change it when the patient changes their mind, and the full history of every earlier answer stays available.

Item consent is a record of consent that was obtained somewhere else — a signed paper form, a verbal yes at the front desk, a scanned intake package. There is no consent portal, no patient-facing flow, and no external consent system involved: the EMR records what staff were told, in whatever way they were told it.

Item consent is one of three Consent families in this guide, and they share a store. The other two are app consent, which grants a named application access to the record, and the privacy directive, which restricts what may be done with the record at all. All three carry scope = patient-privacy, all three come back from one GET Consent?patient={id}, and all three state their decision in provision.type. The rule that reads every family, and the discriminator that tells them apart, are in Consent, Privacy and Record Integrity — read that before writing a consumer that touches more than one family.

Use Case

The Problem

A clinic that wants to email a patient, text them an appointment reminder, or turn on an AI scribe needs an answer it can act on and defend later. That requires:

  1. One answer per item - a single current status per patient per catalogue item, not a pile of consent records to reconcile
  2. A trivially readable state - the answer must be legible to any consumer that understands plain FHIR Consent, without interpreting nested rule structures
  3. Attribution - who made the decision, distinct from which account typed it in
  4. Full history - every earlier answer, when it changed, and why, for legal and regulatory review
  5. Evidence where it exists - the scanned form attached to the decision, without exposing the scan to everyone who can read consent status
  6. Room to grow - a fixed phase-1 catalogue that can be extended without a breaking change

The Solution

One FHIR R4 Consent resource per (patient, catalogue item) pair, profiled as NexusEmrCoreItemConsent and stored in the tenant's FHIR store. The resource is updated in place for the life of the pair:

  • Consent.provision.type carries the answerpermit is Provided, deny is Declined. status says only whether the record is operative.
  • Consent.category carries the catalogue item code, which is also the key that distinguishes item consent from every other kind of consent in the store.
  • FHIR resource versioning carries the history. Every change is a new version, readable through _history / vread; nothing is deleted and no separate change log is maintained.

NexusEmrCoreAppConsent is not replaced by this feature. The two profiles answer different questions and both remain in the IG:

App Consent Item Consent
Question answered May this specific application receive this patient's data? Does the clinic have consent for this catalogue item for this patient?
Keyed on The app, as a Device reference in provision.actor A catalogue code in category
Granularity One resource per patient per app One resource per patient per item
Category v3-ActCode#INFA (information access) A nexus-consent-item code (email, sms, ai-scribe, …)
Answer read from provision.type on the root provision.type on the root
Scope of effect The named app Patient-level — the umbrella under which specific grants sit

An AI-scribe example makes the split concrete. Item consent records that the patient agreed to being scribed at all; app consent records that a particular scribe product may receive their data. A clinic can hold item consent for ai-scribe and no app consent at all — nothing has been onboarded yet — and it should not hold an app consent for a scribe while item consent for ai-scribe reads Declined. Nothing in the FHIR layer enforces that second relationship; it is what the umbrella means, and clinic workflow has to honour it.

Both profiles read the same way, and this is a change

Item consent used to carry its answer in status alone — active meant Provided, rejected meant Declined — while app consent used the provision tree. The argument for it was that item consent has no actor to scope, so the provision machinery carried no information, and a status-only read is one field comparison.

That held while item consent and app consent were the only Consents in the store. It stopped holding when the privacy directive arrived, because an active directive means the patient said no. One element then meant opposite things across families sharing a resource type, a scope, and a search, and a consumer that read status first read a lockbox as permission.

So the decision moved to provision.type on the root provision, where the other families already carried it. The simplicity argument survives intact — it is still one field comparison, and it is now the same field in every family:

Read provision.type. permit grants, deny restricts. status says only whether the record is operative.

rejected is no longer written. It is an R4 lifecycle code meaning a party rejected the consent, and borrowing it to carry a patient's answer was the root of the divergence.

Nested provisions (provision.provision) are left unconstrained but unused, reserved for a later phase that needs per-purpose granularity within an item — consenting to email for appointment reminders but not for lab results, for instance. They are never how grant or refusal is expressed here. Read the decision from provision.type on the root and nothing else: a consumer that walks the provision tree looking for a nested deny finds none, and would read every declined item as Provided.

Phase 1 ships a fixed catalogue of three items, defined as concepts in the nexus-consent-item CodeSystem and collected by the nexus-consent-items ValueSet:

Code Item Covers
email Email Emailing the patient for any reason — their own health information as well as routine non-clinical mail such as appointment reminders
sms SMS Texting the patient for any reason, on the same terms as email
ai-scribe AI Scribe An AI scribe listening to, transcribing, or summarizing the visit to help generate clinical notes

The CodeSystem concept definitions are the single source of truth for the standard item descriptions. The wording staff see beside each item in the Consent section is the concept's definition, rendered from the terminology — not copy embedded in the client, and not a per-tenant string. When the description of a standard item needs to change, it changes in the CodeSystem and every surface follows.

Extensibility

The catalogue is fixed for phase 1 but the model is not closed:

  • Built-in items are concepts in the IG's nexus-consent-item CodeSystem. Adding one is an additive IG change.
  • Clinic-defined items are expected in a later phase and will be published in tenant-scoped CodeSystems rather than added to the IG one, so a clinic can record consent for something the IG has never heard of without waiting for a release.

This is why the category binding is extensible rather than required. Three consequences:

  • Do not treat the three phase-1 codes as exhaustive, and do not assume every item coding you meet comes from the IG's system. Render an unfamiliar item from the display the coding carries and leave its status alone.
  • Do not mint a clinic-specific code inside the nexus-consent-item system. That system is IG-governed.
  • The item slice is discriminated on coding.system and pinned to the IG system, so a tenant-defined item cannot arrive inside it. When tenant catalogues land, the profile gains a slice for them; nothing is smuggled into item under a foreign system.

Profile Overview

Item-specific elements

Element Cardinality Purpose
status 1..1 — active | entered-in-error Whether the record is operative. Not the answer
category[item] 1..1 — a single coding from nexus-consent-item Which catalogue item this resource is about — the key of the (patient, item) pair
category[family] 1..1 — nexus-consent-family#item (pinned) Which Consent family this is. All three families share a store, a scope and a search, so this is what separates them
patient 1..1 — Reference to NexusEmrCorePatient Whose consent this is
dateTime 1..1 — ISO 8601 timestamp When this status was recorded; re-stamped on every change
performer 0..1 — Reference to Patient, Practitioner, or PractitionerRole (with display) Who made the decision. The display source for "who" in the UI and in history
extension nexus-consent-status-reason 0..1 — valueString Optional free-text reason for the current status
source[x] 0..1 — Reference to NexusEmrCoreDocumentReference only The evidence document, when one exists. The Attachment choice is removed, so evidence cannot be inlined — see Evidence documents
provision.type 1..1 — permit | deny The answer. See the state mapping below

category itself is 1..* with open slicing and two mandatory slices: item carries the coding that identifies the catalogue item, and family carries the fixed family code item. A resource may carry further categories alongside them (a jurisdictional or workflow classification, say). Neither the item nor the family may be inferred from any other category.

Select the family in the search rather than sorting it out afterwards:

GET /Consent?patient={patient-id}&category=https://fhir.apps.health/CodeSystem/nexus-consent-family|item

Inherited FHIR R4 requirements

FHIR R4 Consent requires scope, and constraint ppc-1 requires either policy or policyRule. This profile satisfies both exactly as its app-consent sibling does: scope is fixed to consentscope#patient-privacy, and policy.uri names the governing policy. (policyRule was previously fixed to consentpolicycodes#cric — US human-subjects-research consent — and is no longer used.) Neither carries per-resource meaning, so implementers can treat them as boilerplate, and must never derive the answer from them.

State Mapping

Three states are visible to staff, plus one data-quality state. The mapping is deliberately total and unambiguous:

Consent section shows FHIR representation Notes
Provided status = active, provision.type = permit The clinic has consent for this item
Declined status = active, provision.type = deny The clinic does not have consent — covers both an initial refusal and a later revocation
Not Recorded No Consent resource exists for that (patient, item) Absence is the state — never a placeholder resource. Displayed distinctly from Declined, but equally non-permissive
(mistake) status = entered-in-error The record should never have existed. Not a decline — see below

One value for "no"

deny covers both "the patient said no" and "the patient previously said yes and has now revoked". A consumer asking may we email this patient? gets its answer from one comparison and never has to know which of the two happened. The distinction is not lost — it is exactly what the version history shows — but it is kept out of the current-state read.

status stays active through both, because the record is operative either way. The remaining R4 consent-state codes — draft, proposed, inactive and rejected — are not used by this profile. They are legal against the required binding but carry no defined meaning here, and the EMR never writes them.

Displaying Not Recorded, and gating on it

Not Recorded and Declined are different things to show and the same thing to act on:

  • In the UI they must stay distinct. A patient nobody has asked yet is a prompt to ask, not a refusal, and showing Declined for an item nobody raised misrepresents the patient.
  • For gating a behaviour they are equivalent. A consumer deciding whether it may email, text, or scribe treats a missing Consent exactly as it treats provision.type = deny: no record means no permission. Consent is opt-in, so silence is never a yes.

entered-in-error is not a decline

entered-in-error means this record should never have existed — wrong patient, wrong item, mis-click. It is a data-quality state, not a consent decision, and a consumer must not read it as "the patient said no". It means "there is no valid answer here", so it displays and gates exactly like Not Recorded: shown as unanswered, treated as no permission. Use it only for genuine mistakes; a patient who changed their mind keeps status = active and gets provision.type = deny.

Transitions

Withdrawal, modification and reinstatement are one mechanism

All three are the same act — a change of answer on the same resource, recorded as a new version — and which one a given change was is derived by comparing consecutive versions rather than labelled:

Transition Read from
Withdrawal permitdeny version n−1 against version n
Reinstatement denypermit the same comparison
Modification any change of answer the superset of the other two

There is deliberately no transition-type element. A label recording "this was a withdrawal" can disagree with the data it describes; a comparison cannot. Every version carries its own dateTime, its own performer, its own optional reason and its own optional evidence, so each step of the sequence is attributable on its own terms.

A privacy directive behaves differently on purpose: it is superseded rather than updated in place, because a directive is the evidence an auditor holds up to justify a release decision, and what did this say on the day we released the record has to be answerable without reconstructing it from history.

FHIR imposes no state machine here, and neither does this profile:

(absent)  ->  permit | deny         first recording of this item
permit    ->  deny                  revocation
deny      ->  permit                patient changes their mind again
any       ->  entered-in-error      the record was a mistake (on status)

permit and deny may alternate any number of times, with status staying active throughout. There is no legal-transition table to enforce and no monotonic progression to assume. Consumers must handle a decision that flips back and forth over a patient's history, and writers must not block a change because of the value it is leaving.

FHIR R5 note. R5 reworks this area: the grant/deny answer moves out of status into an explicit decision element (permit | deny), and rejected leaves the status value set entirely. R5 is doing to Consent what this profile does — a lifecycle element is the wrong home for a decision — and the R4 element closest to R5's decision is provision.type, so this shape is also the shorter path there.

Recording a Change

Every change to an item's status is an update in place of the single Consent resource for that (patient, item) pair. Never a second resource, never a delete.

  1. Find the resource for the pair (see Reading consent) — or skip the read entirely and use a conditional update, as the singleton invariant describes. If none exists, this is the first recording and one is created.
  2. Set provision.type to the new answer — permit for Provided, deny for Declined. Leave status at active; it is not the answer and does not change on a decision.
  3. Re-stamp dateTime to when the decision was recorded. This is a business fact about the decision, not an audit timestamp.
  4. Set performer to whoever made the decision, with a display so a reader gets a name without resolving the reference. Set it on every status change — otherwise the displayed attribution lags on whoever recorded the previous one, which is worse than showing nobody.
  5. Optionally set the reason and optionally attach evidence (both below).
  6. PUT the resource. The store keeps the previous version; the new version becomes current.

Who decided, and who typed it in

These are two different facts and they live in two different places:

Element Set by Answers
Who decided performer The client The patient themselves, or the practitioner / role who made the decision on their behalf
Who wrote it meta.extension http://services.well.com/fhir/StructureDefinition/UpdatedBy The gateway Which authenticated account submitted this version, as an IdP user id

performer is the display source — it is what the Consent section and the history view show for "who". It is client-supplied, because only the client knows whether the front desk was talking to the patient or to a substitute decision maker.

UpdatedBy is the tamper-evident backstop. The gateway stamps it from the authenticated caller's identity on every write, so a client can neither omit it nor forge it. When the two disagree — a performer naming one person and an UpdatedBy belonging to another account — that is not an error, it is the normal case of staff recording a patient's decision. What it does mean is that the audit answer comes from UpdatedBy and the clinical answer comes from performer.

Timestamps split the same way: dateTime is when the decision was recorded, meta.lastUpdated is when the version was written.

Recording a Reason

The nexus-consent-status-reason extension carries an optional free-text (valueString, 0..1) reason for the current status. In practice it matters most on a revocation — why did this patient withdraw? — but nothing restricts it to that direction.

  • Prompt softly, never require. When staff move an item to Declined, the UI may ask for a reason. It must never make the reason mandatory and must never block the save on it. A refusal that goes unrecorded because the reason field was empty is strictly worse than a refusal with no reason attached.
  • Free text, not a code. Reasons are open-ended and mostly human; a required binding here would produce "other" on most records.
  • Per version. The reason belongs to the version it was written on, so history shows the reason that applied to each earlier status.
  • Readable by anyone who can read the Consent. It is not access-gated separately, so keep it to why the decision was made. Clinical detail, third-party information, and anything that belongs behind a document permission goes in the evidence document instead.

Evidence Documents

When the clinic holds a scanned form, signed page, or other artifact behind a decision, it is attached with sourceReference pointing at a NexusEmrCoreDocumentReference. Inline attachments are prohibited: the profile restricts source[x] to that one Reference type, removing Attachment from the choice altogether, so a Consent carrying sourceAttachment fails validation rather than passing with a note.

Three reasons, in order of importance:

  1. Permission boundary. Reading the evidence requires documentreference:read, enforced at the gateway on the DocumentReference. Reading the consent requires consent:read. Referencing the evidence keeps those two decisions separate: a consumer that needs to know whether the clinic has consent gets the status and the reason, while the bytes of the signed form stay behind the document permission. An inline sourceAttachment would collapse the two — anyone with consent:read would receive the evidence itself.
  2. Every version would carry it. The resource is updated in place and the store keeps each version. An inlined 2 MB scan is re-stored in full on every subsequent status change, forever. A reference is a few dozen bytes per version.
  3. Evidence stays with the change it evidences. Each version keeps its own sourceReference. Evidence attached to an earlier decision remains attached to that version in history, and a later change made with no new evidence does not retroactively remove it. A single mutable attachment could not represent "the 2026 revocation had a signed form, the 2027 re-consent was verbal".

The evidence DocumentReference is an ordinary document in the patient's chart, subject to the same profile, storage, and permissions as any other. Nothing about consent evidence is special except what points at it.

Current status of every item

The Consent section renders from a single search — a system-only token match on category:

GET /Consent?patient={patient-id}&category=https://fhir.apps.health/CodeSystem/nexus-consent-item|

The trailing | with no code matches any code in that system, so the result is every item consent for the patient and nothing else — app consents (v3-ActCode#INFA) and any other consent in the store are excluded by construction.

Read the result as follows: each returned resource gives one item's current status, and every catalogue item absent from the result is Not Recorded. That is the only way Not Recorded is detected, which is why no placeholder resource is ever written for it.

A single item

Add the code to the token:

GET /Consent?patient={patient-id}&category=https://fhir.apps.health/CodeSystem/nexus-consent-item|email

History

GET /Consent/{id}/_history            # every version, newest first
GET /Consent/{id}/_history/{version-id}   # one specific version

Every version is a complete resource, carrying its own dateTime, status, performer, reason extension, sourceReference, and meta (including lastUpdated and the gateway-stamped UpdatedBy). The audit view is built from _history alone — there is no side table to join and no event log to correlate.

The Singleton Invariant

At most one Consent resource may exist per (patient, item) pair. Everything above depends on it: the single-element read, the absence-means-not-recorded rule, and update-in-place history all break if a pair forks into two resources.

The conditional write protects it. The pair is a patient and an item, and both are modelled elements, so a writer addresses the pair directly and creates-or-updates in one call, with no prior read:

PUT /Consent?patient={patient-id}&category=https://fhir.apps.health/CodeSystem/nexus-consent-item|ai-scribe

The server matches the existing resource and adds a version, or creates the resource when there is no match. That means two staff saving the same item at the same moment produce two versions of one resource rather than two competing resources — which a read-then-PUT-by-id sequence does not guarantee. It is the same search a reader uses to find an item consent, so a reader and a writer name the pair the same way, and there is no computed value to keep in step with either.

If a reader does encounter more than one resource for a pair, treat the one with the most recent meta.lastUpdated as current and surface the duplication as a data error. Do not merge them silently.

The retired identifier

Older records carry an identifier under nexus-item-consent-identifier, whose value composed the pair as {patient-id}:{item-code} — for example avery-linwood:ai-scribe. That namespace is retired.

The patient half was the Patient resource's logical id, which is the address a resource has on one server rather than a business identifier. Identifier is the datatype that exists to survive a move between servers; a logical id does not, because a re-ingest, a store migration and a copy between deployments each assign their own. After any such event a recorded value no longer names its pair, and the consequence is quiet in both directions: a conditional update keyed on a stale value matches nothing and creates a second resource for a pair that must have exactly one, while a writer that reads the value back as a precondition — refusing an update when it disagrees with the value it computes — stops writing for that patient instead, with no error that names the cause.

Re-basing the key on the patient's MRN was considered and does not hold: Patient.identifier's MRN slice is 0..1, so a patient without one has no key at all, and the MRN system guarantees no format, so a colon inside an MRN makes the composed value ambiguous to parse.

Read a recorded value as what it is, a key computed at write time. Do not recompute one, match on one, or use one as a precondition, and do not write new ones. Moving the records that carry one is outstanding work and is not described here.

No source/raw identifier is expected on an item consent. Profiles describing resources converted from a source EMR should still carry the source system's own identifier, so that a lossy conversion is visible -- a recommendation this IG no longer checks, since the *-has-raw-identifier invariants were removed in 1.21.0. An item consent is a different case rather than an exception to a rule: it has no upstream record to point back at, because it is created here when staff record the patient's answer. A resource carrying no identifier at all is complete.

Migrating a Store

This design puts the record of what a patient agreed to, and when, in the resource's version history rather than in a resource of its own. That is a deliberate consequence of the requirement to serve the history through _history/vread, and it makes item consent unusual to move: two ordinary migration steps that are safe for every other resource silently destroy something here. Both belong in the runbook of any migration that moves this data between stores.

Version history does not survive a bulk export/import. $export and the usual bulk pipelines carry the current state of each resource — one row per resource, not one per version. Import the result into a new store and every item consent arrives correct and every earlier decision is gone: the record that the patient consented in March and withdrew in June flattens to "declined", with no date for the consent that preceded it and no reason attached to the withdrawal. Nothing in the target store reports the loss, because the resources that survive are individually valid.

A migration must therefore carry the history explicitly. The straightforward form is a per-version replay: read each item consent's _history, then write the versions into the target in chronological order so the target rebuilds the same version chain. Preserve each version's own dateTime, performer, status-reason extension and sourceReference — they describe the change they arrived with, not the resource. Note that meta.lastUpdated and the version numbers will be the target's own; a replay reconstructs the sequence of decisions, not the original server metadata.

Re-keying patients does not break the write path, and this is why the identifier is retired. A conditional write on patient plus item is computed from the resource the writer is holding, so it keeps working across a move that re-ids patients. A key that embedded the old logical id does not: it names an id that no longer exists, the next conditional update against it matches nothing, and a second resource appears for a pair that must have exactly one. A migration should carry the retired identifiers across unchanged as the historical values they are, and key its writes on the pair.

If a migration cannot carry history, that is a decision to make deliberately and record, not a detail to discover afterwards: consent history is treated as the legal record of what the patient agreed to.

For Implementers

Go through the shared consent service and helpers rather than reading the resource yourself. The catalogue lookup, the decision-to-display mapping, the rule that absence and entered-in-error show as unanswered but gate as a no, and the conditional-update write path all live in one place there.

Every one of those rules is small enough to reimplement at a call site, which is exactly the problem: hand-rolled checks are how a UI ends up showing Declined for a record that was entered in error, or treating an unknown clinic-defined item as a refusal. When the mapping changes — the R5 decision element being the obvious candidate — a consumer on the shared helpers follows automatically.

Examples

The published examples show the shapes described above:

Example Shows
nexus-emr-item-consent-email-provided Provided — provision.type = permit on the email item, recorded by a practitioner on the patient's behalf
nexus-emr-item-consent-sms-declined Declined — provision.type = deny on the sms item, with a free-text status reason
nexus-emr-item-consent-ai-scribe-declined-evidence A previously provided consent withdrawn by the patient — reason plus sourceReference evidence
nexus-emr-consent-evidence-document The scanned withdrawal form the consent above references

The email-provided example, with only its generated narrative omitted:

{
  "resourceType": "Consent",
  "meta": {
    "profile": ["https://fhir.apps.health/StructureDefinition/nexus-emr-core-item-consent"]
  },
  "status": "active",
  "scope": {
    "coding": [
      {
        "system": "http://terminology.hl7.org/CodeSystem/consentscope",
        "code": "patient-privacy",
        "display": "Privacy Consent"
      }
    ]
  },
  "category": [
    {
      "coding": [
        {
          "system": "https://fhir.apps.health/CodeSystem/nexus-consent-family",
          "code": "item",
          "display": "Item consent"
        }
      ]
    },
    {
      "coding": [
        {
          "system": "https://fhir.apps.health/CodeSystem/nexus-consent-item",
          "code": "email",
          "display": "Email"
        }
      ]
    }
  ],
  "patient": {
    "reference": "Patient/avery-linwood",
    "display": "Avery Linwood"
  },
  "dateTime": "2025-04-08T10:35:00-06:00",
  "performer": [
    {
      "reference": "Practitioner/dr-emil-navarro",
      "display": "Dr. Emil Navarro"
    }
  ],
  "policy": [
    {
      "uri": "https://fhir.apps.health/policy/nexus-privacy-policy"
    }
  ],
  "provision": {
    "type": "permit"
  }
}

Recording a revocation flips provision.type, re-stamps dateTime, points performer at whoever decided, and — where staff captured them — adds the reason and the evidence reference. Everything else, the resource id and identifiers included, stays as it was, because this is the same resource gaining a version. From the AI-scribe withdrawal example, the elements that carry the change:

{
  "status": "active",
  "provision": { "type": "deny" },
  "dateTime": "2025-06-12T09:15:00-06:00",
  "performer": [
    {
      "reference": "Patient/avery-linwood",
      "display": "Avery Linwood"
    }
  ],
  "extension": [
    {
      "url": "https://fhir.apps.health/StructureDefinition/nexus-consent-status-reason",
      "valueString": "Patient no longer comfortable with the visit being transcribed; signed a withdrawal form at reception"
    }
  ],
  "sourceReference": {
    "reference": "DocumentReference/nexus-emr-consent-evidence-document",
    "display": "Signed AI Scribe consent withdrawal form - June 12, 2025"
  }
}

That second snippet is a fragment, not a complete resource — it omits the elements the first one shows unchanged. The published examples are complete and are the ones to copy.

Best Practices

  1. Update, never duplicate or delete. One resource per (patient, item), for the life of the pair. History is the record.
  2. Write through the conditional-update path so concurrent saves converge on one resource.
  3. Read provision.type, and only provision.type. permit grants, deny restricts. status says whether the record is operative and never what the patient decided — the same rule in every family in this store.
  4. Show Not Recorded as unanswered, gate on it as a no. Absence and entered-in-error are prompts to ask the patient, and neither is permission to act.
  5. Never block a status change on a missing reason. Recording the decision matters more than explaining it.
  6. Reference evidence; never inline it. The profile removes the Attachment choice from source[x] for the permission and storage reasons above.
  7. Render item descriptions from the CodeSystem, not from strings in the client.
  8. Tolerate unknown item codes. The catalogue is extensible and tenants may define their own.