From openmed-skills
Fetches and pages FHIR R4 resources from EHR APIs, decodes base64 attachments, and extracts clinical narrative for OpenMed de-identification and analysis.
How this skill is triggered — by the user, by Claude, or both
Slash command
/openmed-skills:fetching-fhir-resourcesThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
FHIR R4 is the modern EHR API: a RESTful, JSON-or-XML interface over resources
FHIR R4 is the modern EHR API: a RESTful, JSON-or-XML interface over resources
like Patient, Encounter, Condition, Observation, DiagnosticReport,
and DocumentReference. The unstructured clinical text you want for NLP lives
in DocumentReference.content.attachment and
DiagnosticReport.presentedForm — usually base64-encoded PDF, RTF, or
plain text. This skill pulls those resources, pages through results, decodes the
attachments, and hands the narrative to OpenMed.
Bundle.link[next]).Search is GET [base]/[Type]?param=value. Results come back as a
searchset Bundle; the next page is the URL in Bundle.link where
relation == "next". Use _count to size pages, _revinclude to pull related
resources in one round trip, and _since/_lastUpdated for incremental sync.
GET /Patient?identifier=http://hospital.org/mrn|12345
GET /DocumentReference?patient=Patient/abc&category=clinical-note&_count=50
GET /DiagnosticReport?patient=Patient/abc&_revinclude=Observation:related
Page a search, decode attachments, hand narrative to OpenMed:
import base64
import requests
import openmed
BASE = "https://fhir.example.org/r4"
HEADERS = {"Accept": "application/fhir+json", "Authorization": "Bearer <token>"}
def iter_bundle(url, params=None):
"""Yield resources across all pages following Bundle.link[next]."""
while url:
bundle = requests.get(url, params=params, headers=HEADERS, timeout=30).json()
for entry in bundle.get("entry", []):
yield entry.get("resource", {})
params = None # next links are fully-qualified
url = next(
(l["url"] for l in bundle.get("link", []) if l.get("relation") == "next"),
None,
)
def attachment_text(att):
"""Decode a FHIR Attachment to text (handles base64 and inline text/plain)."""
if att.get("data"):
raw = base64.b64decode(att["data"])
if att.get("contentType", "").startswith("text/"):
return raw.decode("utf-8", "replace")
return "" # PDF/RTF: route to OpenMed multimodal/OCR intake instead
return ""
# Pull a patient's clinical notes and analyze each.
for doc in iter_bundle(f"{BASE}/DocumentReference",
{"patient": "Patient/abc",
"category": "clinical-note", "_count": 50}):
for content in doc.get("content", []):
text = attachment_text(content.get("attachment", {}))
if not text.strip():
continue
deid = openmed.deidentify(text, method="replace", policy="hipaa_safe_harbor")
result = openmed.analyze_text(deid.text, output_format="dict")
patient_ref = doc.get("subject", {}).get("reference") # rejoin key
system/DocumentReference.read, system/DiagnosticReport.read).patient, category, type (LOINC),
date, and _count. Prefer server-side filtering over client-side.Bundle.link[next] until exhausted. Never assume one page.DocumentReference.content.attachment and
DiagnosticReport.presentedForm. Decode base64; for PDF/RTF/scanned
content, route bytes to OpenMed's document intake (multimodal/ocr)
rather than decoding as UTF-8.subject.reference (patient) and context.encounter
so downstream consumers can group by patient/encounter — storing hashed,
not raw, identifiers.To OpenMed (client-side): decoded narrative → openmed.deidentify →
openmed.analyze_text. Carry subject.reference as the rejoin key.
Server-side $de-identify: openmed.interop.fhir_operations implements
the FHIR $de-identify operation logic over the OpenMed privacy pipeline:
de_identify_resource(resource, policy=..., method=...)de_identify_bundle(bundle, policy=..., method=...)de_identify(parameters) — accepts/returns a Parameters envelope and
reports modified element paths as an OperationOutcome.
It de-identifies free-text strings, identifier values, and text.div
narrative while never altering codes, references, systems, or temporal
values. Use this to de-identify a whole fetched Bundle before storage:from openmed.interop.fhir_operations import de_identify_bundle
safe_bundle = de_identify_bundle(bundle, policy="hipaa_safe_harbor",
method="replace")
Onward: re-export structured findings with
openmed.clinical.exporters.fhir (to_bundle, to_operation_outcome).
attachment.data is base64; large files use
attachment.url (a separate Binary fetch) instead. Handle both.application/pdf, text/rtf, scanned TIFF — do
not utf-8 decode these; send bytes to OpenMed multimodal/OCR intake.next links; cap page
count and dedupe by resource id._revinclude vs _include. _include pulls referenced resources;
_revinclude pulls resources that reference yours. Mixing them changes
Bundle entry search.mode (match vs include) — filter on it./metadata
CapabilityStatement) and US Core-conformant; field cardinality differs across
FHIR versions.429/Retry-After; batch with _count and back off.presentedForm): https://hl7.org/fhir/R4/diagnosticreport.htmlnpx claudepluginhub maziyarpanahi/openmed --plugin openmed-skillsConnects to a FHIR R4 EHR (Epic, Cerner, MEDITECH, athenahealth) via SMART-on-FHIR, pulls patient clinical data and notes, and extracts structured findings.
Executes FHIR Bulk Data $export (system/group/patient) and streams NDJSON into OpenMed's de-identification + NER pipeline. Handles async kickoff, polling, and _type/_since filters.
Exports personal health records from a local HealthClaw FHIR store as a portable, de-identified FHIR R4 transaction bundle. Useful for data migration, archiving, or sharing with providers.