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

CapabilityCoversDefaultRisk
sdk:readread, readList, readPage, aggregate, summarizegrantedlow
sdk:writecreate, createMany, update, updateWhere, generateRecordsgrantedlow
sdk:transactionwithTransactiongrantedlow
sdk:app_readsdk.app.branding()grantedlow
sdk:storage_readListing files and folders, metadata, download URLsgrantedlow
sdk:crons_readListing scheduled jobs, their runs and roles, previewing a schedulegrantedlow
sdk:roles_readsdk.roles.list()grantedlow
sdk:deletedelete, deleteWheredenymedium
sdk:external_emailsdk.sendEmaildenymedium
sdk:importsdk.importCsvdenymedium
sdk:storage_writeUpload, copy, move, rename, delete files and folders (implies sdk:storage_read)denymedium
sdk:queryRaw SQL: sdk.sql and sdk.querydenyhigh
sdk:shared_readReading across every organization with scope: { shared: true }denyhigh
sdk:external_stripesdk.stripe.*denyhigh
sdk:crons_writeCreate, change, pause, delete and trigger scheduled jobs (implies sdk:crons_read)denyhigh
sdk:roles_writeGive or take away a person's role (implies sdk:roles_read)denyhigh
sdk:mcp:<alias>Calling tools on the external tool server granted to the app under <alias>denyhigh
sdk:query_ddlRaw SQL that changes schema (implies sdk:query)denycritical

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.query takes only a static string; dynamic SQL goes through sdk.sql. Otherwise HANDLER_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 inside withTransaction, since an outside call cannot be rolled back.
  • scope: { shared: true } written anywhere in the code counts as sdk:shared_read.

Next

On this page