The short answer
In SMART App Launch 2, a scope has the form context/ResourceType.letters. The letters are an in-order subset of cruds: create, read, update, delete, search.1 So patient/Observation.rs lets a client read and search one patient’s Observations and nothing more. A scope can be narrowed further with query parameters, such as patient/Observation.rs?category=…|laboratory.1
For an AI agent, that means you can ask for exactly the interactions its task needs, instead of the old all-or-nothing .write. Ask for less than you think you need, check what the server actually granted, and keep a second layer of rules for what scopes can’t express.
Deedglass
The server said “saved”. Deedglass looked.
Deedglass reads it back
The syntax
The five letters, from SMART App Launch 2.2.0:1
| Letter | Allows |
|---|---|
c |
Create (type-level create) |
r |
Read, vread and instance history |
u |
Update and patch |
d |
Delete |
s |
Search, type and system history |
They must appear in that order, so rs is valid and sr isn’t. Old SMART v1 scopes map onto them: .read is .rs, .write is .cud, and .* is .cruds.1
That last mapping is the point of v2. A v1 .write scope always included delete. An agent that needs to create draft documents had to be trusted with deleting them too. In v2 it can have c alone.
patient, user and system
The context prefix decides whose data the scope reaches:1
patient/: data about one patient, chosen by the launch context, not by the scope.user/: data the signed-in user can reach, which may be many patients.system/: data a client system may reach directly, usually with no user present.
Agents running unattended, such as a nightly coding or summarisation job, typically use SMART Backend Services, which authenticate with client credentials and a signed JWT; the spec says access tokens should not exceed 300 seconds and refresh tokens should not be issued.2 Those clients get system/ scopes, and system/ is the broadest context: no single patient, no single user. That makes the resource type, the letters and the filters the only narrowing you have, so use all three.
Granular scopes with filters
Any scope can carry query parameters after a ?, joined with &, to limit it to resources matching a search.1 The specification’s own example:
Code What the screen or file shows, copied as is.
patient/Observation.rs?category=http://terminology.hl7.org/CodeSystem/observation-category|laboratory
That scope reads and searches only laboratory Observations for the launch patient. Vital signs, social history and other categories stay out of reach. The page marks search modifiers, chaining and _filter inside scopes as experimental, so stick to simple parameters for now.1
Server support for filtered scopes varies. Treat a filter as a request, and test that the server enforces it: ask for a lab-only scope, then try to read a vital-signs Observation, and confirm it’s refused.
A scope set for an agent
Here is a worked example: an agent that drafts a summary of a patient’s recent lab results for a clinician to review, and saves the draft as a document.
| Need | v1 scope you might have used | v2 scope that’s enough |
|---|---|---|
| Find the patient’s lab results | patient/Observation.read |
patient/Observation.rs?category=…|laboratory |
| Read the patient’s basic details | patient/Patient.read |
patient/Patient.r |
| Save a draft summary | patient/DocumentReference.write |
patient/DocumentReference.c |
What the v2 set leaves out is the useful part:
- No delete anywhere. The agent can’t remove a document, even its own.
- No update. A saved draft can’t be silently rewritten later; a new version needs a new create.
- No search on Patient. It can read the launch patient, not look up others.
- No non-lab Observations. If the server enforces the filter, notes and vitals are out of reach.
Writing this table before you register the client is a good exercise in itself. Every row you can’t justify is access to remove. Revisit it when the task changes, too: scopes requested for one feature tend to stay long after the feature has gone, and nobody notices until a review asks why a summarisation agent can create orders.
What the server actually grants
The scopes a server grants may differ from the ones the client requested. The server may grant none, and a request with undefined or out-of-order letters may be ignored, replaced with server defaults, or rejected.1
So read the scope value in the token response every time and compare it with what you asked for. Three outcomes need handling:
- Fewer scopes than requested. The agent’s task may fail partway; fail early instead, with a clear message.
- More scopes than requested, because of server defaults. Don’t use them; log it, and raise it with the server’s administrators.
- v1 scopes back for v2 requested, on servers that only speak v1. Map them with the table above, and notice that
.writebrings delete with it.
Where scopes stop
Scopes answer “may this client use this interaction on this resource type?”. They don’t answer:
- Is this particular write sensible?
DocumentReference.callows any document, not just a well-formed draft for the right patient. - How many? A scope doesn’t limit an agent that creates a thousand documents in a loop.
- Did the server store what was sent? That’s a question for a read-back of the reported version.
- Should a person approve it first? Scopes have no notion of approval.
Those need a second layer between the agent and the server: rules on the content of each call, limits on volume, approvals for risky writes, and a record. Our FHIR connector sits in that layer. It authenticates with SMART Backend Services, using client credentials with a signed JWT assertion, tokens kept in memory for at most 300 seconds and no refresh tokens; so far it has been tested against a local stand-in authorization server, not a vendor sandbox.3 Rules and approvals then apply per call, inside whatever the scopes allow. Nothing here is clinical; scopes and rules decide access, not care.
For the broader setup, see what to put around an agent writing to patient records; the blast radius entry explains why the scope table above matters. To try per-call rules on a real database in your browser, open the playground; to grade what an agent can reach on your machine, run npx deedglass scan (the grade, explained).
Written with AI assistance and checked against our claims register.