Skip to content

Composition Profile

Canonical../StructureDefinition/nexus-emr-core-composition
Statusdraft · 1.26.0
BaseComposition (constraint)
SourceFSH · JSON

Nexus EMR profile for an AUTHORED clinical note -- principally the encounter note (progress note, consult note, telephone note) written by an EMR user or drafted by an AI scribe and attested by a clinician.

TypedFhir API / Pydantic schema

The typed accessor surface Composition exposes for Composition — generated from this IG's compiled profile; each typed accessor has a validating raw-FHIR twin (xRaw) where marked. The three ports are parity-enforced.

The AI-facing shape: from typed_fhir.facade.generated.schemas import CompositionSchemamodel_json_schema() is a structured-output / tool definition; instantiating validates an extraction. Generated source.

class CompositionSchema(BaseModel):
    """AI-facing shape of the Composition profile: the facade's typed accessor
    surface (curated + salient fields) as a pydantic model. `model_json_schema()` is a
    structured-output / tool schema; instantiating validates an extraction."""

    model_config = ConfigDict(extra="forbid")

    status: Literal["amended", "entered-in-error", "final", "preliminary"] | None = Field(
        None,
        description=(
            "preliminary (draft) | final (signed) | amended (edited after signing) | "
            "entered-in-error. The note's lifecycle state. This is the element that answers \"is "
            "this note a draft or is it signed?\" -- there is no extension for it and none should "
            "be created. | Note state | status | "
            "|-----------------------------|---------------------| | draft, being typed | "
            "`preliminary` | | signed | `final` | | signed, then edited | `amended` | | voided / "
            "created in error | `entered-in-error` | AMENDMENTS ARE VERSION HISTORY, NOT NEW "
            "RESOURCES. When a signed note is edited, update THIS resource in place and set "
            "`status` to `amended`. The server's version history preserves the as-signed content. "
            "Do NOT create a second Composition and relate it back -- that leaves two resources "
            "where the chart must know to display one, and every consumer must learn to filter. "
            "`relatesTo` is deliberately left unprofiled. It stays available in core R4 if a true "
            "ADDENDUM workflow later appears -- original untouched, new signed content appended, "
            "with its own author and sign-off date -- which is a genuinely different clinical act "
            "from an in-place correction. It is not the mechanism for ordinary amendment. ⚠ "
            "IMPLEMENTER WARNING -- this model depends on infrastructure this IG has not yet "
            "declared. Version history is server-local: `_history` does not travel in bundles, "
            "`$everything`, or sync pipelines, all of which move current versions only. Across a "
            "system boundary, an amended note arrives as its amended text with the as-signed "
            "content absent. Furthermore this IG currently publishes NO CapabilityStatement, so "
            "no versioning policy or supported-interaction set is declared anywhere, and "
            "`meta.versionId` is type `id` -- server-opaque in R4 and unconstrained here. "
            "Consumers MUST NOT infer ordering, authorship, or \"which version was signed\" by "
            "parsing or sorting `versionId`. Reconstructing the as-signed note is, today, only "
            "reliable on the authoring server. Declaring the version-history contract is tracked "
            "as outstanding IG work and is a prerequisite for relying on this model across "
            "boundaries. Note: If a composition is marked as withdrawn, the "
            "compositions/documents in the series, or data from the composition or document "
            "series, should never be displayed to a user without being clearly marked as "
            "untrustworthy. The flag \"entered-in-error\" is why this element is labeled as a "
            "modifier of other elements. Some reporting work flows require that the original "
            "narrative of a final document never be altered; instead, only new narrative can be "
            "added. The composition resource has no explicit status for explicitly noting whether "
            "this business rule is in effect. This would be handled by an extension if required. "
            "Composition.status — 1..1"
        ),
    )
    type: Concept | None = Field(
        None,
        description=(
            "What kind of document this is: note, summary or letter. What kind of document this "
            "is, at the coarsest useful level: an authored **note**, a generated **summary**, or "
            "a **letter**. Three codes, and this element carries exactly one of them. **This is "
            "the element to switch on, and the element to search.** It is 1..1, so it is always "
            "present, and its vocabulary is small and closed enough that a consumer can branch on "
            "it exhaustively. A surface that renders Compositions decides here whether it is "
            "showing clinician-authored content or machine-generated content -- and `author` will "
            "not answer that, because a scribe-drafted note and a generated summary both carry a "
            "Device author. `author` says who wrote it; this element says what it is. | code | "
            "what it is | |---|---| | LOINC 34109-9 \"Note\" | a clinician-authored note | | LOINC "
            "34133-9 \"Summary of episode note\" | a generated summary, of any kind | | LOINC "
            "51852-2 \"Letter\" | a letter | **The finer question goes on `category`.** Which kind "
            "of note (progress, consult, discharge, history and physical), and which kind of "
            "summary (day-sheet, interval, encounter recap), are refinements of what this element "
            "already says. They are repeating and open-ended, which is why they belong on a "
            "`0..*` element rather than this one. See `category`. **Converted data usually needs "
            "remapping here.** In OSCAR, every `casemgmt_note` arrives with LOINC 11488-4 "
            "\"Consult note\" whatever the note actually is. That is a refinement code, not a class "
            "code: a converter should write 34109-9 \"Note\" here and carry 11488-4 on `category`, "
            "rather than putting a refinement in the class slot. ⚠ IMPLEMENTER WARNING -- an "
            "`extensible` binding is not silent in TypedFhir. A `required` binding grades at "
            "ERROR; `extensible` and `preferred` both grade at WARNING; `example` is not checked. "
            "The check fires only for a code OUTSIDE the bound set, so a document typed with "
            "something other than the three codes above raises a validation warning. That is the "
            "intended signal here: the class axis is small and this set enumerates it, so a code "
            "outside it is usually a refinement code in the wrong slot. **A warning is not a "
            "rejection** -- `extensible` keeps a genuinely unclassifiable document conformant. "
            "Where this code was mapped from a source system's own vocabulary, carry the raw "
            "coding alongside the mapped one, flagged `userSelected = true`. See [Carrying the "
            "raw code](../guide/patterns/raw-codes.md). Note: For "
            "Composition type, LOINC is ubiquitous and strongly endorsed by HL7. Most "
            "implementation guides will require a specific LOINC code, or use LOINC as an "
            "extensible binding. Composition.type — 1..1. codes (extensible): 34109-9 | 34133-9 | "
            "51852-2"
        ),
    )
    category: Concept | None = Field(
        None,
        description=(
            "Refinements of the type: which kind of note, which kind of summary. The finer "
            "classification of a document, refining what `type` has already said at the class "
            "level. Repeating and optional: a document may carry several refinements, or none. "
            "**Switch on `type`, not on this element.** `type` is 1..1 and always present; "
            "`category` is 0..* and may legitimately be absent. A consumer keying off `category` "
            "alone will miss documents. What belongs here: - **which kind of note** -- LOINC "
            "11506-3 \"Progress note\", 11488-4 \"Consult note\", 18842-5 \"Discharge summary\", "
            "34117-2 \"History and physical note\" - **which kind of summary** -- the codes in "
            "`nexus-composition-type`: a day-sheet summary written ahead of a clinic, an interval "
            "summary covering the gap since the last visit, a recap of a single encounter. LOINC "
            "has no concept for any of them, and they are not interchangeable - a **repeat of the "
            "`type` code**, which is allowed: the bound value set contains all three class codes "
            "precisely so the repeat validates - administrative or workflow labels a source "
            "system carries that have no place on `type` **A refinement does not restate the "
            "class, it narrows it.** A progress note is `type` = 34109-9 \"Note\" and `category` = "
            "11506-3 \"Progress note\". A day-sheet summary is `type` = 34133-9 \"Summary of episode "
            "note\" and `category` = `nexus-daysheet-summary`. Reading `type` alone always tells a "
            "consumer which of the three populations it is holding; reading `category` tells it "
            "which kind within that population, when the producer knew. **Bound `preferred`, "
            "deliberately weaker than the binding on `type`.** LOINC's document axis is large and "
            "this guide does not enumerate it. A producer holding a more precise code than the "
            "ones listed should write it here and should not earn a warning for being more "
            "precise. **This profile and the DocumentReference profile differ on which element "
            "classifies, for a reason.** That profile directs classification to `category` rather "
            "than `type`; this one puts the class on `type`. The difference follows from the base "
            "cardinalities: `DocumentReference.type` is 0..1 and may be absent, so `category` has "
            "to carry the load there. `Composition.type` is 1..1 and is always present, so it "
            "carries it here. Note: This is a metadata field from "
            "[XDS/MHD](http://wiki.ihe.net/index.php?title=Mobile_access_to_Health_Documents_(MHD)). "
            "Composition.category — 0..*. codes (preferred): 11488-4 | 11506-3 | 18842-5 | "
            "34109-9 | 34117-2 | 34133-9 | 34748-4 | 51852-2 | …"
        ),
    )
    subject: Reference | None = Field(
        None,
        description=(
            "Patient the note is about. The patient this note documents. Required. R4 permits any "
            "subject; this IG restricts to Patient, consistent with every other clinical profile "
            "here. A note about something other than a patient has no home in this model. Note: "
            "For clinical documents, this is usually the patient. Composition.subject — 1..1"
        ),
    )
    encounter: Reference | None = Field(
        None,
        description=(
            "The encounter this note documents. The clinical encounter this note belongs to. "
            "Optional in R4 and left optional here, because notes legitimately exist outside an "
            "encounter (a telephone note taken before the visit is booked, a chart correction, a "
            "note migrated from a source EMR whose encounter linkage was never recorded). Where "
            "the note documents a visit, populate it: it is what groups notes onto the encounter "
            "in the chart and what lets the day sheet find them. Composition.encounter — 0..1"
        ),
    )
    date: FuzzyDate | None = Field(
        None,
        description=(
            "When the note was last logically changed. The composition editing time -- when the "
            "note was last logically changed. Read the name carefully: this MOVES when the note "
            "is amended. It is not the authored date and it is not the date the care happened. A "
            "timeline sorting notes by this element will re-position an amended note to the date "
            "of its amendment. Timing relationships on this profile: - `date`: when this "
            "Composition was last edited (this element) - `event.period`: when the documented "
            "CARE occurred -- the clinical date - `encounter`: the visit the note belongs to, "
            "when there is one Example. A progress note for an April 10 visit, written up the "
            "next morning and corrected a week later: - `event.period.start`: 2026-04-10 (the "
            "visit) - `date` at first save: 2026-04-11 - `date` after the correction: 2026-04-18 "
            "-- and the visit is still April 10 **Sort a clinical timeline by "
            "`event.period.start`, not by `date`.** This element is the FALLBACK arm of the "
            "profile's declared effective date, not the first one: the declaration is "
            "`event.period.start | date`. A note that populates `event` dates to its care; one "
            "that does not falls back to this element, which is a statement about resource "
            "metadata rather than about clinical time. See the note on `event` above. Note: Not "
            "the clinical date, and not the authored date. Composition.date — 1..1"
        ),
    )
    author: Reference | None = Field(
        None,
        description=(
            "Who or what wrote the note. Who is responsible for the note's content. Required and "
            "repeating: a note has at least one author, and may have several. Practitioner is the "
            "primary provider identity in this IG; PractitionerRole is permitted where the "
            "clinic/role binding is part of what is being asserted (Practitioner has no "
            "`organization` and so cannot express it). AI-DRAFTED NOTES. A Device may appear as "
            "an author alongside the human. This is one of the two mechanisms for recording that "
            "a note was scribed rather than typed; the other is a Provenance record "
            "(`NexusEmrCoreProvenance`), which carries the fuller trail -- model, inputs, "
            "supervising human. They are not exclusive and the IG does not yet rule on which "
            "consumers should read. Absent a Device author, the origin is UNKNOWN, not \"typed\". "
            "Legacy notes migrated from OSCAR Pro carry no origin information and must not be "
            "inferred to be human-typed. AN AI AGENT AUTHOR IS A LOGICAL DEVICE REFERENCE. "
            "`Reference.identifier` under the `nexus-harness-graph` NamingSystem names the "
            "harness graph that produced the text, with `Reference.type = \"Device\"`; no Device "
            "resource need exist on this server, and the granularity is the graph/feature level. "
            "This is the same mechanism `NexusEmrCoreClinicalTask` uses for `requester` and "
            "`owner`, so an AI-scribed note and the review Task carrying it name their agent "
            "identically. That is also why `author` admits **bare `Device`** alongside the two "
            "profiled Device types: a logical, identifier-only reference cannot conform to "
            "`NexusEmrCoreAppDevice` or `NexusEmrCoreEmrDevice`, because conforming to a profile "
            "presupposes a resource that exists to be validated against it. The profiled targets "
            "remain for a Device actually registered in this store; the bare target is what an AI "
            "agent uses. Note: A Practitioner, or a Device for AI-drafted content. "
            "Composition.author — 1..*"
        ),
    )
    title: str | None = Field(
        None,
        description=(
            "Human-readable label for the note. The note's label as shown in a chart list -- e.g. "
            "\"Progress Note - 8 Apr 2025\". **Mandatory in base R4, not by this profile.** R4 "
            "makes `title` 1..1 on the reasoning that CDA left it optional and no useful case for "
            "omitting it was known. A profile cannot relax a base cardinality, so this element is "
            "required here whatever a primary care EMR would otherwise choose. Much of the time "
            "there is no editorially meaningful title to write. **When there is no meaningful "
            "title, write the display name of the `type` coding, exactly.** That is the fallback "
            "R4's own guidance anticipates, and it is what downstream consumers expect to find. "
            "Do not invent a label, and do not leave a placeholder string. **Renderers may "
            "suppress a title that duplicates the type.** A consumer comparing `title` against "
            "the display of `Composition.type.coding` and finding them equal may omit the title "
            "from its presentation. The equality is the signal that no editorial title was "
            "authored. Note: For many compositions, the title is the same as the text or a "
            "display name of Composition.type (e.g. a \"consultation\" or \"progress note\"). Note "
            "that CDA does not make title mandatory, but there are no known cases where it is "
            "useful for title to be omitted, so it is mandatory here. Feedback on this "
            "requirement is welcome during the trial use period. Composition.title — 1..1"
        ),
    )
    confidentiality: Literal["L", "M", "N", "R", "U", "V"] | None = Field(
        None,
        description=(
            "Confidentiality level, where the EMR distinguishes. The code specifying the level of "
            "confidentiality of the Composition. Note: The exact use of this element, and "
            "enforcement and issues related to highly sensitive documents are out of scope for "
            "the base specification, and delegated to implementation profiles (see security "
            "section). This element is labeled as a modifier because highly confidential "
            "documents must not be treated as if they are not. Composition.confidentiality — 0..1"
        ),
    )
    attester: dict | None = Field(
        None,
        description=(
            "Who signed the note, and when. Attestation -- the medico-legal signature. REQUIRED "
            "ONCE SIGNED: a note with `status` of `final` or `amended` must carry an attester "
            "with mode `legal` or `professional` (invariant `composition-signed-has-attester`). A "
            "signed note with nobody identified as having signed it is not a record anyone can "
            "stand behind. `attester.time` is when the signature happened -- distinct from `date` "
            "(last change) and from the encounter's period (when care happened). For AI-drafted "
            "notes, the attester is the CLINICIAN, never the Device. The Device may be an author; "
            "it cannot attest. This asymmetry is the whole point of separating the two elements: "
            "generation and responsibility are different acts. Note: Only list each attester "
            "once. Composition.attester — 0..*"
        ),
    )
    custodian: Reference | None = Field(
        None,
        description=(
            "Organization responsible for maintaining the note. The organization stewarding this "
            "note. Optional here (DocumentReference requires it) because an authored note's "
            "custodian is normally the clinic operating the EMR and is implied by tenancy. Note: "
            "This is useful when documents are derived from a composition - provides guidance for "
            "how to get the latest version of the document. This is optional because this is "
            "sometimes not known by the authoring system, and can be inferred by context. "
            "However, it is important that this information be known when working with a derived "
            "document, so providing a custodian is encouraged. Composition.custodian — 0..1"
        ),
    )
    event: dict | None = Field(
        None,
        description=(
            "The care being documented -- what happened, and WHEN it happened. The clinical "
            "service(s) this note documents. `event.period` is the CLINICAL date: the time the "
            "documented care occurred, which is not the same as when the note was written, and "
            "not the same as when it was last edited. Populate it whenever the note is about care "
            "that happened at an identifiable time -- which for a progress note, a consult note "
            "or a discharge summary is always. A note whose only date is `date` cannot be placed "
            "on a clinical timeline correctly: an amendment moves it. `event.detail` may "
            "reference the thing being documented; `event.code` says what kind of care it was. "
            "Both are optional and neither substitutes for the period. Note: The event needs to "
            "be consistent with the type element, though can provide further information if "
            "desired. Composition.event — 0..*"
        ),
    )
    section: dict | None = Field(
        None,
        description=(
            "The attachment packets travelling with this document, one entry each. The document's "
            "own account of what is attached to it: a section coded LOINC `28562-7` whose `entry` "
            "references one or more attachment packets (`List` resources profiled as Attachment "
            "Packet). A letter sent with enclosures carries this section; a document sent with "
            "nothing carries no such section. **This is the only record of a document's "
            "attachments.** The packets do not point back, so a consumer asking what went out "
            "with a letter reads this section and nothing else. **Resolve it by the coding, not "
            "by position.** The body is the section at a known index; this one follows it and may "
            "sit anywhere after it. Match `system` and `code` together -- `28562-7` under another "
            "producer's system is that producer's section, not this one. **The heading is "
            "`section.title`.** Write \"Enclosures\" there. `28562-7` is named \"Chart section Set\" "
            "in LOINC and \"Enclosures\" is not a term of that concept, so it does not belong in "
            "the coding's `display`. **Membership lives on the packet, not here.** `entry` names "
            "the packets; which chart items each holds, in what order, and which of their pages "
            "are left out are all on the packet. Composition.section — 0..1"
        ),
    )
    body: str | None = Field(
        None,
        description=(
            "The whole note body, as XHTML -- every section's narrative, joined. The note text, "
            "as XHTML. **Complete on its own**: a consumer that reads nothing but this value, and "
            "no extension in this guide, must still get the clinically meaningful content of the "
            "note -- negative findings included. See the contract above `section.text`. This is "
            "the value the chart renders and the value an AI reads. It is TEXT IN FHIR -- not "
            "base64, not a blob pointer -- which is the substantive difference from the "
            "DocumentReference model this replaced for authored notes: it is greppable, diffable, "
            "and readable without a second fetch. DERIVED, and READ-ONLY. It is every section's "
            "narrative joined, so it exists nowhere in the resource and there is no way to "
            "redistribute an assigned string back across the sections it came from. To WRITE a "
            "note body, build the blocks and assign `section`. Note: Every section's narrative, "
            "in stored order, joined. A block-authored note stores one section per authoring "
            "block, so this is the whole note body rather than its first block; a note filed as "
            "SOAP sections reads the same way. A consumer needing the blocks apart -- their "
            "headings, their kinds, their structured values -- walks `section`, which reads back "
            "as a typed collection of authoring blocks, rather than reading this. Salient field — "
            "section.text.div.join(' ')"
        ),
    )