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
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:
section[] LOINC blocks
(chief complaint, HPI, physical findings, assessment, plan, diagnosis).Composition.id and Encounter.id are the same UUID — GET
/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.
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:
409 Conflict).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).
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.
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.
| 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 |
| 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 |
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}→cancelledbut rejectsfinished→cancelled, so setting an already-finalComposition toentered-in-erroris an illegal transition (422) — matching the model where a finalized visit cannot be cancelled. Void apreliminarynote instead.
| 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.
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.
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.
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>" }
}
]
}
Composition supports optimistic concurrency control so concurrent editors do
not silently clobber each other:
Composition.meta.versionId (derived from
the row's modified_on) and a weak ETag header (W/"<versionId>").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.PUT (no If-Match) is accepted — concurrency enforcement is
opt-in by sending If-Match.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.
_history / vread. The Composition exposes only its current state —
there is no version-history read surface.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.| 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 |
| 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.