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.
74
91%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
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).
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.
OpenMed deliberately ships the purely mechanical pieces and leaves clinical judgement to you. The verified entry points:
# 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.
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.
Condition.code{
"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"
}
}openmed.analyze_text(note, model_name=...) → result.entities.Condition; a drug → MedicationStatement;
a lab/vital/measurement → Observation.CodeableConcept with only text and no coding.CodeableConcept with coding(...) + codeable_concept(...).clinicalStatus/status, subject,
dates). Use the cheat-sheet below.{"reference": "Patient/<id>"}.to_bundle(...) (assembling-fhir-bundles) and validate
(validating-us-core).| 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 |
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"},
}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"},
}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.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).OperationOutcomeIssue(severity="warning", code="incomplete", diagnostics=..., expression="Condition.code") → to_operation_outcome([...]). Keep
diagnostics PHI-free (offsets/labels, never raw identifiers).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.verificationStatus/clinicalStatus or be dropped. Resolve assertion first
(resolving-clinical-context, openmed.clinical).id; to_bundle rejects
duplicate ResourceType/id pairs because they corrupt cross-references.80da98c
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.