<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Faheem</title>
    <description>The latest articles on DEV Community by Faheem (@faheem_syed).</description>
    <link>https://dev.to/faheem_syed</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3800855%2Fa4cb0460-1260-475b-9054-e9a8f55927a1.jpg</url>
      <title>DEV Community: Faheem</title>
      <link>https://dev.to/faheem_syed</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/faheem_syed"/>
    <language>en</language>
    <item>
      <title>Building an AI Agent on OpenEMR: What the Docs Don't Tell You</title>
      <dc:creator>Faheem</dc:creator>
      <pubDate>Mon, 02 Mar 2026 04:44:19 +0000</pubDate>
      <link>https://dev.to/faheem_syed/building-an-ai-agent-on-openemr-what-the-docs-dont-tell-you-4kkj</link>
      <guid>https://dev.to/faheem_syed/building-an-ai-agent-on-openemr-what-the-docs-dont-tell-you-4kkj</guid>
      <description>&lt;p&gt;OpenEMR is one of the most widely deployed open-source EHR systems in the world, but building an AI agent on top of it involves a handful of non-obvious gotchas that will burn you if you go in blind.&lt;/p&gt;

&lt;p&gt;This is a practical guide based on real production experience. Not a tutorial — just the things I wish were documented.&lt;/p&gt;




&lt;h2&gt;
  
  
  Use FHIR for Clinical Reads, REST for Exceptions
&lt;/h2&gt;

&lt;p&gt;OpenEMR exposes two APIs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FHIR R4&lt;/strong&gt; at &lt;code&gt;/apis/default/fhir/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Standard REST&lt;/strong&gt; at &lt;code&gt;/apis/default/api/&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The short answer: &lt;strong&gt;use FHIR for everything you can.&lt;/strong&gt; The Standard API mixes numeric &lt;code&gt;pid&lt;/code&gt; with UUID &lt;code&gt;puuid&lt;/code&gt; depending on the endpoint (medications and vitals use numeric IDs, everything else uses UUIDs). FHIR uses UUIDs consistently.&lt;/p&gt;

&lt;p&gt;The two exceptions where you'll need the REST API:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Insurance/coverage&lt;/strong&gt; — the FHIR Coverage mapper is incomplete (missing &lt;code&gt;plan_name&lt;/code&gt;, &lt;code&gt;group_number&lt;/code&gt;, &lt;code&gt;date_end&lt;/code&gt;). Use &lt;code&gt;GET /api/patient/:puuid/insurance&lt;/code&gt; instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Practitioner specialty&lt;/strong&gt; — FHIR Practitioner doesn't map the &lt;code&gt;specialty&lt;/code&gt; field. Use &lt;code&gt;GET /api/practitioner&lt;/code&gt; which reads directly from the &lt;code&gt;users&lt;/code&gt; table.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  OAuth2: Five Things That Will Break You
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Clients are disabled by default
&lt;/h3&gt;

&lt;p&gt;After OAuth client registration, your client is created with &lt;code&gt;is_enabled=0&lt;/code&gt;. Auth fails silently — no clear error message. Fix with SQL after registration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;oauth_clients&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;is_enabled&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;grant_types&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'authorization_code password refresh_token'&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;client_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'&amp;lt;your_client_id&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Consider two separate tokens for FHIR and REST
&lt;/h3&gt;

&lt;p&gt;The official docs show &lt;code&gt;api:fhir&lt;/code&gt; and &lt;code&gt;api:oemr&lt;/code&gt; being registered together, and it works in many setups. In practice we found that separating them — one token per API type — made scope debugging and token renewal much cleaner. If you're only using FHIR, don't worry about it. If you're hitting both APIs, separate tokens are worth considering.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. &lt;code&gt;user_role=users&lt;/code&gt; is required (and not in the OAuth spec)
&lt;/h3&gt;

