How approval-gated writes let an AI agent write to the chart

Writing to a patient's chart is a high-stakes operation. Last EHR's approval gate is a human-in-the-loop boundary: the agent proposes a write, you review it on screen, and only your click saves it to the FHIR backend. This page explains how the pattern works, what it protects against, and what it does not.

Why writes are the risk surface

An AI agent working over clinical data has two kinds of operations: reads and writes. Reads are a privacy question: any chart context the agent pulls goes to your model provider, under your API key, with no approval step. Writes are a different kind of risk: they touch the system of record.

When the agent writes, it is making a claim that becomes part of the clinical record. A model might misread a lab value or confabulate a detail that sounds right but is wrong. If those writes saved silently, they would become chart facts that influence later decisions.

The approval gate interrupts that flow. It stops unilateral writes. But it is not magic: it is a boundary, and boundaries only work if someone stands there and pays attention.

The proposal pattern: propose, then approve, then save

Last EHR uses the Vercel AI SDK's needsApproval flag on its write tools. This is the actual tool definition, abridged from lib/ai/tools.ts:

record_observation: tool({
  inputSchema: z.object({
    patientId: z.string(),
    label: z.string(),   // e.g. "Systolic blood pressure"
    value: z.number(),
    unit: z.string(),    // e.g. "mmHg"
  }),
  needsApproval: true,   // the SDK pauses here
  execute: async ({ patientId, label, value, unit }) => {
    // Runs ONLY after the user clicks Approve.
    return medplum.createResource({
      resourceType: "Observation",
      status: "final",
      code: { text: label },
      subject: { reference: `Patient/${patientId}` },
      valueQuantity: { value, unit },
    });
  },
}),
  1. Agent proposes: the agent calls add_note or record_observation with the data filled in.
  2. SDK intercepts: because needsApproval is set, execute does not run. Nothing hits the backend.
  3. UI renders the card: you see exactly what is proposed: the note text, or the label, value, and unit.
  4. Approve: execute runs and the resource is created on the backend.
  5. Cancel: the proposal disappears. Nothing is saved.

The proposal itself is not persisted. It lives in the chat session until you decide.

Anatomy of a proposed write: the two resource types

Last EHR makes two kinds of writes, both standard FHIR resources.

Notes (Communication). A note becomes a FHIR Communication with status completed, a subject pointing at the Patient, the note text as payload, and a sent timestamp. The approval card shows the full text, word for word.

Observations (Observation). A vital or lab value becomes a FHIR Observation with a label, a numeric value, and a UCUM-style unit in valueQuantity, plus status final and an effectiveDateTime. The card shows the label, value, and unit.

The exact resource shapes are in the open source, so what gets written is inspectable down to the field. On the public demo, writes are additionally tagged with your session so you only see the seed data plus your own edits; on a self-hosted instance, your Medplum AccessPolicy is the only boundary that matters.

Two enforcement layers: the UI gate and the backend AccessPolicy

The UI layer (the approval card). The SDK holds the write until you click Approve. This is a user-experience boundary, not a security boundary: it is where a human reads and decides.

The backend layer (AccessPolicy). When the write executes, it runs as the signed-in user, scoped by your Medplum AccessPolicy. If your policy forbids creating Observations, the backend rejects the request no matter what happened in the UI. The agent cannot see what you cannot see and cannot write what your policy forbids.

The two layers are independent. The UI gate stops thoughtless writes; the backend gate enforces permissions even if the UI is bypassed. Neither is a guarantee, but together they raise the bar.

What the gate does not protect against

Approval fatigue. If you always click Approve, the card becomes a ritual, not a review. Two decades of alert-fatigue research in clinical software says this is the default failure mode. The UI cannot fix that; only reading the proposals can.

Hallucinated content. The agent can propose a note containing a detail nobody said. The card gives you the chance to catch it; it does not catch it for you.

Ungated reads. Anything the agent reads from the chart goes to your model provider as context before any write is proposed. The gate controls writes; reads are governed by your provider choice and agreements, not by approval.

Where chart context goes: the model provider, your key, no PHI stored

When the agent reads the chart, it pulls FHIR resources from your Medplum backend and includes them in the prompt to your model provider, under your API key. Last EHR keeps no database of its own: no cache of chart data, no copy of the record. Data flows through your browser session, your Medplum backend, and your model provider; on the hosted demo it also passes through the app's server routes on Vercel.

Handling real PHI would require BAAs with both your model provider and your FHIR backend; consumer API keys do not qualify. The demo runs on synthetic data only. The design intent is that the layer holds nothing: you bring the backend, the model key, and the responsibility.

Open questions and where the pattern should go next

The approval gate is a starting point, not a solved problem. Open questions worth debating:

  • Approval modes. Should low-risk writes ever batch, or skip the card? Today every write is gated, every time. Safe, but it may not scale past a few writes per session.
  • Editability. Should you be able to edit a proposed note before approving? Today proposals are atomic: approve as-is or cancel.
  • Audit. The backend versions and logs the write itself, but a canceled proposal leaves no trace. Should "user saw this and rejected it" be recorded?
  • Scope. Notes and observations are deliberately low-risk write types. Conditions, medications, and orders would each raise the stakes and demand more than a single confirm click.

If you have opinions on any of these, the GitHub repo is open.

Frequently asked questions

Can the AI agent write to the chart without my approval?

No. Last EHR sets needsApproval: true on the write tools (add_note and record_observation). The Vercel AI SDK pauses before the write executes, shows you an approval card with the exact data the agent proposes, and only runs the write when you click Approve. This is a UI-level boundary enforced by the SDK, plus a backend-level boundary enforced by your Medplum AccessPolicy.

What if the proposal looks wrong?

The card shows what will be saved: the note text word for word, or the observation label, value, and unit. If it looks wrong, click Cancel. The proposal is not persisted anywhere; it simply disappears. Only clicking Approve triggers the backend write, and you can reject proposals as many times as needed.

Does the approval gate protect me from the AI hallucinating?

It gives you a chance to catch hallucinations, but only if you read the proposals. If the agent invents a symptom and you approve without reading, it will be saved. The gate is a human-in-the-loop boundary, not an automatic fact checker. Approval fatigue is real; if you approve every proposal without reading, the gate fails.

What data does the AI see, and where does it go?

When the agent reads the chart, it pulls FHIR resources from your Medplum backend and sends them to your model provider (OpenAI or Anthropic) as context, under your API key. Last EHR stores no patient data itself. For real PHI, you would need BAAs with both your model provider and your backend. The demo runs on synthetic data only.

Can I edit a proposal before I approve it?

No. Proposals are atomic: you approve them as-is or cancel them. If you want different wording or a different value, cancel and ask the agent again. This keeps the approval simple: you approve exactly what you see, and what you see is what saves.

Who enforces the approval gate at the backend?

Your Medplum AccessPolicy. The agent runs as the signed-in user, scoped by your access controls. If your AccessPolicy says the user cannot create Observations, the backend rejects the write even after the UI gate passes. The approval card is a user-experience boundary; the backend is the security boundary.

See the gate in action

Ask the demo to record a vital for a synthetic patient and watch the proposal stop at the card.