Capabilities & governance
The elevated SDK powers a handler can use, who must hold each one, and how a published version is held to them.
Most of the SDK is open to every handler: reading and writing rows, transactions, aggregates. A handful of SDK powers reach further (raw SQL, deleting rows, sending email, calling Stripe, reading across organizations), and those are capabilities.
A capability is not granted to a handler. It is held by people, through the app's policies, and the platform reads which ones a handler's code uses straight from its source. A handler can use a power only if everyone who wrote, tested and approved it holds that power, and only the powers its published version was frozen with.
The capabilities
| Capability | Covers | Default | Risk |
|---|---|---|---|
sdk:read | read, readList, readPage, aggregate, summarize | granted | low |
sdk:write | create, createMany, update, updateWhere, generateRecords | granted | low |
sdk:transaction | withTransaction | granted | low |
sdk:app_read | sdk.app.branding() | granted | low |
sdk:storage_read | Listing files and folders, metadata, download URLs | granted | low |
sdk:crons_read | Listing scheduled jobs, their runs and roles, previewing a schedule | granted | low |
sdk:roles_read | sdk.roles.list() | granted | low |
sdk:delete | delete, deleteWhere | deny | medium |
sdk:external_email | sdk.sendEmail | deny | medium |
sdk:import | sdk.importCsv | deny | medium |
sdk:storage_write | Upload, copy, move, rename, delete files and folders (implies sdk:storage_read) | deny | medium |
sdk:query | Raw SQL: sdk.sql and sdk.query | deny | high |
sdk:shared_read | Reading across every organization with scope: { shared: true } | deny | high |
sdk:external_stripe | sdk.stripe.* | deny | high |
sdk:crons_write | Create, change, pause, delete and trigger scheduled jobs (implies sdk:crons_read) | deny | high |
sdk:roles_write | Give or take away a person's role (implies sdk:roles_read) | deny | high |
sdk:mcp:<alias> | Calling tools on the external tool server granted to the app under <alias> | deny | high |
sdk:query_ddl | Raw SQL that changes schema (implies sdk:query) | deny | critical |
Granted capabilities are part of every author's base grant, so ordinary handlers never meet this system. Deny capabilities must be granted explicitly. The SDK reference badges each method with the capability it needs.
Two namespaces split by method, because one name covers both harmless and dangerous calls:
sdk.storage.list() is a read and sdk.storage.delete() destroys bytes;
sdk.crons.list() is a read and sdk.crons.update() decides what code an armed job runs. A method
of either namespace that is not classified as a read is treated as a write.
Checked at every boundary
Save
The code is scanned for every power it uses, and saving is refused unless you hold all of them:
SDK_CAP_NOT_GRANTED (403), listing each missing capability with its risk. The scan ignores
comments, so a method named only in a comment needs nothing.
Dry run
To dry-run the code, you must hold every power it uses too. A dry run skips per-call checks, so this is the check that stops it being a way around them.
Approve and publish
The approver must hold every power the version uses. Nobody can approve a power they could not have written. Publishing then freezes the detected set onto the version as its manifest.
Run time
The frozen manifest decides the generic powers on every call: raw SQL, email, storage, scheduled
job management, cross-org reads, and external tool servers. A call outside it fails with
SDK_CAP_NOT_IN_MANIFEST (403). Everything else (which tables, which rows) is decided by the
run's ordinary authorization: the endpoint's allowed actions, or a job's roles.
Freezing at publish is what makes "what can this version do" a question with an answer. Granting an author a new capability does not change what an already-published version may do; publish a new version to pick it up. Revoking one does not quietly break code already in production; the next version simply cannot ship without someone who holds it.
Granting a capability
Capabilities are rows in the policy editor, alongside ordinary actions, and they are checked where the person's own roles live:
- for members of your organization working in the console, in the organization's policies (Access › Policies);
- for the app's own users and agents that author handlers, in the app's (App access › Policies).
Grant one to the role of whoever needs to author, test or approve that kind of handler. When the editor refuses a save, it links to the policy editor with the missing capabilities picked. An organization owner holds every capability already.
High and critical capabilities ask for confirmation when granted. Grant the narrowest set that
works: most raw SQL can be a readList or an aggregate, and a handler with no elevated power is
one you never have to think about again.
Rules that are about the code, not the person
Some code is refused whoever writes it, because the manifest could not describe what it does:
- String-built SQL.
sdk.querytakes only a static string; dynamic SQL goes throughsdk.sql. OtherwiseHANDLER_UNSAFE_RAW_SQL. See Raw SQL safely. - Dynamic tool-server access.
sdk.mcp.billing.create_refund(args)is fine;sdk.mcp[server][tool](args)is refused, because the manifest has to name the servers a handler can reach. A tool the app does not have is refused too, and so is an external tool call insidewithTransaction, since an outside call cannot be rolled back. scope: { shared: true }written anywhere in the code counts assdk:shared_read.