Use client.cap.call for capability reads/mutations and client.cap.watch/live for capability subscriptions. A script receives cap only. These are typed entry names, not Electron channel names.
Arguments and results are JSON objects. Optional fields must be omitted when unused, not set to undefined or an unsupported null. Closed schemas reject extra fields. Page limits are 1–100 unless stated otherwise. Opaque cursors come from the previous response and must not be constructed by the app.
For desktop capability calls, a revoked resource grant or expired caller stops waiting and prevents result delivery. Changing an Allow grant to Ask also ends that call. A canceled write can already have reached its target; read current state before an explicit retry. Apps cannot supply the host's invocation context or provider credentials in arguments.
Resource selection
Call resources.list({action, limit?}) to list resource IDs and names on which
the bound app already has Allow. Ask and Off resources are excluded. The result
has resources and complete; an empty list does not prove that no accounts are
connected. There is no separate discovery grant. Use selection rules and limits
before building an account or collection picker.
Website data
Use Apple Calendar reads for calendar.listCalendars and
calendar.listEvents. Declare apple-calendar.read. Select observed calendar
IDs and an explicit time range of at most 31 days. These reads use this desktop's
managed native resource and require system permission as well as app Access.
Use Google Connections and the complete Google API catalog
for the 144 retained native Gmail v1, Drive v3 and Docs v1 methods, with separate Ask
Permissions for reading, editing, deletion, sharing, settings and subscriptions.
The raw sends gmail.users.messages.send and gmail.users.drafts.send are retired.
Use gmail.sendMessage to send new email with the Send email Permission.
Native method calls use named parameters plus body/media and return { data, media }.
The existing bounded helpers are gmail.listMessages,
gmail.getMessage, drive.listFiles and docs.getDocument. These use separate
Google account resources and Ask read actions. Gmail returns metadata only;
Drive returns file metadata; Docs returns bounded plain tab body text.
Use notifications.show({title, body, silent?}) to submit an app-identified
desktop alert. Declare notifications.show; Desktop notifications defaults to
Ask. See notification delivery and limits.
Use Desktop clipboard for explicit plain-text copy/paste actions
with clipboard.readText and clipboard.writeText.
For public API and feed data without browser sign-in, use web.fetch and
web.feed. Use clock.now for the current desktop time and IANA time zones.
See Clock and public data for permissions, examples and limits.
Both views and scripts can call browser.inspect({url, scope?, limit?}) and
browser.extract({recipe, inputs}). Declare browser.read. To click an observed
button and wait for a modal before extraction, call
browser.workflow({recipe, inputs, steps}) and declare browser.interact.
The existing Workspace browser resource controls these operations. See
website commands for the closed recipe schema, bounded
results, origin checks, and the script/skill/schedule workflow. A reusable
website command is an app script, not a dynamically registered capability.
For full browser automation and diagnostics, use browser.openSession,
browser.cdp, browser.events, and browser.closeSession. Declare
browser.debug for page code and network/debugging access. See the
raw Browser tools guide for session ownership, CDP calls,
capture, limits, and a script example.
An MCP server that the owner adds in Connections gives one function for each
approved tool: entry mcp.<slug>.<toolId> with permission
mcp-<slug>.<toolId>. Call it with { connection?, arguments }. See
MCP server functions for names, results, errors and limits.
Collection records
Both views and scripts can call these entries. Use database:<app-id>:<collection-name>, for example database:task-counter:tasks. A short name such as tasks is not the collection connection ID. The app needs the listed action and resource visibility. Another app's connection cannot be obtained by changing the ID string.
| Entry | Kind | Arguments | Result | Action |
|---|---|---|---|---|
database.get | read | connection, id | { record }, nullable | database.read |
database.list | read | connection, optional cursor, limit | { records, nextCursor } | database.read |
database.put | mutate | connection, id, body, expectedRevision, UUID requestKey | Mutation result below | database.write |
database.delete | mutate | connection, id, expectedRevision, UUID requestKey | Mutation result below | database.delete |
A record contains id, revision, body, createdAt, and updatedAt. The body is a scalar-valued object that must match the collection's registered schema. A put replaces the record body; it is not a partial merge. New records use expected revision 0; edits use the revision read from the host. A deleted ID can retain revision metadata, so 0 is not a way to recreate any arbitrary old ID.
Mutation results are { state: 'written', record }, or { state: 'deleted' | 'absent' | 'conflict', revision }. A record conflict is a returned state, not a thrown SDK error. Reload the current data and obtain a new intent before saving a changed payload. Exact retries keep the full original payload, revision, ID and request key.
There is no collection record subscription or general SQL/query API. Use paged list/get reads and refresh the view after a write.
Virtual files
Each VFS mount is a separate Access connection: fs:mount:apps, fs:mount:skills, fs:mount:references, or fs:mount:<custom-name>. Views and scripts use client.cap.call / cap.call. Declare fs.read and/or fs.write; both default to Ask. The host derives the connection from the path; a grant covers that mount. There is no per-folder ACL.
| Entry | Kind | Arguments | Action |
|---|---|---|---|
fs.stat | read | path | fs.read |
fs.search | read | path, literal query, optional limit, caseSensitive | fs.read |
fs.list | read | path, optional cursor, limit | fs.read |
fs.read | read | path, optional encoding, offset, length, expectedRevision | fs.read |
fs.write | mutate | path, content, expectedRevision, requestKey; optional encoding, offset, truncate | fs.write |
fs.mkdir | mutate | path, requestKey; optional parents | fs.write |
fs.rename | mutate | path, to, expectedRevision, requestKey | fs.write |
fs.remove | mutate | path, expectedRevision, requestKey; optional recursive | fs.write |
apps.describe | read | {} | No mount action |
apps.preview | mutate | Source directory path, expectedRevision | fs.read |
apps.load | mutate | Source directory path, expectedRevision | fs.loadApp |
apps.update | mutate | appId, source path, expectedRevision, expectedInstalledRevision | fs.loadApp |
apps.describe returns the static manifest schema, an index of the SDK contracts (the full schemas are in /references/sdk/ files), instructions, and a runnable starter. apps.preview creates a fixed source review in Advanced › Developer › Apps from virtual files. It runs no code. The owner can inspect the source and select Load app or Update app from that review. Chat cannot open app pages before install. apps.load reads and installs the specified source revision through AppCatalog; fs.loadApp defaults to Ask on each mount and covers reading and loading the package. The loaded app gets its own grants and collections, and schedules start paused. See VFS app loading.
Mutations return { state: 'ok' | 'conflict', entry }. Revisions are opaque strings; create-only writes use null. Keep the exact payload and key for a retry. Reads return { entry, encoding, content, nextOffset }; listings return { directory, entries, nextCursor }. No subscription is provided. Examples and limits, contract schemas.
Owner vault
Both views and scripts can call the vault entries when their app declares the action and Access permits it. The Access connection is vault:<vault-id>. Omit vault to use the default vault.
| Entry | Kind | Arguments | Result | Action |
|---|---|---|---|---|
vault.list | read | Optional vault | { vault, items } | vault.see |
vault.save | mutate | Optional vault, item, UUID requestKey | { itemId } | vault.edit |
vault.list returns the id, kind, title and fields of each vault item. A field has its label, secret, and a reference (vault:<vault>/<item>/<field>); the list does not return values. vault.save takes an item with kind, title, optional websites and fields. An app or agent cannot send a secret value (secretValue): the owner sets passwords and keys in Vault. An app or agent also cannot change the websites of a sign-in that exists ("Change the websites of a sign-in in Vault."). Keep the exact payload and key for a retry.
Sign in to a website
A sign-in is a turn of a conversation: there is no sign-in entry. An app that declares ai.use and vault.signIn gets the built-in profile sign-in. Send it the page URL with agent.send({ profile: 'sign-in', model, message: 'Sign in to https://www.costco.com/LogonForm', requestKey }), then read the turn with agent.turn or agent.watch. The turn runs with your app's Access and models. Redac finds the saved sign-in of the website in the vault that Access allows; when it must, it asks the owner to add one or to choose one. Cards for a code, a new password or another value show in the app chat, in Alerts and on the phone (agent.interactions). The tabs share their cookies, so your later browser calls are signed in. The answer of the turn says whether the sign-in worked; it has no value and no reference.
Your own profile can sign in too: list sign_in_start and the session tools (see the AI tool catalog) and declare vault.signIn. sign_in_start opens a session bound to one sign-in in its own tab. While the session is open, the turn's other tools are refused; sign_in_done ends it. A session has at most 30 tool calls and 5 minutes, and a turn opens at most 3 sessions.
Agent and AI
Both pages and scripts can use these APIs for their own app. They talk to the app's agent in app chats: chats with one of the app's profiles, shown in the owner's Chat list in one group for the app. They are internal app APIs with handler-enforced ownership, not an agent connector. AI use still requires ai.use on the selected AI connection. Tools require their declared actions. An app chat uses the app's Access, never Chat's.
| Entry | Kind | Arguments | Result |
|---|---|---|---|
agent.models | read | {} | { connections: [{ connection, label, models: [{ ref, id, name }] }] } |
agent.send | mutate | Manifest profile name, model ref, message, UUID requestKey, optional chatId | { chatId, turnId } |
agent.turn | read | turnId | Turn |
agent.watch | subscribe | turnId | Turn |
agent.stop | mutate | turnId | { state: 'cancel_requested' | 'already_terminal' } |
agent.interactions | subscribe | turnId | { interactions } |
agent.answer | mutate | interactionId, nonempty answer, UUID requestKey | { state: 'answered' } |
agent.chats | read | Optional cursor, limit (at most 100, default 20) | { chats: [{ chatId, title, updatedAt, latestTurn }], nextCursor } |
A turn is one message to the agent and the work until its answer: { chatId, turnId, state, text, error, pendingInteractions }. text is the answer text so far. States are queued, running, completed, failed, cancelled, or interrupted. A send receipt means acceptance, not completion. Watch or read the turn to get the answer. A message is at most 256 KiB. A repeated request key gives the first receipt.
agent.send with no chatId makes a new app chat. With chatId it continues that chat with the same profile; the agent of a chat cannot change. A page can continue the chats that pages made and the chats that the owner wrote in. A script can continue only the chats that the same script run made. One chat has one live turn at a time; a send into a busy chat is refused with unavailable. An app has at most 4 live turns, and all apps together at most 32 (rate_limited). When the app is turned off, its chats refuse new messages ("This app is turned off.").
A script's send is a child of the script run. The run waits for its children before it ends, and a child that does not complete fails the run with child_failed. Stopping the run stops its children, and a child's approvals end at the run's deadline. A child's questions show in the run's pendingInteractions. A run has at most 32 children.
agent.answer answers a question of a turn of this app; a script answers and stops only its own children. It cannot approve an Access request. The owner answers approval cards. agent.turn of a removed chat gives not_found.
The model ref is <connection-id>/<model-id>. Let the user select from agent.models; do not hardcode current model names or provider credentials. The catalog is bundled with Pi, not a live provider list. Public execution requires piiPolicy: 'redact'; known-value redaction does not guarantee removal of unknown sensitive text.
AI tool catalog
A profile's tools list selects host tools. An SDK call such as database.list and an AI tool such as database_list are different interfaces.
| General app tool | Entry or service | Required manifest action |
|---|---|---|
vault_list | vault.list | vault.see |
vault_save | vault.save | vault.edit |
sign_in_start | Opens a sign-in session in its own tab | vault.signIn |
sign_in_read, sign_in_click, sign_in_key, sign_in_fill, sign_in_fill_code, sign_in_ask, sign_in_image, sign_in_click_at, sign_in_type, sign_in_done | The tools of the open session | None (the session checked Access) |
database_get, database_list | Record reads | database.read |
database_put | Record replacement | database.write |
database_delete | Record deletion | database.delete |
fs_stat, fs_list, fs_read | Virtual metadata, directory and byte reads | fs.read |
fs_write, fs_mkdir, fs_rename, fs_remove | Virtual disk changes | fs.write |
apps_describe | Static SDK guide, contract index and starter | None |
apps_preview | Fixed source review for install or update | fs.read |
apps_update | Replace the installed app version, including older code | fs.loadApp |
apps_load | Load source as an installed app | fs.loadApp |
ask_user | Host question interaction | No resource action for the question itself |
Host validation rejects unknown tool names, a tool reserved for another app, or a tool whose action is absent from the manifest. An app profile can use the raw browser tools browser_openSession, browser_cdp,
browser_events and browser_closeSession when the manifest declares browser.debug
(browser_inspect and browser_extract need browser.read; browser_workflow needs
browser.interact). An app profile can also use the scripts_* tools (they need
scripts.run) and the schedules_* tools (they need schedules.manage). Only Chat can use update_plan, cite_sources,
send_file_to_phone and activity_recent. Add host support first if an imported app needs a new effect.
Script and schedule capabilities
Chat, app views, and app agent profiles use the same capability dispatcher.
Declare scripts.run to run a script, read its results, or cancel it. The
resource is scripts:<target-app-id>. Declare schedules.manage to manage
script schedules on schedules:<target-app-id>. The owner app has Allow for
actions it declares. An app with a manifest reaches another app's data only
through its manifest uses field (see
Use another app's data). Access lists
only declared data of another app. Declared database data starts at Ask.
Scripts and schedules of another app start Off in Access. The install review
sets Ask on them when the user keeps the Scripts or Schedules switch on. If the
owner app installs after the declaring app, the declared rows start at their
defaults. Chat keeps its script and schedule rows for other apps; they start
Off. Ask requests approval for each call. These grants never give the target
app more effect permissions.
| Entry | Kind | Arguments |
|---|---|---|
scripts.catalog | read | Optional target appId, cursor, limit (up to 20) |
scripts.run | mutate | appId, definitionRevision, script, input, UUID requestKey |
scripts.get | read | appId, runId |
scripts.list | read | appId, optional cursor, limit |
scripts.watch | subscribe | appId, runId |
scripts.cancel | mutate | appId, runId |
schedules.create | mutate | Target appId, revision, name, cron, zone, script target, UUID key |
schedules.list | read | appId, optional paging and includeRemoved |
schedules.get | read | appId, schedule id, optional occurrence paging |
schedules.update | mutate | appId, id, expectedVersion, definitionRevision, changes |
schedules.remove | mutate | appId, id, expectedVersion |
schedules.setEnabled | mutate | appId, id, expectedVersion, enabled |
Catalog returns public installed definitions, input schemas, and revisions;
it does not grant execution or expose run data. Use client.cap.call for
reads and writes, and client.cap.watch/live for the run subscription.
Script snapshots include initiatorAppId. The target app can read all its
runs. Another app can read or cancel only runs that it started. It can manage
only schedules that it created. Retry keys are scoped to caller and target.
New schedules are paused. Automatic delegated runs need the creator's Run
scripts and Manage schedules grants set to Allow at each admission, and a
live creator app. Script effects use the installed target app's own grants.
Actual scripts cannot start other scripts or manage schedules.
The owner can use the Functions tab of app details for run inputs, results, history, and paused schedule creation. App Access links to the same grant store.
This is contract version 3. The old script platform API and Chat-only tool names are removed. There are no aliases. Update app packages and saved calling skills that use the old API. Existing run history and schedules remain.
Privileged APIs and unavailable surfaces
| Surface | Who may call it |
|---|---|
vfs.mounts, vfs.mount | Owner shell only; apps cannot register mounts |
app.source.* | Owner shell only: review, watch, read, open, layout, stop, install, discard |
scriptRuns.* | Owner shell, not ordinary app views |
scheduleAdmin.*, scheduleOwner.* | Owner shell and explicitly scoped bundled Schedules view; see scheduling |
interactions.pending/resolve | Owner interface for questions and approvals, not an ordinary app client |
Desktop chat.* / conversations.* | Shell Chat interface, not app conversation storage |
| Browser, workspace, settings, update and package administration | Host/shell interfaces, not automatically exposed by default AppClient |
Use client.availability.supports(bridge, name) to detect a served entry, then handle refusal. This is not a permission grant. Type exports or names in the generated API list do not remove runtime caller checks. See PII protection before handling owner data.