OpenClaw-Medical-Skills fhir-developer-skill
install
source · Clone the upstream repo
git clone https://github.com/FreedomIntelligence/OpenClaw-Medical-Skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/FreedomIntelligence/OpenClaw-Medical-Skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/fhir-developer-skill" ~/.claude/skills/freedomintelligence-openclaw-medical-skills-fhir-developer-skill && rm -rf "$T"
OpenClaw · Install into ~/.openclaw/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/FreedomIntelligence/OpenClaw-Medical-Skills "$T" && mkdir -p ~/.openclaw/skills && cp -r "$T/skills/fhir-developer-skill" ~/.openclaw/skills/freedomintelligence-openclaw-medical-skills-fhir-developer-skill && rm -rf "$T"
manifest:
skills/fhir-developer-skill/SKILL.mdsource content
FHIR Developer Skill
Quick Reference
HTTP Status Codes
| Code | When to Use |
|---|---|
| Successful read, update, or search |
| Successful create (include header) |
| Successful delete |
| Malformed JSON, wrong resourceType |
| Missing, expired, revoked, or malformed token (RFC 6750) |
| Valid token but insufficient scopes |
| Resource doesn't exist |
| If-Match ETag mismatch (NOT 400!) |
| Missing required fields, invalid enum values, business rule violations |
Required Fields by Resource (FHIR R4)
| Resource | Required Fields | Everything Else |
|---|---|---|
| Patient | (none) | All optional |
| Observation | , | Optional |
| Encounter | , | Optional (including , ) |
| Condition | | Optional (including , ) |
| MedicationRequest | , , , | Optional |
| Medication | (none) | All optional |
| Bundle | | Optional |
Required vs Optional Fields (CRITICAL)
Only validate fields with cardinality starting with "1" as required.
| Cardinality | Required? |
|---|---|
, | NO |
, | YES |
Common mistake: Making
subject or period required on Encounter. They are 0..1 (optional).
Value Sets (Enum Values)
Invalid enum values must return
422 Unprocessable Entity.
Patient.gender
male | female | other | unknown
Observation.status
registered | preliminary | final | amended | corrected | cancelled | entered-in-error | unknown
Encounter.status
planned | arrived | triaged | in-progress | onleave | finished | cancelled | entered-in-error | unknown
Encounter.class (Common Codes)
| Code | Display | Use |
|---|---|---|
| ambulatory | Outpatient visits |
| inpatient encounter | Hospital admissions |
| emergency | Emergency department |
| virtual | Telehealth |
Condition.clinicalStatus
active | recurrence | relapse | inactive | remission | resolved
Condition.verificationStatus
unconfirmed | provisional | differential | confirmed | refuted | entered-in-error
MedicationRequest.status
active | on-hold | cancelled | completed | entered-in-error | stopped | draft | unknown
MedicationRequest.intent
proposal | plan | order | original-order | reflex-order | filler-order | instance-order | option
Bundle.type
document | message | transaction | transaction-response | batch | batch-response | history | searchset | collection
Validation Pattern
Python/FastAPI:
from fastapi import FastAPI from fastapi.responses import JSONResponse app = FastAPI() def operation_outcome(severity: str, code: str, diagnostics: str): return { "resourceType": "OperationOutcome", "issue": [{"severity": severity, "code": code, "diagnostics": diagnostics}] } VALID_OBS_STATUS = {"registered", "preliminary", "final", "amended", "corrected", "cancelled", "entered-in-error", "unknown"} @app.post("/Observation", status_code=201) async def create_observation(data: dict): if not data.get("status"): return JSONResponse(status_code=422, content=operation_outcome( "error", "required", "Observation.status is required" ), media_type="application/fhir+json") if data["status"] not in VALID_OBS_STATUS: return JSONResponse(status_code=422, content=operation_outcome( "error", "value", f"Invalid status '{data['status']}'" ), media_type="application/fhir+json") # ... create resource
TypeScript/Express:
const VALID_OBS_STATUS = new Set(['registered', 'preliminary', 'final', 'amended', 'corrected', 'cancelled', 'entered-in-error', 'unknown']); app.post('/Observation', (req, res) => { if (!req.body.status) { return res.status(422).contentType('application/fhir+json') .json(operationOutcome('error', 'required', 'Observation.status is required')); } if (!VALID_OBS_STATUS.has(req.body.status)) { return res.status(422).contentType('application/fhir+json') .json(operationOutcome('error', 'value', `Invalid status '${req.body.status}'`)); } // ... create resource });
Pydantic v2 Models (use
Literal, not const=True):
from typing import Literal from pydantic import BaseModel class Patient(BaseModel): resourceType: Literal["Patient"] = "Patient" id: str | None = None gender: Literal["male", "female", "other", "unknown"] | None = None
Coding Systems (URLs)
| System | URL |
|---|---|
| LOINC | |
| SNOMED CT | |
| RxNorm | |
| ICD-10 | |
| v3-ActCode | |
| Observation Category | |
| Condition Clinical | |
| Condition Ver Status | |
Common LOINC Codes (Vital Signs)
| Code | Description |
|---|---|
| Heart rate |
| Systolic blood pressure |
| Diastolic blood pressure |
| Body temperature |
| Oxygen saturation (SpO2) |
Data Type Patterns
Coding (direct) vs CodeableConcept (wrapped)
Coding - Used by
Encounter.class:
{"system": "http://terminology.hl7.org/CodeSystem/v3-ActCode", "code": "AMB"}
CodeableConcept - Used by
Observation.code, Condition.code:
{"coding": [{"system": "http://loinc.org", "code": "8480-6"}], "text": "Systolic BP"}
Reference
{"reference": "Patient/123", "display": "John Smith"}
Identifier
{"system": "http://hospital.example.org/mrn", "value": "12345"}
Common Mistakes
| Mistake | Correct Approach |
|---|---|
Making or required on Encounter | Both are 0..1 (optional). Only and are required |
Using CodeableConcept for | uses Coding directly: |
| Returning 400 for ETag mismatch | Use for If-Match failures |
| Returning 400 for invalid enum values | Use for validation errors |
| Forgetting Content-Type header | Always set |
| Missing Location header on create | Return with 201 Created |
Resource Structures
For complete JSON examples of all resources, see references/resource-examples.md.
Quick reference for error responses:
{ "resourceType": "OperationOutcome", "issue": [{"severity": "error", "code": "not-found", "diagnostics": "Patient/123 not found"}] }
RESTful Endpoints
POST /[ResourceType] # Create (returns 201 + Location header) GET /[ResourceType]/[id] # Read PUT /[ResourceType]/[id] # Update DELETE /[ResourceType]/[id] # Delete (returns 204) GET /[ResourceType]?param=value # Search (returns Bundle) GET /metadata # CapabilityStatement POST / # Bundle transaction/batch
Conditional Operations
If-Match (optimistic locking):
- Client sends:
If-Match: W/"1" - Mismatch returns
412 Precondition Failed
If-None-Exist (conditional create):
- Client sends:
If-None-Exist: identifier=http://mrn|12345 - Match exists: return existing (200)
- No match: create new (201)
Reference Files
For detailed guidance, see:
- Resource Examples: Complete JSON structures for Patient, Observation, Encounter, Condition, MedicationRequest, OperationOutcome, CapabilityStatement
- SMART on FHIR Authorization: OAuth flows, scope syntax (v1/v2), backend services, scope enforcement
- Pagination: Search result pagination,
/_count
parameters, link relations_offset - Bundle Operations: Transaction vs batch semantics, atomicity, processing order
Implementation Checklist
- Set
on all responsesContent-Type: application/fhir+json - Return
andmeta.versionId
on resourcesmeta.lastUpdated - Return
header on create:Location/Patient/{id} - Return
header:ETagW/"{versionId}" - Use OperationOutcome for all error responses
- Validate required fields → 422 for missing
- Validate enum values → 422 for invalid
- Search returns Bundle with
type: "searchset"
Quick Start Script
To scaffold a new FHIR API project with correct Pydantic v2 patterns:
python scripts/setup_fhir_project.py my_fhir_api
Creates a FastAPI project with correct models, OperationOutcome helpers, and Patient CRUD endpoints.