Reference

Putting AI agents on FHIR data

SMART on FHIR was designed for applications whose behaviour is fixed before anyone grants them access. You can read an app's code, certify it, and know what it will ask for. An agent decides at runtime what to fetch, and that decision is shaped by the data it has just read.

Two things follow, and most of this page is a consequence of them. The scope you grant is the scope the agent may fully use. And the data becomes an input to control flow, which means retrieved content is untrusted in a way a database row never was.

Written from building HealthClaw, an open-source guardrail layer for exactly this problem.

01 / Patterns

Four ways to connect an agent

The choice here determines what every later control can do. Policy you cannot enforce at the connection point has to be enforced in a prompt, and a prompt is a request rather than a constraint.

PatternShapePolicy granularityAudit
Policy brokerAgent → broker → FHIR serverPer callCentral, complete
Scoped SMART appAgent → FHIR server, OAuth tokenPer sessionServer-side, coarse
MCP server over FHIRAgent → tools → FHIR serverPer tool callPer tool call
Direct database or warehouseanti-patternAgent → SQLNone meaningfulQuery logs at best
Policy broker
The broker holds the credentials, applies policy to every request and response, and is the only thing the model can reach. Costs you a service to operate and a hop of latency. This is what HealthClaw implements.
Scoped SMART app
Standard, portable, and works against any conformant server. But the scope is fixed when the token is minted, so whatever you grant the agent may fully use for the life of that token. The server sees one client id, not which of the agent’s steps made the call.
MCP server over FHIR
The tool boundary becomes the policy boundary, and tools are enumerable and testable in a way that free-form HTTP is not. Newer, and the tool descriptions are themselves model-visible text, so they are part of your injection surface.
Direct database or warehouse
Skips resource-level authorization, consent, and the compartment model entirely. Attractive because it is fast to build and the data is already flattened for analytics. Treat it as an anti-pattern for anything patient-identifiable.

02 / Scopes

Grant read and search, not everything

SMART App Launch 2.0 replaced the coarse .read and .write with five letters — create, read, update, delete, search — which must appear in that order. The distinction matters for agents specifically: reading a known resource and searching across a type are different capabilities, and an agent exploring a record does far more of the second.

# Too broad. The agent may read and search every resource type
# in the compartment, for the life of the token.
patient/*.read

# Better. Read and search, two resource types, nothing else.
patient/Observation.rs
patient/Condition.rs

# Better still. Constrain with search parameters, so the grant
# cannot be widened by changing the query.
patient/Observation.rs?category=http://terminology.hl7.org/CodeSystem/observation-category|laboratory

# A write path, granted separately and deliberately.
patient/ServiceRequest.c
v1 scopes still map onto v2: .read becomes .rs, .write becomes .cud, and .* becomes .cruds. A server advertising permission-v1 will accept the old form — which is why an agent asking for patient/*.read is asking for more than it looks like.

Write scopes are a different decision

Read access to the wrong resource is a privacy incident. Write access to the wrong resource changes a clinical record. Grant c, u and d in a separate token from the read path, so that widening the agent's reading never silently widens what it can change.

03 / Redaction

Minimum necessary, applied to a context window

HIPAA's minimum necessary standard predates the idea that a request would be assembled by a statistical model, but it maps onto it cleanly: the agent should receive the least data that lets it do the job. The practical difficulty is that FHIR resources carry identity in more places than people expect.

The narrative block is the one people miss

Nearly every resource may carry a text.div — an XHTML restatement of its contents for human display. Strip Patient.name and leave the narrative untouched and you have redacted nothing; the name is still there, in markup, one field over.

{
  "resourceType": "Patient",
  "id": "example",
  "text": {
    "status": "generated",
    "div": "<div xmlns=\"http://www.w3.org/1999/xhtml\">
              Jane Q. Doe, MRN 44xxxx19, born 1974-03-02
            </div>"
  },
  "name": [ { "_family": { "extension": [ /* redacted */ ] } } ],
  "identifier": [ /* redacted */ ]
}
Structured redaction that ignores narrative is not redaction. Either drop text.div entirely for agent traffic, or regenerate it from the redacted resource.

The other reliable carriers are Patient.telecom, Patient.address and every free-text note on a clinical resource. Dates of service are quasi-identifiers too: a handful of them plus a postcode re-identifies a person more often than teams assume.

04 / Audit

Record what a model did, not just what a client did

A conventional access log answers “which client read this record.” For an agent that is not enough to answer the question you will actually be asked, which is why it read the record and who was accountable for the result.

FHIR gives you two resources for this and they do different jobs. AuditEvent records that an access happened. Provenance records where a piece of data came from, which is what you need when an agent contributes to the record rather than only reading it.

Capture the model as a first-class actor

At minimum, an agent action should be reconstructable from the audit trail alone: which model and version, which system prompt revision, which tool was called with which parameters, which human approved it if a human did, and which patient compartment it ran in. If you cannot answer those from storage six months later, you do not have an audit trail — you have logs.

05 / Approval

Writes go to a queue, not to the server

The safest agent write path does not write. It proposes: the agent produces a candidate resource, the broker holds it, and a human approves or rejects it before anything reaches the FHIR server. The approval is itself recorded, so the clinical record carries a human's accountability rather than a model's.

This is unglamorous and it is the control that survives contact with a compliance review. It also degrades well: when the model is wrong, the failure is a rejected proposal rather than a corrected chart.

06 / Failure modes

What actually goes wrong

Ordered roughly by how often they appear in real implementations rather than by severity.

  1. 01

    Prompt injection carried in clinical content

    Clinical free text is authored by many parties, and some of it is patient-supplied. A DocumentReference attachment, an Observation.note, or a Condition.note can contain text that reads as instruction to a model. The mitigation is architectural, not lexical: never let retrieved content reach the model in a position where it can be interpreted as instruction, and never give the model a tool whose blast radius you would not accept being triggered by a sentence in a scanned note.

  2. 02

    Narrative that survives structured redaction

    Almost every FHIR resource can carry a text.div narrative — an XHTML restatement of the resource intended for human display. Redacting Patient.name and Patient.identifier while passing the resource through untouched leaves the same identifiers sitting in the narrative block. Redaction has to run over narrative and structured fields both, or it is theatre.

  3. 03

    Silent overfetch

    Patient/$everything returns the entire patient compartment in one call, and _include / _revinclude quietly widen a search well past what the query appears to ask for. An agent optimising for "have enough context" will find these. Bound them at the broker, not in the prompt.

  4. 04

    Context bleed between patients

    A long-running session that touches two patients can carry the first one’s data into reasoning about the second. Bind a session to a patient compartment and make crossing it require a new session rather than a new instruction.

  5. 05

    Scope reuse across workflows

    A token minted generously for one workflow gets reused for the next one because it is already there and it works. Mint narrowly, expire aggressively, and make issuing a new token cheaper than widening an old one.

Next

Reference implementation

HealthClaw is the broker pattern described above, built in the open. Redaction, audit and human sign-off are enforced server-side rather than requested in a prompt, which is the whole argument of this page reduced to running code.