# Exporting To Fhir

> Convert OpenMed NER output (entities from openmed.analyze_text) into FHIR R4 resources — Condition, MedicationStatement, Observation — using OpenMed's built-in FHIR R4 export helpers in openmed.clinical.exporters. Covers the verified CodeableConcept builder (coding, codeable_concept, system_uri), deterministic fullUrl references, and OperationOutcome reporting. Use after running OpenMed NER when the user wants standards-conformant FHIR JSON, mentions FHIR, Condition/Observation/MedicationStatement, CodeableConcept, RxNorm/LOINC/ICD-10/SNOMED coding, or interoperability with an EHR. Pairs after extracting-clinical-entities; feeds assembling-fhir-bundles and validating-us-core.

- Skill: `maziyarpanahi/exporting-to-fhir` (Agent Skill)
- Install (CLI): `npx skillmds@latest add maziyarpanahi/exporting-to-fhir`
- Raw SKILL.md: https://api.skillmd.com/api/skills/maziyarpanahi/exporting-to-fhir/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- License: Apache-2.0
- Author: maziyarpanahi (https://skillmd.com/u/maziyarpanahi)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/maziyarpanahi/exporting-to-fhir

---


# Exporting to FHIR

OpenMed's NER (`openmed.analyze_text`) returns spans — text, label, offsets,
confidence. To make those spans interoperable you wrap each clinically relevant
span in a **FHIR R4 resource** (`Condition`, `MedicationStatement`,
`Observation`, ...) carrying a coded `CodeableConcept`. OpenMed ships the
**mechanical** R4 export helpers for this in `openmed.clinical.exporters`; you
own the small amount of clinical mapping (which span becomes which resource).

## When to use

Use this after NER, when the consumer is a FHIR system (an EHR, a registry, a
data lake on FHIR). Reach for it when the user says "export to FHIR", "make a
Condition/Observation", "build a CodeableConcept", or needs RxNorm/LOINC/ICD-10/
SNOMED-coded resources. For packaging many resources into one transaction
Bundle, hand off to `assembling-fhir-bundles`. To check the result against US
Core, hand off to `validating-us-core`.

## What OpenMed gives you (verified API)

OpenMed deliberately ships the *purely mechanical* pieces and leaves clinical
judgement to you. The verified entry points:

```python
# CodeableConcept builder — openmed/clinical/exporters/codeable_concept_simple.py
from openmed.clinical.exporters.codeable_concept_simple import (
    system_uri,        # vocab id -> canonical HL7 system URI
    coding,            # (system, code, display) -> Coding dict
    codeable_concept,  # [Coding, ...] -> CodeableConcept dict (deterministic order)
)

# Bundle + reference + OperationOutcome — openmed/clinical/exporters/fhir/
from openmed.clinical.exporters.fhir import (
    to_bundle,                 # [resource, ...] -> R4 transaction Bundle
    deterministic_fullurl,     # (doc_id, index) -> stable urn:uuid
    OperationOutcomeIssue,     # issue dataclass
    to_operation_outcome,      # [issue, ...] -> OperationOutcome
    from_validation_result,    # validator result -> OperationOutcome
)
```

`system_uri` knows these vocabularies out of the box: `rxnorm`, `icd-10-cm`,
`loinc`, `snomed`, `hpo`, `mesh` (and passes through any `http(s)://` URI
unchanged). It is the single source of truth for vocab-id → system-URI mapping.

There is **no** `to_condition()` / `to_observation()` magic function. You build
the resource shell (a small dict) and drop a `codeable_concept(...)` into its
coded slot. That is by design: the resource *type* and *clinical status* are
decisions OpenMed will not make for you.

## Quick start: entity → Condition

```python
import openmed
from openmed.clinical.exporters.codeable_concept_simple import coding, codeable_concept

# 1) NER (synthetic note — no real PHI)
result = openmed.analyze_text(
    "Assessment: type 2 diabetes mellitus, stable.",
    model_name="disease_detection_superclinical",
)
# result.entities -> EntityPrediction(text, label, confidence, start, end)
span = result.entities[0]            # e.g. text="type 2 diabetes mellitus"

# 2) Ground to a code OUT OF PROCESS (your terminology server / mapping table).
#    OpenMed bundles no restricted vocab — see querying-terminology-service.
icd_code, snomed_code = "E11.9", "44054006"

# 3) Build the CodeableConcept with OpenMed's builder
cc = codeable_concept(
    [
        coding("snomed", snomed_code, "Diabetes mellitus type 2"),
        coding("icd-10-cm", icd_code, "Type 2 diabetes mellitus without complications"),
    ],
    text=span.text,
)

# 4) Assemble the resource shell yourself
condition = {
    "resourceType": "Condition",
    "id": "cond-1",
    "clinicalStatus": {
        "coding": [{
            "system": "http://terminology.hl7.org/CodeSystem/condition-clinical",
            "code": "active",
        }]
    },
    "verificationStatus": {
        "coding": [{
            "system": "http://terminology.hl7.org/CodeSystem/condition-ver-status",
            "code": "confirmed",
        }]
    },
    "category": [{
        "coding": [{
            "system": "http://terminology.hl7.org/CodeSystem/condition-category",
            "code": "encounter-diagnosis",
        }]
    }],
    "code": cc,                                  # OpenMed-built CodeableConcept
    "subject": {"reference": "Patient/patient-1"},
    "recordedDate": "2024-03-02",
}
```

`codeable_concept` sorts codings deterministically (SNOMED, LOINC, RxNorm,
ICD-10-CM, HPO, MeSH first; everything else alphabetical), so the JSON is
byte-stable across runs — important for diffable pipelines and golden tests.

## Worked: the resulting `Condition.code`

```json
{
  "code": {
    "coding": [
      { "system": "http://snomed.info/sct", "code": "44054006",
        "display": "Diabetes mellitus type 2" },
      { "system": "http://hl7.org/fhir/sid/icd-10-cm", "code": "E11.9",
        "display": "Type 2 diabetes mellitus without complications" }
    ],
    "text": "type 2 diabetes mellitus"
  }
}
```

## Workflow

1. **NER** — `openmed.analyze_text(note, model_name=...)` → `result.entities`.
2. **Classify** each span: a disease label → `Condition`; a drug → `MedicationStatement`;
   a lab/vital/measurement → `Observation`.
3. **Ground** the surface text to a code out of process (terminology server or
   your own map). Never invent codes; if you cannot ground a span, emit a
   `CodeableConcept` with only `text` and no `coding`.
4. **Build** the `CodeableConcept` with `coding(...)` + `codeable_concept(...)`.
5. **Wrap** it in the resource shell (set `clinicalStatus`/`status`, `subject`,
   dates). Use the cheat-sheet below.
6. **Reference** the Patient/Encounter via `{"reference": "Patient/<id>"}`.
7. Pass the list to `to_bundle(...)` (`assembling-fhir-bundles`) and validate
   (`validating-us-core`).

### Resource cheat-sheet (where the CodeableConcept goes)

| OpenMed entity kind | FHIR resource | Coded slot | Required status field |
| --- | --- | --- | --- |
| Disease / diagnosis | `Condition` | `code` | `clinicalStatus`, `verificationStatus` |
| Drug / medication | `MedicationStatement` | `medicationCodeableConcept` | `status` (e.g. `active`) |
| Lab / vital / finding | `Observation` | `code` (+ `valueQuantity`/`valueCodeableConcept`) | `status` (e.g. `final`) |
| Procedure | `Procedure` | `code` | `status` |
| Allergy | `AllergyIntolerance` | `code` | `clinicalStatus` |

### MedicationStatement (drug span)

```python
med = {
    "resourceType": "MedicationStatement",
    "id": "med-1",
    "status": "active",
    "medicationCodeableConcept": codeable_concept(
        [coding("rxnorm", "860975", "metformin hydrochloride 500 MG Oral Tablet")],
        text="metformin 500 mg",
    ),
    "subject": {"reference": "Patient/patient-1"},
}
```

### Observation (lab/vital span)

```python
obs = {
    "resourceType": "Observation",
    "id": "obs-1",
    "status": "final",
    "category": [{"coding": [{
        "system": "http://terminology.hl7.org/CodeSystem/observation-category",
        "code": "laboratory",
    }]}],
    "code": codeable_concept(
        [coding("loinc", "4548-4", "Hemoglobin A1c/Hemoglobin.total in Blood")],
        text="HbA1c",
    ),
    "valueQuantity": {
        "value": 7.4, "unit": "%",
        "system": "http://unitsofmeasure.org", "code": "%",
    },
    "subject": {"reference": "Patient/patient-1"},
}
```

## Hand-off to / from OpenMed

- **From OpenMed:** `result.entities` (`EntityPrediction.text/.label/.confidence
  /.start/.end`) is the input. Keep `confidence` and the offsets in an
  extension or a side log so the resource is auditable back to the source span.
- **To OpenMed:** before exporting a note that still contains PHI, run
  `openmed.deidentify(...)`; or de-identify a *built* resource/Bundle with
  `openmed.interop.fhir_operations.de_identify_resource` /
  `de_identify_bundle` (see that module — it walks free-text + narrative and
  never touches codes, references, systems, or temporal values).
- **OperationOutcome:** report any spans you could not map as
  `OperationOutcomeIssue(severity="warning", code="incomplete", diagnostics=...,
  expression="Condition.code")` → `to_operation_outcome([...])`. Keep
  diagnostics PHI-free (offsets/labels, never raw identifiers).

## Edge cases & gotchas

- **No code? Still valid.** A `CodeableConcept` with only `text` and no
  `coding` is legal R4. Emit it rather than inventing a code, and flag it via
  OperationOutcome. US Core may still require a code — see `validating-us-core`.
- **`system_uri` raises** on an unknown short id that is not a URL. Pass a known
  short id (`rxnorm`/`loinc`/`snomed`/`icd-10-cm`/`hpo`/`mesh`) or a full
  `http(s)://` system URI.
- **Negation / temporality.** OpenMed NER finds the mention, not its assertion.
  A negated ("no diabetes") or historical span should change
  `verificationStatus`/`clinicalStatus` or be dropped. Resolve assertion first
  (`resolving-clinical-context`, `openmed.clinical`).
- **Stable ids.** Give each resource a unique `id`; `to_bundle` rejects
  duplicate `ResourceType/id` pairs because they corrupt cross-references.
- **Local-first.** Grounding to RxNorm/LOINC/SNOMED is out-of-process with the
  user's own credentials. OpenMed bundles no restricted terminology.

## Standards & references

- FHIR R4 Condition: https://hl7.org/fhir/R4/condition.html
- FHIR R4 MedicationStatement: https://hl7.org/fhir/R4/medicationstatement.html
- FHIR R4 Observation: https://hl7.org/fhir/R4/observation.html
- FHIR R4 CodeableConcept: https://hl7.org/fhir/R4/datatypes.html#CodeableConcept
- HL7 terminology systems (system URIs): https://hl7.org/fhir/R4/terminologies-systems.html
- FHIR R4 OperationOutcome: https://hl7.org/fhir/R4/operationoutcome.html

