Huli FHIR Core Implementation Guide (R4)
0.1.0 - Release

Huli FHIR Core Implementation Guide (R4) - Local Development build (v0.1.0) built by the FHIR (HL7® FHIR® Standard) Build Tools. See the Directory of published versions

Composition Mapping

Composition Mapping: Huli -> FHIR R4

HuliComposition is the clinical-note projection of a Huli encounter: it renders the encounter's SOAP narrative as a single LOINC-coded consultation note. The Public API supports read, search, create, and update for this resource.

A Composition is a sibling projection of the same ehr_encounter row the Encounter resource renders — the two are two wire views of one stored visit:

  • Encounter exposes the visit envelope (status lifecycle, class, period, participants, structured diagnoses).
  • Composition exposes the clinical narrative as section[] LOINC blocks (chief complaint, HPI, physical findings, assessment, plan, diagnosis).

Composition.id and Encounter.id are the same UUIDGET /fhir/R4/Composition/{id} and GET /fhir/R4/Encounter/{id} read the same row from two angles, and Composition.encounter always references Encounter/{that-same-id}.

Both projections derive meta.versionId from the shared row's modified_on (mirrored in the weak ETag) and run optimistic concurrency (If-Match), so a version read from either projection tracks the same underlying row.

Write-path behavior

POST / PUT /fhir/R4/Composition route through the same practice encounter service the Encounter surface uses (the shared service seam), so a Composition write inherits the Encounter guard stack rather than writing straight to the database:

  • Deceased-patient write guard — creating or amending a note against a deceased patient is rejected (409 Conflict).
  • Readiness / finalize validators — the encounter readiness and seal-the-snapshot rules run on save when the document is finalized.
  • MX consultation-start gates — for an organization/practitioner effectively active under la normativa, the consultation-start gate suite applies on create.
  • subject is immutable on update — a PUT whose subject differs from the stored patient is rejected.

Because a Composition has no visit envelope of its own, the encounter class, period, and any linked appointment are preserved from the stored row on update and defaulted on create (class = ambulatory, period.start = Composition.date when supplied, else server time).

Diagnoses are preserved, never written

The diagnosis section (29308-4) is a read-only narrative projection of the encounter's diagnoses. Diagnoses are set through the Encounter surface (Encounter.diagnosis[] with contained HuliConditions). A Composition write never touches them: the write decoder emits only the narrative JSONB keys it owns, and the encounter service's deep-merge preserves the Encounter-owned diagnoses key. So a Composition PUT that omits — or carries a stale rendering of — the diagnosis section leaves the structured diagnosis list intact.

This is the load-bearing reason the Composition projection declares its own owned-key set (reason, subjective, objective, diagnostic_impression, care_plan_narrative) and deliberately omits diagnoses: a PUT clears a narrative section the client omits, but can never wipe the diagnosis list.

Authorization

Every operation requires the SMART scope system/Composition.rs (read / search) or system/Composition.cru (create / read / update) AND the corresponding practice clinical-chart permission on the api key's user (the same permission the Encounter surface gates on). A valid scope on a key whose user lacks the permission is denied with 403.

Composition is clinical-sensitive and therefore BAA-gated: an api key without an executed Business Associate Agreement cannot obtain these scopes (it is part of the "Medical records" scope card, alongside DocumentReference). See Data handling and compliance for the integrator BAA obligations.

Resource Identity

Huli field FHIR path Cardinality Must Support Notes
id_encounter Composition.id 1..1 UUID; the same id as the underlying Encounter. Client-supplied IDs are rejected on POST
modified_on Composition.meta.lastUpdated 0..1 Set by the platform; also drives meta.versionId (see Concurrency)
modified_on Composition.meta.versionId 0..1 Yes Derived from modified_on; mirrored in the weak ETag header
Composition.meta.profile[0] 0..1 Always https://fhir.huli.ai/r4/StructureDefinition/HuliComposition

Type and Title

FHIR path Cardinality Notes
Composition.type.coding[0] 1..1 Always http://loinc.org#11488-4 (Consultation note) — every Huli Composition is a consultation note
Composition.title 0..1 Always emitted as Clinical note

Status

Composition.status is mapped from the underlying encounter status:

Huli encounter status FHIR Composition.status Notes
in-progress preliminary The visit is still open — a preliminary note
completed final The visit was finalized
cancelled entered-in-error The visit was voided

On write, the FHIR status maps back to an encounter status:

FHIR Composition.status Huli encounter status Notes
preliminary in-progress Default for an open note
final completed Finalizes the visit through the seal-the-snapshot path
amended completed Treated as a finalized visit (Huli has no distinct amend state)
entered-in-error cancelled Voids the underlying visit

Composition.status is required on write. A value outside the FHIR R4 CompositionStatus value set (preliminary | final | amended | entered-in-error) is rejected 422 with a value issue at Composition.status — the surface refuses to silently coerce an unrecognized status to a different document state.

A finalized note cannot be voided through this surface. The Huli encounter transition guard allows {planned,in-progress}→cancelled but rejects finished→cancelled, so setting an already-final Composition to entered-in-error is an illegal transition (422) — matching the model where a finalized visit cannot be cancelled. Void a preliminary note instead.

Subject and References

FHIR path Cardinality Must Support Notes
Composition.subject 1..1 Yes Reference(HuliPatient); required on read and write
Composition.author 1..* Yes Reference(HuliPractitioner); author[0] is consumed as the encounter practitioner on write — additional authors are not persisted
Composition.encounter 0..1 Reference(HuliEncounter); always the same id as Composition.id
Composition.date 1..1 The clinical date; on create it seeds the encounter period.start