&lt;p&gt;The password grant requires a non-standard parameter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;grant_type=password
user_role=users   ← required, not standard OAuth2
username=admin
password=...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Omit it and you get a generic auth error.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Use &lt;code&gt;user/*&lt;/code&gt; scopes, not &lt;code&gt;patient/*&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;patient/*&lt;/code&gt; scopes require a SMART on FHIR launch context. For a backend service agent, always use &lt;code&gt;user/*&lt;/code&gt;. And FHIR resource names in scopes are case-sensitive uppercase:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✅ user/Patient.rs
✅ user/MedicationRequest.rs
❌ user/patient.rs   (Standard API format, wrong for FHIR)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. Registration silently drops some REST scopes
&lt;/h3&gt;

&lt;p&gt;Registering with &lt;code&gt;user/practitioner.read&lt;/code&gt; in the scope string? It gets silently dropped during registration. After registering, inject missing scopes via SQL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;oauth_clients&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;scope&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CONCAT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;' user/practitioner.read'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;client_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'&amp;lt;your_client_id&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  FHIR Bugs to Know About
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Don't trust &lt;code&gt;bundle.total&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;total&lt;/code&gt; field in FHIR search responses is unreliable — &lt;code&gt;_summary=count&lt;/code&gt; always returns 0. Always use &lt;code&gt;len(bundle["entry"])&lt;/code&gt; instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;Practitioner?name=&lt;/code&gt; returns 500
&lt;/h3&gt;

&lt;p&gt;Searching practitioners by full name crashes. Use &lt;code&gt;family&lt;/code&gt; instead and strip title prefixes ("Dr.", "Doctor") before passing them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /fhir/Practitioner?family=Chen    ✅
GET /fhir/Practitioner?name=Dr.+Chen  ❌  (500)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;Appointment?status=free&lt;/code&gt; returns nothing
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;free&lt;/code&gt; is a valid FHIR &lt;em&gt;Slot&lt;/em&gt; status, but not a valid Appointment status. Querying it returns an empty bundle. To find unbooked appointment slots, fetch all appointments in a date range and detect free slots client-side: a slot with no Patient participant is unbooked.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# A slot is free if it has no patient in participants
&lt;/span&gt;&lt;span class="n"&gt;is_free&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Patient&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;actor&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reference&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;appointment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;participant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;_count&lt;/code&gt; breaks Appointment search
&lt;/h3&gt;

&lt;p&gt;Passing &lt;code&gt;_count=N&lt;/code&gt; to &lt;code&gt;GET /fhir/Appointment&lt;/code&gt; returns empty results regardless of data. Leave it out and accept OpenEMR's default page size.&lt;/p&gt;

&lt;h3&gt;
  
  
  FHIR Appointment write may not work (version-dependent)
&lt;/h3&gt;

&lt;p&gt;On OpenEMR 7.0.x, &lt;code&gt;POST /fhir/Appointment&lt;/code&gt; returned 404 in our setup, and write scopes for Appointment were rejected at the token endpoint. The API README lists Appointment create as supported, so this may be fixed in newer versions or specific to certain configurations. If you hit this, inserting directly via SQL into &lt;code&gt;openemr_postcalendar_events&lt;/code&gt; is the fallback.&lt;/p&gt;

&lt;h3&gt;
  
  
  The REST insurance &lt;code&gt;provider&lt;/code&gt; field is a numeric FK
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;GET /api/patient/:puuid/insurance&lt;/code&gt; returns a &lt;code&gt;provider&lt;/code&gt; field that looks like a company name but is actually a numeric foreign key to another table. Use &lt;code&gt;plan_name&lt;/code&gt; for display instead.&lt;/p&gt;




&lt;h2&gt;
  
  
  One External API Note
&lt;/h2&gt;

&lt;p&gt;If you're planning to use the &lt;strong&gt;RxNorm Drug Interaction API&lt;/strong&gt; for medication safety checks — don't. The interaction check endpoints were discontinued on January 2, 2024. The RxCUI lookup (&lt;code&gt;/rxcui.json&lt;/code&gt;) still works, but &lt;code&gt;/interaction/list.json&lt;/code&gt; returns 404. Plan for a local curated dataset or DrugBank (paid) instead.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Architecture That Works
&lt;/h2&gt;

&lt;p&gt;Given all of the above, here's what a clean integration looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FHIRClient
├── _token      → FHIR token (api:fhir scopes, user/Resource.rs)
├── _rest_token → REST token (api:oemr scopes, user/resource.read)
├── get_token() → lazy acquire, re-auth before 1hr expiry (no refresh tokens)
└── Methods
    ├── All clinical reads → FHIR endpoints
    ├── get_insurance()   → REST primary, FHIR fallback
    └── get_providers()   → FHIR search + REST specialty enrichment
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No refresh tokens are issued with the password grant. Just re-authenticate before the 1-hour expiry.&lt;/p&gt;




&lt;h2&gt;
  
  
  Quick Reference: Things Not to Do
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Don't use Standard API for clinical reads&lt;/li&gt;
&lt;li&gt;Don't trust &lt;code&gt;bundle.total&lt;/code&gt; — check &lt;code&gt;entry&lt;/code&gt; array length&lt;/li&gt;
&lt;li&gt;Don't assume mixed &lt;code&gt;api:oemr&lt;/code&gt; + &lt;code&gt;api:fhir&lt;/code&gt; scope behavior is consistent across versions&lt;/li&gt;
&lt;li&gt;Don't use &lt;code&gt;patient/*&lt;/code&gt; scopes (need &lt;code&gt;user/*&lt;/code&gt; for backend services)&lt;/li&gt;
&lt;li&gt;Don't use &lt;code&gt;Practitioner?name=&lt;/code&gt; (500 error) — use &lt;code&gt;family=&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Don't query &lt;code&gt;Appointment?status=free&lt;/code&gt; — detect free slots client-side&lt;/li&gt;
&lt;li&gt;Don't pass &lt;code&gt;_count&lt;/code&gt; to Appointment search&lt;/li&gt;
&lt;li&gt;Don't rely on the RxNorm Drug Interaction API (discontinued Jan 2024)&lt;/li&gt;
&lt;li&gt;Don't forget &lt;code&gt;user_role=users&lt;/code&gt; in token requests&lt;/li&gt;
&lt;li&gt;Don't assume registered scopes are actually saved — verify via SQL&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Built by Faheem Syed while integrating a LangGraph-based clinical AI assistant with OpenEMR 7.x.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>openemr</category>
      <category>fhir</category>
      <category>aiagents</category>
      <category>ehr</category>
    </item>
  </channel>
</rss>