References passed on create must resolve inside the authenticated organization; cross-organization references are rejected. On update, a subject or author omitted from the body falls back to the stored value.

Sections

The clinical narrative is carried as LOINC-coded section[] entries. Each section's text rides in a FHIR Narrative (section.text.div, XHTML). On read the API emits a section only when its source field is non-empty; on write it routes each section back to its field by matching the LOINC code (the section.title is advisory — routing is driven by the code).

section.code (LOINC) Section Direction
10154-3 Chief complaint read + write
10164-2 History of present illness read + write
29545-1 Physical findings read + write
51848-0 Assessment read + write
18776-5 Plan of care read + write
29308-4 Diagnosis read-only (narrative projection; see Diagnoses)

The diagnosis section is rendered as a plain narrative listing (one <icd-10-code> <display> line per diagnosis); it is not consumed on write and carries no structured section.entry references. A 29308-4 section in a write body is ignored.

Narrative encoding

Section text is plain text wrapped in an XHTML div (<div xmlns="http://www.w3.org/1999/xhtml">…</div>) and HTML-escaped, so a plain-text round-trip (read → write → read) is lossless. On write the API extracts the inner text of the supplied div, tolerating an arbitrary client-authored wrapper.

Example body

A create that sets the chief complaint, HPI, assessment, and plan:

{
  "resourceType": "Composition",
  "status": "preliminary",
  "type": { "coding": [{ "system": "http://loinc.org", "code": "11488-4", "display": "Consultation note" }] },
  "subject": { "reference": "Patient/01936b7c-8d4e-7000-8000-000000004001" },
  "author": [{ "reference": "Practitioner/01936b7c-8d4e-7000-8000-000000003001" }],
  "date": "2026-06-24T15:30:00Z",
  "section": [
    {
      "code": { "coding": [{ "system": "http://loinc.org", "code": "10154-3" }] },
      "text": { "status": "generated", "div": "<div xmlns=\"http://www.w3.org/1999/xhtml\">Cefalea de 3 días</div>" }
    },
    {
      "code": { "coding": [{ "system": "http://loinc.org", "code": "51848-0" }] },
      "text": { "status": "generated", "div": "<div xmlns=\"http://www.w3.org/1999/xhtml\">Cefalea tensional, probable origen postural</div>" }
    },
    {
      "code": { "coding": [{ "system": "http://loinc.org", "code": "18776-5" }] },
      "text": { "status": "generated", "div": "<div xmlns=\"http://www.w3.org/1999/xhtml\">Control en 1 semana. Higiene postural.</div>" }
    }
  ]
}

Optimistic concurrency (versioning)

Composition supports optimistic concurrency control so concurrent editors do not silently clobber each other:

  • Every read/write response carries Composition.meta.versionId (derived from the row's modified_on) and a weak ETag header (W/"<versionId>").
  • A conditional update — PUT /fhir/R4/Composition/{id} with an If-Match header carrying the version the client last read — is accepted only when the supplied version still matches the stored version. A stale If-Match is rejected with 409 Conflict (HPB-00103); the client should re-read and retry. The conditioned-on version is re-checked against the locked row inside the update transaction, closing the lost-update (TOCTOU) window.
  • If-Match: * means "any existing version" — it asserts only that the resource exists, and skips the version compare.
  • An unconditional PUT (no If-Match) is accepted — concurrency enforcement is opt-in by sending If-Match.

Search parameters

GET /fhir/R4/Composition reuses the Encounter keyset-cursor query (organization-scoped) and renders each matching row as a Composition:

Parameter Type Notes
_id token Direct lookup by Composition.id
patient reference Reference(HuliPatient); logical id or full reference
date date The clinical date (prefix-aware: eq/gt/ge/lt/le)
type token The Composition LOINC type. Only 11488-4 (or http://loinc.org\|11488-4) matches — every Huli Composition is a consultation note; any other type yields an empty searchset
_count number Page size
_cursor string Opaque cursor returned in Bundle.link[rel=next]

Search is active-note discovery: a voided (entered-in-error) note is excluded from search results, but stays retrievable by id (GET) and is surfaced as a tombstone in Patient/$everything — the same contract as DocumentReference search.

Search responses are returned as a FHIR Bundle of type searchset with cursor-based pagination.

No history, no delete

  • No _history / vread. The Composition exposes only its current state — there is no version-history read surface.
  • No DELETE verb. A note reaches entered-in-error (voiding the underlying visit) only via PUT status=entered-in-error, subject to the finalized-note transition guard above.

Fields not mapped to FHIR

Huli concept Reason it is not on Composition
Vital signs / lab observations Carried as discrete Observation resources (see Observation Mapping)
Prescriptions / study orders Carried as MedicationRequest / ServiceRequest resources
Custom intake forms, drawings Not modelled through FHIR in this IG
Encounter visit envelope (class, period, participants) Exposed on the sibling Encounter resource, not the note
Structured diagnoses (write) Set via Encounter.diagnosis[]; here a read-only narrative section only

Errors

Code HTTP When
HPB-00103 409 Stale If-Match (optimistic-concurrency conflict)
HPB-00117 404 Composition (encounter) not found in the authenticated organization

All errors are returned as a FHIR OperationOutcome.