> ## Documentation Index
> Fetch the complete documentation index at: https://docs.phoneuse.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Tool reference

> Every tool on the MCP surface, with its scope, annotations and fields.

The surface is 27 tools across 8 scopes. 10 of them are the frozen baseline: they were there from the first release and only leave through a deprecation programme. The rest are additions, marked with the version that introduced them. Additions never change an existing tool, so a client written against the baseline keeps working.

Every tool rejects unknown parameters instead of dropping them, so a misspelled field fails loudly rather than acting without it.

**Billing, purchasing, returning an order, revoking a key and leaving an organisation are not here, and there is no scope for them.** They are on the REST API, and the ones that spend or unwind money also require a signed-in administrator rather than an API key. A tool you cannot find in the table below is not missing from your credential's scopes; it does not exist on this surface.

## Surface at a glance

| Tool | Shown as | Scope | Added in | `readOnlyHint` | `destructiveHint` | `idempotentHint` | `openWorldHint` |
| - | - | - | - | - | - | - | - |
| [`list_app_device_capabilities`](#list_app_device_capabilities) | list app device capabilities | `apps:read + devices:read` | 0.8.0 | true | false | true | false |
| [`get_app_inventory`](#get_app_inventory) | get app inventory | `apps:read + devices:read` | 0.8.0 | true | false | true | false |
| [`refresh_app_inventory`](#refresh_app_inventory) | refresh app inventory | `apps:read + devices:read` | 0.8.0 | false | false | true | false |
| [`get_app_inventory_refresh`](#get_app_inventory_refresh) | get app inventory refresh | `apps:read + devices:read` | 0.8.0 | true | false | true | false |
| [`list_app_inventory_refresh_results`](#list_app_inventory_refresh_results) | list app inventory refresh results | `apps:read + devices:read` | 0.8.0 | true | false | true | false |
| [`get_app_request`](#get_app_request) | get app request | `apps:read` | 0.8.0 | true | false | true | false |
| [`list_devices`](#list_devices) | List your phones | `devices:read` | baseline | true | false | true | false |
| [`acquire_device`](#acquire_device) | Take a phone | `devices:lease` | baseline | false | false | false | false |
| [`renew_lease`](#renew_lease) | Keep the phone longer | `devices:lease` | 0.4.0 | false | false | false | false |
| [`release_device`](#release_device) | Give the phone back | `devices:lease` | baseline | false | false | true | false |
| [`screenshot`](#screenshot) | See the screen | `devices:read` | baseline | true | false | true | false |
| [`ui_snapshot`](#ui_snapshot) | Read the screen's elements | `devices:read` | baseline | true | false | true | false |
| [`tap`](#tap) | Tap | `devices:act` | baseline | false | false | true | false |
| [`swipe`](#swipe) | Swipe | `devices:act` | baseline | false | false | true | false |
| [`type_text`](#type_text) | Type | `devices:act` | baseline | false | false | true | false |
| [`press_key`](#press_key) | Press a key | `devices:act` | baseline | false | false | true | false |
| [`open_app`](#open_app) | Open an app | `devices:act` | baseline | false | false | true | false |
| [`long_press`](#long_press) | Press and hold | `devices:act` | 0.2.0 | false | false | true | false |
| [`wake`](#wake) | Wake the screen | `devices:act` | 0.6.0 | false | false | true | false |
| [`get_device`](#get_device) | Look at one phone | `devices:read` | 0.2.0 | true | false | true | false |
| [`get_receipt`](#get_receipt) | Look up what an action did | `devices:read` | 0.2.0 | true | false | true | false |
| [`start_run`](#start_run) | Start a run | `runs:start` | 0.7.0 | false | true | false | false |
| [`get_run`](#get_run) | Watch a run | `runs:start` | 0.7.0 | true | false | true | false |
| [`list_runs`](#list_runs) | List your runs | `runs:start` | 0.7.0 | true | false | true | false |
| [`cancel_run`](#cancel_run) | Stop a run | `runs:start` | 0.7.0 | false | false | true | false |
| [`resume_run`](#resume_run) | Let a run carry on | `runs:start` | 0.7.0 | false | true | true | false |
| [`pause_run`](#pause_run) | Hold a run | `runs:start` | 0.7.0 | false | false | false | false |

## Scopes

Your API key carries scopes, and a tool outside them is never registered on your session. That means `tools/list` shows exactly what you can call. Calling a tool that exists but needs a scope your key lacks returns an error that names the scope, such as `permission_denied` with "This key needs devices:lease to call acquire\_device". A name that is not a tool is a protocol-level method not found.

| Scope | Tools |
| - | - |
| `devices:read` | `list_app_device_capabilities`, `get_app_inventory`, `refresh_app_inventory`, `get_app_inventory_refresh`, `list_app_inventory_refresh_results`, `list_devices`, `screenshot`, `ui_snapshot`, `get_device`, `get_receipt` |
| `devices:act` | `tap`, `swipe`, `type_text`, `press_key`, `open_app`, `long_press`, `wake` |
| `devices:lease` | `acquire_device`, `renew_lease`, `release_device` |
| `runs:start` | `start_run`, `get_run`, `list_runs`, `cancel_run`, `resume_run`, `pause_run` |
| `orders:place` | |
| `apps:read` | `list_app_device_capabilities`, `get_app_inventory`, `refresh_app_inventory`, `get_app_inventory_refresh`, `list_app_inventory_refresh_results`, `get_app_request` |
| `apps:write` | |
| `apps:delete` | |

`runs:start` also needs `devices:act`. Handing a device to a run is permission to let a loop act for you, not permission to act without saying so, so a key without `devices:act` gets no run tools at all.

## What the annotations mean

Every hint is set explicitly on every tool. Read them literally:

* `title` is the name a client shows a person. The tool name is the protocol handle; this is the product's own word for it, and the table above prints both.
* `destructiveHint` marks the tools that can destroy state you did not create: the run tools hand the device to an autonomous loop. A tap, a swipe, a hold, typing and a key press are NOT marked, because they do what a finger does and a client that prompts on the hint would prompt on every one of them. What keeps a person in the loop is `_meta.requiresUserInteraction`, which no "do not ask again" turns off.
* `idempotentHint` on an action tool reflects the `idempotencyKey` contract: a retry that reuses the key replays the recorded outcome instead of executing twice.
* `openWorldHint` is `false` everywhere. Tools act on your own configured fleet, nothing beyond it.

## Tools

### list\_app\_device\_capabilities

Apps library list app device capabilities. Uses the same permissions and durable request key as REST. A pending or unconfirmed result is not success; query the original key. Never send APK bytes.

**Required scope:** `apps:read + devices:read` · **Added in:** 0.8.0

**Annotations:** `title` "list app device capabilities", `readOnlyHint` true, `destructiveHint` false, `idempotentHint` true, `openWorldHint` false

**Parameters**

| Field | Type | Required | Constraints | Description |
| - | - | - | - | - |
| `cursor` | string | no | - | - |
| `limit` | integer | no | min 1, max 100 | - |
| `q` | string | no | max length 200 | - |
| `deviceId` | string | no | pattern `^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$` | - |

**Result fields** (the `structuredContent` of a successful call)

| Field | Type | Required | Constraints | Description |
| - | - | - | - | - |
| `items` | object\[] | yes | - | - |
| `items[].deviceId` | string | yes | pattern `^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$` | - |
| `items[].deviceName` | string | yes | - | - |
| `items[].platform` | enum | yes | one of `android`, `ios`, `unknown` | - |
| `items[].scope` | object or null | yes | - | - |
| `items[].inventoryRead` | object | yes | - | Authoritative independent operation support. Registration/read metadata only; unknown never permits writes. |
| `items[].install` | object | yes | - | Authoritative independent operation support. Registration/read metadata only; unknown never permits writes. |
| `items[].update` | object | yes | - | Authoritative independent operation support. Registration/read metadata only; unknown never permits writes. |
| `items[].uninstall` | object | yes | - | Authoritative independent operation support. Registration/read metadata only; unknown never permits writes. |
| `items[].updatedAt` | string or null | yes | - | - |
| `nextCursor` | string or null | yes | - | - |
| `snapshotId` | string | yes | - | - |
| `snapshotExpiresAt` | string | yes | - | - |

### get\_app\_inventory

Apps library get app inventory. Uses the same permissions and durable request key as REST. A pending or unconfirmed result is not success; query the original key. Never send APK bytes.

**Required scope:** `apps:read + devices:read` · **Added in:** 0.8.0

**Annotations:** `title` "get app inventory", `readOnlyHint` true, `destructiveHint` false, `idempotentHint` true, `openWorldHint` false

**Parameters**

| Field | Type | Required | Constraints | Description |
| - | - | - | - | - |
| `cursor` | string | no | - | - |
| `limit` | integer | no | min 1, max 100 | - |
| `packageName` | string | no | min length 1, max length 255, pattern `^[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z][A-Za-z0-9_]*)+$` | - |
| `deviceId` | string | no | pattern `^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$` | - |
| `scopeId` | string | no | pattern `^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$` | - |

**Result fields** (the `structuredContent` of a successful call)

| Field | Type | Required | Constraints | Description |
| - | - | - | - | - |
| `items` | object\[] | yes | - | - |
| `items[].inventoryId` | string | yes | pattern `^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$` | - |
| `items[].deviceId` | string | yes | pattern `^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$` | - |
| `items[].scope` | object | yes | - | A stable adapter-verified read/write and actual effect scope. false or absent proof never permits writes. No implicit user 0/current/all. |
| `items[].packageName` | string | yes | min length 1, max length 255, pattern `^[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z][A-Za-z0-9_]*)+$` | - |
| `items[].presence` | enum | yes | one of `present`, `absent`, `unknown` | - |
| `items[].absenceBasis` | enum or null | yes | - | - |
| `items[].app` | object or null | yes | - | - |
| `items[].requestedAt` | string | yes | - | - |
| `items[].receivedAt` | string | yes | - | - |
| `items[].observedAt` | string or null | yes | - | - |
| `items[].completeness` | enum | yes | one of `complete`, `partial`, `unknown` | - |
| `items[].source` | enum | yes | one of `device_package_manager`, `managed_device_service` | - |
| `items[].freshness` | object | yes | - | receivedAt is not observedAt. adapter\_guaranteed\_live requires documented adapter guarantees and bounded acquisition latency; a fast HTTP response is insufficient. |
| `items[].lastRefresh` | object or null | yes | - | - |
| `nextCursor` | string or null | yes | - | - |
| `snapshotId` | string | yes | - | - |
| `snapshotExpiresAt` | string | yes | - | - |

### refresh\_app\_inventory

Apps library refresh app inventory. Uses the same permissions and durable request key as REST. A pending or unconfirmed result is not success; query the original key. Never send APK bytes.

**Required scope:** `apps:read + devices:read` · **Added in:** 0.8.0

**Annotations:** `title` "refresh app inventory", `readOnlyHint` false, `destructiveHint` false, `idempotentHint` true, `openWorldHint` false

**Parameters**

| Field | Type | Required | Constraints | Description |
| - | - | - | - | - |
| `requestKey` | string | yes | min length 1, max length 128, pattern `^(?!\.{1,2}$)[A-Za-z0-9._:-]+$` | Caller-generated unique intent key, retained by client before sending. New library restriction, not a change to the legacy visible-ASCII key contract. |
| `body` | object | yes | - | Explicit finite scope. Omit packageName to collect the full installed-app inventory. Pure read only: never acquire/renew leases, wake/boot devices or run a substitute app action. Unsupported read-only transport returns a per-target reason. |
| `body.packageName` | string | no | min length 1, max length 255, pattern `^[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z][A-Za-z0-9_]*)+$` | - |
| `body.targets` | object\[] | yes | - | - |

**Result fields** (the `structuredContent` of a successful call)

| Field | Type | Required | Constraints | Description |
| - | - | - | - | - |
| `refreshId` | string | yes | pattern `^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$` | - |
| `state` | enum | yes | one of `processing`, `completed`, `partially_completed`, `not_completed` | - |
| `requestedAt` | string | yes | - | - |
| `finishedAt` | string or null | yes | - | - |
| `targetCount` | integer | yes | min 1 | - |
| `completedCount` | integer | yes | min 0 | - |
| `notCompletedCount` | integer | yes | min 0 | - |
| `nextPollAfterSeconds` | integer | yes | min 1 | - |
| `servicePromise` | object | yes | - | Original promisedBy never resets on retry/poll. nextUpdateAt must correspond to a real scheduled update and accountable service; null does not authorize indefinite waiting. |
| `servicePromise.promisedBy` | string or null | yes | - | - |
| `servicePromise.estimatedCompletionAt` | string or null | yes | - | - |
| `servicePromise.nextUpdateAt` | string or null | yes | - | - |
| `servicePromise.delayed` | boolean | yes | - | - |

### get\_app\_inventory\_refresh

Apps library get app inventory refresh. Uses the same permissions and durable request key as REST. A pending or unconfirmed result is not success; query the original key. Never send APK bytes.

**Required scope:** `apps:read + devices:read` · **Added in:** 0.8.0

**Annotations:** `title` "get app inventory refresh", `readOnlyHint` true, `destructiveHint` false, `idempotentHint` true, `openWorldHint` false

**Parameters**

| Field | Type | Required | Constraints | Description |
| - | - | - | - | - |
| `refreshId` | string | yes | pattern `^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$` | - |

**Result fields** (the `structuredContent` of a successful call)

| Field | Type | Required | Constraints | Description |
| - | - | - | - | - |
| `refreshId` | string | yes | pattern `^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$` | - |
| `state` | enum | yes | one of `processing`, `completed`, `partially_completed`, `not_completed` | - |
| `requestedAt` | string | yes | - | - |
| `finishedAt` | string or null | yes | - | - |
| `targetCount` | integer | yes | min 1 | - |
| `completedCount` | integer | yes | min 0 | - |
| `notCompletedCount` | integer | yes | min 0 | - |
| `nextPollAfterSeconds` | integer | yes | min 1 | - |
| `servicePromise` | object | yes | - | Original promisedBy never resets on retry/poll. nextUpdateAt must correspond to a real scheduled update and accountable service; null does not authorize indefinite waiting. |
| `servicePromise.promisedBy` | string or null | yes | - | - |
| `servicePromise.estimatedCompletionAt` | string or null | yes | - | - |
| `servicePromise.nextUpdateAt` | string or null | yes | - | - |
| `servicePromise.delayed` | boolean | yes | - | - |

### list\_app\_inventory\_refresh\_results

Apps library list app inventory refresh results. Uses the same permissions and durable request key as REST. A pending or unconfirmed result is not success; query the original key. Never send APK bytes.

**Required scope:** `apps:read + devices:read` · **Added in:** 0.8.0

**Annotations:** `title` "list app inventory refresh results", `readOnlyHint` true, `destructiveHint` false, `idempotentHint` true, `openWorldHint` false

**Parameters**

| Field | Type | Required | Constraints | Description |
| - | - | - | - | - |
| `refreshId` | string | yes | pattern `^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$` | - |
| `cursor` | string | no | - | - |
| `limit` | integer | no | min 1, max 100 | - |

**Result fields** (the `structuredContent` of a successful call)

| Field | Type | Required | Constraints | Description |
| - | - | - | - | - |
| `items` | object\[] | yes | - | - |
| `items[].deviceId` | string | yes | pattern `^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$` | - |
| `items[].scopeId` | string | yes | pattern `^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$` | - |
| `items[].state` | enum | yes | one of `pending`, `completed`, `not_completed` | - |
| `items[].inventoryId` | string or null | yes | - | - |
| `items[].reason` | enum or null | yes | - | - |
| `items[].completeness` | enum | no | one of `complete`, `partial`, `unknown` | Actual persisted collection completeness; omitted if no successful sample exists. |
| `items[].message` | string or null | yes | - | - |
| `nextCursor` | string or null | yes | - | - |
| `snapshotId` | string | yes | - | - |
| `snapshotExpiresAt` | string | yes | - | - |

### get\_app\_request

Apps library get app request. Uses the same permissions and durable request key as REST. A pending or unconfirmed result is not success; query the original key. Never send APK bytes.

**Required scope:** `apps:read` · **Added in:** 0.8.0

**Annotations:** `title` "get app request", `readOnlyHint` true, `destructiveHint` false, `idempotentHint` true, `openWorldHint` false

**Parameters**

| Field | Type | Required | Constraints | Description |
| - | - | - | - | - |
| `requestKey` | string | yes | min length 1, max length 128, pattern `^(?!\.{1,2}$)[A-Za-z0-9._:-]+$` | Caller-generated unique intent key, retained by client before sending. New library restriction, not a change to the legacy visible-ASCII key contract. |

**Result fields** (the `structuredContent` of a successful call)

| Field | Type | Required | Constraints | Description |
| - | - | - | - | - |
| `requestKey` | string | yes | min length 1, max length 128, pattern `^(?!\.{1,2}$)[A-Za-z0-9._:-]+$` | Caller-generated unique intent key, retained by client before sending. New library restriction, not a change to the legacy visible-ASCII key contract. |
| `operationId` | string | yes | - | - |
| `state` | enum | yes | one of `processing`, `completed`, `not_completed` | - |
| `resourceType` | enum or null | yes | - | - |
| `resourceId` | string or null | yes | - | - |
| `resourcePath` | string or null | yes | - | - |
| `originalStatus` | integer or null | yes | - | - |
| `createdAt` | string | yes | - | - |
| `updatedAt` | string | yes | - | - |
| `retainedUntil` | string | yes | - | - |
| `error` | object or null | yes | - | - |

### list\_devices

Enumerate every reachable device with its per-device capabilities. Consult capabilities before calling action/observe tools, backends differ (a robot hand has camera frames but no UI tree).

**Required scope:** `devices:read`

**Annotations:** `title` "List your phones", `readOnlyHint` true, `destructiveHint` false, `idempotentHint` true, `openWorldHint` false

**Parameters**

This tool takes no parameters.

**Result fields** (the `structuredContent` of a successful call)

| Field | Type | Required | Constraints | Description |
| - | - | - | - | - |
| `devices` | object\[] | yes | - | - |
| `devices[].deviceId` | string | yes | - | - |
| `devices[].backendKind` | enum | yes | one of `adb_cloud`, `adb_real`, `emulator`, `robot_hand`, `esp32_hid`, `byod_portal` | - |
| `devices[].kind` | enum | yes | one of `emulator`, `cloud_phone`, `physical`, `robot` | - |
| `devices[].status` | enum | yes | one of `online`, `offline`, `unauthorized`, `unknown` | - |
| `devices[].capabilities` | object | yes | discriminated by `fidelityTier`: `"software"`, `"hid"`, `"physical"` | - |
| `devices[].metadata` | object | yes | arbitrary keys | - |
| `devices[].displayName` | string | no | - | - |
| `devices[].info` | object | no | - | - |
| `devices[].staffHold` | object | no | - | Present while our staff hold this device (a calibration, maintenance, a check): acquire\_device will be refused with this reason, hint and retryable. Absent when nothing holds it, and when holdsUnknown is true (then it is not known). |
| `holdsUnknown` | literal | no | always `true` | Present, and true, only when we could not check which devices our staff hold right now. The devices are listed as normal, but a missing staffHold then means not known rather than not held; acquire\_device still refuses a held device. Absent when the check ran, and when nothing here can be held (only a robot arm can). |

### acquire\_device

Take a device for your organization before acting on it. Optional: any gesture tool takes the device for you when your organization does not hold it. Returns the lease (leaseId + expiry); keep leaseId to give the device back with release\_device. A lease expires on its own after 10 minutes unless renewed.

**Required scope:** `devices:lease`

**Annotations:** `title` "Take a phone", `readOnlyHint` false, `destructiveHint` false, `idempotentHint` false, `openWorldHint` false

**Parameters**

| Field | Type | Required | Constraints | Description |
| - | - | - | - | - |
| `deviceId` | string | yes | min length 1 | Device id from list\_devices. |

**Result fields** (the `structuredContent` of a successful call)

| Field | Type | Required | Constraints | Description |
| - | - | - | - | - |
| `orgId` | string | yes | - | - |
| `deviceId` | string | yes | - | - |
| `leaseId` | string | yes | - | - |
| `fencingToken` | integer | yes | - | - |
| `acquiredAt` | string | yes | - | - |
| `expiresAt` | string | yes | - | - |

### renew\_lease

Extend the lease your organization holds on a device, so work longer than one lease can keep its device. leaseId is optional; if supplied it must match the current lease. The lease may be one a colleague or a run took. Slides the expiry to ten minutes from now (it does not add ten minutes to the old expiry), and leaves leaseId, fencingToken and acquiredAt unchanged. Renew before the lease expires: an expired lease cannot be renewed, because by then the device is available to everyone else, and you have to take it again.

**Required scope:** `devices:lease` · **Added in:** 0.4.0

**Annotations:** `title` "Keep the phone longer", `readOnlyHint` false, `destructiveHint` false, `idempotentHint` false, `openWorldHint` false

**Parameters**

| Field | Type | Required | Constraints | Description |
| - | - | - | - | - |
| `deviceId` | string | yes | min length 1 | Device id from list\_devices. |
| `leaseId` | string | no | min length 1 | Optional. Lease id from acquire\_device. Leave it out: the action runs under the lease your organization holds on the device, and a gesture takes the device for you when it holds none. |

**Result fields** (the `structuredContent` of a successful call)

| Field | Type | Required | Constraints | Description |
| - | - | - | - | - |
| `orgId` | string | yes | - | - |
| `deviceId` | string | yes | - | - |
| `leaseId` | string | yes | - | Unchanged: the same lease continues, so keep using this id. |
| `fencingToken` | integer | yes | - | Unchanged by a renewal: the holder generation is the same. |
| `acquiredAt` | string | yes | - | Unchanged: the original acquire, which is what billing integrates from. |
| `expiresAt` | string | yes | - | The NEW expiry, ten minutes from the moment the renewal was granted. |

### release\_device

Give a device back, by the lease id acquire\_device returned. This tool requires the id; other actions accept an omitted id but reject a stale one. An id that is not the current one is refused with lease\_mismatch.

**Required scope:** `devices:lease`

**Annotations:** `title` "Give the phone back", `readOnlyHint` false, `destructiveHint` false, `idempotentHint` true, `openWorldHint` false

**Parameters**

| Field | Type | Required | Constraints | Description |
| - | - | - | - | - |
| `deviceId` | string | yes | min length 1 | Device id from list\_devices. |
| `leaseId` | string | yes | min length 1 | The lease id acquire\_device returned. Required. |

**Result fields** (the `structuredContent` of a successful call)

| Field | Type | Required | Constraints | Description |
| - | - | - | - | - |
| `ok` | literal | yes | always `true` | - |
| `deviceId` | string | yes | - | - |

### screenshot

Capture the device screen. Returns the image plus its pixelSize and, when your organization holds the device, a frameId; aim taps with the 0-1000 grid, and pass frameId as observedFrame on the next action so a stale frame is refused instead of mis-tapped. `others` says what anybody else did on the device since the previous frame. On a device your organization does not hold this is a watch: you get the image and no frame, and nothing is taken or billed.

**Required scope:** `devices:read`

**Annotations:** `title` "See the screen", `readOnlyHint` true, `destructiveHint` false, `idempotentHint` true, `openWorldHint` false

**Parameters**

| Field | Type | Required | Constraints | Description |
| - | - | - | - | - |
| `deviceId` | string | yes | min length 1 | Device id from list\_devices. |
| `leaseId` | string | no | min length 1 | Optional. Lease id from acquire\_device. Leave it out: the action runs under the lease your organization holds on the device, and a gesture takes the device for you when it holds none. |

**Result fields** (the `structuredContent` of a successful call)

| Field | Type | Required | Constraints | Description |
| - | - | - | - | - |
| `format` | enum | yes | one of `png`, `jpeg` | - |
| `source` | enum | yes | one of `framebuffer`, `camera` | - |
| `pixelSize` | object | yes | - | Raw frame geometry in device/image pixels, informational; aim actions with the 0-1000 grid. |
| `pixelSize.width` | integer | yes | - | - |
| `pixelSize.height` | integer | yes | - | - |
| `frameId` | string | no | - | Pass as observedFrame on the next action to bind it to this frame. Absent when your organization does not hold the device (a watch mints no frame). |
| `sequence` | integer | no | - | - |
| `capturedAt` | string | no | - | - |
| `others` | object | no | - | What everybody else did on this device since you last looked. Present on every action result and every leased screenshot. |
| `others.actions` | integer | yes | - | How many actions principals other than you took on this device since your anchor. Pending and committed receipts count; failed ones do not. |
| `others.lastAt` | string or null | yes | - | When the newest of them was requested, ISO-8601 on the control-plane clock. Null when actions is 0. |
| `others.by` | enum\[] | yes | each: one of `person`, `agent` | Who they were: person (a signed-in person acting directly) and/or agent (an API key, a copilot, or a run). Each at most once, person first. |

### ui\_snapshot

Structured element tree of the current screen. All bounds inside the tree are already in the 0-1000 grid, coordinates read here can be tapped directly.

**Required scope:** `devices:read`

**Annotations:** `title` "Read the screen's elements", `readOnlyHint` true, `destructiveHint` false, `idempotentHint` true, `openWorldHint` false

**Parameters**

| Field | Type | Required | Constraints | Description |
| - | - | - | - | - |
| `deviceId` | string | yes | min length 1 | Device id from list\_devices. |
| `leaseId` | string | no | min length 1 | Optional. Lease id from acquire\_device. Leave it out: the action runs under the lease your organization holds on the device, and a gesture takes the device for you when it holds none. |

**Result fields** (the `structuredContent` of a successful call)

| Field | Type | Required | Constraints | Description |
| - | - | - | - | - |
| `tree` | string | yes | - | - |
| `format` | enum | yes | one of `xml`, `json` | - |
| `source` | enum | yes | one of `uiautomator`, `a11y` | - |
| `pixelSize` | object | yes | - | Raw frame geometry in device/image pixels, informational; aim actions with the 0-1000 grid. |
| `pixelSize.width` | integer | yes | - | - |
| `pixelSize.height` | integer | yes | - | - |

### tap

Tap at (x, y) in the 0-1000 grid. Returns the ActionOutcome: committed means the device applied it (a robot arm has physically finished and returned to hover). Every action result also carries `others`, what other principals did on the device since your anchor; a person may drive the same device beside you, and you are informed rather than stopped.

**Required scope:** `devices:act`

**Annotations:** `title` "Tap", `readOnlyHint` false, `destructiveHint` false, `idempotentHint` true, `openWorldHint` false

**Parameters**

| Field | Type | Required | Constraints | Description |
| - | - | - | - | - |
| `deviceId` | string | yes | min length 1 | Device id from list\_devices. |
| `x` | number | yes | min 0, max 1000 | Tap x in the normalized 0-1000 grid (origin top-left, current orientation). |
| `y` | number | yes | min 0, max 1000 | Tap y in the normalized 0-1000 grid (origin top-left, current orientation). |
| `leaseId` | string | no | min length 1 | Optional. Lease id from acquire\_device. Leave it out: the action runs under the lease your organization holds on the device, and a gesture takes the device for you when it holds none. |
| `idempotencyKey` | string | no | pattern `^[\x20-\x7e]{1,128}$` | Client-generated request id (1-128 printable ASCII, must not start with "agent:"). Reuse the SAME value when retrying the SAME intended action; the recorded outcome is replayed instead of double-executing. |
| `observedFrame` | string | no | min length 1 | frameId from the screenshot you decided on. The action is refused (stale\_frame) if that frame is no longer current, re-observe and decide again. |
| `refuseIfOthersActed` | boolean | no | - | Refuse this action (stale\_frame, reason others\_acted) if another principal acted on the device after observedFrame was taken. Needs observedFrame. Default: on when you send observedFrame from an API key, a copilot or a run; off for a person acting directly, and off whenever no observedFrame is sent. You are told through `others` either way. |

**Result fields** (the `structuredContent` of a successful call)

| Field | Type | Required | Constraints | Description |
| - | - | - | - | - |
| `status` | literal | yes | always `"committed"` | - |
| `committedAt` | string | yes | - | ISO-8601, control-plane clock, taken when the backend confirmed device commit. |
| `idempotencyKey` | string | no | - | The end-to-end key this action was deduplicated under. |
| `deduplicated` | boolean | no | - | True when replayed from the receipt ledger without re-executing. |
| `others` | object | yes | - | What everybody else did on this device since you last looked. Present on every action result and every leased screenshot. |
| `others.actions` | integer | yes | - | How many actions principals other than you took on this device since your anchor. Pending and committed receipts count; failed ones do not. |
| `others.lastAt` | string or null | yes | - | When the newest of them was requested, ISO-8601 on the control-plane clock. Null when actions is 0. |
| `others.by` | enum\[] | yes | each: one of `person`, `agent` | Who they were: person (a signed-in person acting directly) and/or agent (an API key, a copilot, or a run). Each at most once, person first. |
| `frameNotes` | object | no | - | What a robot arm noticed about the frame this action was bound to. It acts anyway and reports here; you decide whether to take a new screenshot. Present only on a robot-driven phone that reports it. |
| `frameNotes.ageMs` | number | no | - | Milliseconds between the bound frame's capture and the action reaching the arm. |
| `frameNotes.newerFrames` | number | no | - | How many frames the arm's camera captured after the bound one. |
| `frameNotes.edgeMovedSince` | boolean | no | - | True when the arm moved after the bound frame for a reason that did not come through phonebase. Look again. |
| `frameNotes.settled` | boolean | no | - | False when the bound frame was taken before the screen settled after the arm's last move. |

### swipe

Swipe between two points in the 0-1000 grid over durationMs milliseconds (default 300).

**Required scope:** `devices:act`

**Annotations:** `title` "Swipe", `readOnlyHint` false, `destructiveHint` false, `idempotentHint` true, `openWorldHint` false

**Parameters**

| Field | Type | Required | Constraints | Description |
| - | - | - | - | - |
| `deviceId` | string | yes | min length 1 | Device id from list\_devices. |
| `fromX` | number | yes | min 0, max 1000 | Start x in the normalized 0-1000 grid (origin top-left, current orientation). |
| `fromY` | number | yes | min 0, max 1000 | Start y in the normalized 0-1000 grid (origin top-left, current orientation). |
| `toX` | number | yes | min 0, max 1000 | End x in the normalized 0-1000 grid (origin top-left, current orientation). |
| `toY` | number | yes | min 0, max 1000 | End y in the normalized 0-1000 grid (origin top-left, current orientation). |
| `durationMs` | integer | no | min 1, max 10000 | Gesture duration in milliseconds (default 300, max 10000). |
| `leaseId` | string | no | min length 1 | Optional. Lease id from acquire\_device. Leave it out: the action runs under the lease your organization holds on the device, and a gesture takes the device for you when it holds none. |
| `idempotencyKey` | string | no | pattern `^[\x20-\x7e]{1,128}$` | Client-generated request id (1-128 printable ASCII, must not start with "agent:"). Reuse the SAME value when retrying the SAME intended action; the recorded outcome is replayed instead of double-executing. |
| `observedFrame` | string | no | min length 1 | frameId from the screenshot you decided on. The action is refused (stale\_frame) if that frame is no longer current, re-observe and decide again. |
| `refuseIfOthersActed` | boolean | no | - | Refuse this action (stale\_frame, reason others\_acted) if another principal acted on the device after observedFrame was taken. Needs observedFrame. Default: on when you send observedFrame from an API key, a copilot or a run; off for a person acting directly, and off whenever no observedFrame is sent. You are told through `others` either way. |

**Result fields** (the `structuredContent` of a successful call)

| Field | Type | Required | Constraints | Description |
| - | - | - | - | - |
| `status` | literal | yes | always `"committed"` | - |
| `committedAt` | string | yes | - | ISO-8601, control-plane clock, taken when the backend confirmed device commit. |
| `idempotencyKey` | string | no | - | The end-to-end key this action was deduplicated under. |
| `deduplicated` | boolean | no | - | True when replayed from the receipt ledger without re-executing. |
| `others` | object | yes | - | What everybody else did on this device since you last looked. Present on every action result and every leased screenshot. |
| `others.actions` | integer | yes | - | How many actions principals other than you took on this device since your anchor. Pending and committed receipts count; failed ones do not. |
| `others.lastAt` | string or null | yes | - | When the newest of them was requested, ISO-8601 on the control-plane clock. Null when actions is 0. |
| `others.by` | enum\[] | yes | each: one of `person`, `agent` | Who they were: person (a signed-in person acting directly) and/or agent (an API key, a copilot, or a run). Each at most once, person first. |
| `frameNotes` | object | no | - | What a robot arm noticed about the frame this action was bound to. It acts anyway and reports here; you decide whether to take a new screenshot. Present only on a robot-driven phone that reports it. |
| `frameNotes.ageMs` | number | no | - | Milliseconds between the bound frame's capture and the action reaching the arm. |
| `frameNotes.newerFrames` | number | no | - | How many frames the arm's camera captured after the bound one. |
| `frameNotes.edgeMovedSince` | boolean | no | - | True when the arm moved after the bound frame for a reason that did not come through phonebase. Look again. |
| `frameNotes.settled` | boolean | no | - | False when the bound frame was taken before the screen settled after the arm's last move. |

### type\_text

Type into the focused input (focus one first, e.g. by tapping a search field).

**Required scope:** `devices:act`

**Annotations:** `title` "Type", `readOnlyHint` false, `destructiveHint` false, `idempotentHint` true, `openWorldHint` false

**Parameters**

| Field | Type | Required | Constraints | Description |
| - | - | - | - | - |
| `deviceId` | string | yes | min length 1 | Device id from list\_devices. |
| `text` | string | yes | max length 4096 | Text for the focused input (max 4096 chars). Backends may reject characters they cannot inject. |
| `leaseId` | string | no | min length 1 | Optional. Lease id from acquire\_device. Leave it out: the action runs under the lease your organization holds on the device, and a gesture takes the device for you when it holds none. |
| `idempotencyKey` | string | no | pattern `^[\x20-\x7e]{1,128}$` | Client-generated request id (1-128 printable ASCII, must not start with "agent:"). Reuse the SAME value when retrying the SAME intended action; the recorded outcome is replayed instead of double-executing. |
| `observedFrame` | string | no | min length 1 | frameId from the screenshot you decided on. The action is refused (stale\_frame) if that frame is no longer current, re-observe and decide again. |
| `refuseIfOthersActed` | boolean | no | - | Refuse this action (stale\_frame, reason others\_acted) if another principal acted on the device after observedFrame was taken. Needs observedFrame. Default: on when you send observedFrame from an API key, a copilot or a run; off for a person acting directly, and off whenever no observedFrame is sent. You are told through `others` either way. |

**Result fields** (the `structuredContent` of a successful call)

| Field | Type | Required | Constraints | Description |
| - | - | - | - | - |
| `status` | literal | yes | always `"committed"` | - |
| `committedAt` | string | yes | - | ISO-8601, control-plane clock, taken when the backend confirmed device commit. |
| `idempotencyKey` | string | no | - | The end-to-end key this action was deduplicated under. |
| `deduplicated` | boolean | no | - | True when replayed from the receipt ledger without re-executing. |
| `others` | object | yes | - | What everybody else did on this device since you last looked. Present on every action result and every leased screenshot. |
| `others.actions` | integer | yes | - | How many actions principals other than you took on this device since your anchor. Pending and committed receipts count; failed ones do not. |
| `others.lastAt` | string or null | yes | - | When the newest of them was requested, ISO-8601 on the control-plane clock. Null when actions is 0. |
| `others.by` | enum\[] | yes | each: one of `person`, `agent` | Who they were: person (a signed-in person acting directly) and/or agent (an API key, a copilot, or a run). Each at most once, person first. |
| `frameNotes` | object | no | - | What a robot arm noticed about the frame this action was bound to. It acts anyway and reports here; you decide whether to take a new screenshot. Present only on a robot-driven phone that reports it. |
| `frameNotes.ageMs` | number | no | - | Milliseconds between the bound frame's capture and the action reaching the arm. |
| `frameNotes.newerFrames` | number | no | - | How many frames the arm's camera captured after the bound one. |
| `frameNotes.edgeMovedSince` | boolean | no | - | True when the arm moved after the bound frame for a reason that did not come through phonebase. Look again. |
| `frameNotes.settled` | boolean | no | - | False when the bound frame was taken before the screen settled after the arm's last move. |

### press\_key

Press a hardware or navigation key. The keys this tool accepts are the widest set any platform supports; a device supports a subset, published in its list\_devices capabilities.

**Required scope:** `devices:act`

**Annotations:** `title` "Press a key", `readOnlyHint` false, `destructiveHint` false, `idempotentHint` true, `openWorldHint` false

**Parameters**

| Field | Type | Required | Constraints | Description |
| - | - | - | - | - |
| `deviceId` | string | yes | min length 1 | Device id from list\_devices. |
| `key` | enum | yes | one of `home`, `back`, `enter`, `delete`, `app_switch`, `power`, `volume_up`, `volume_down` | Key to press. Check the device's capabilities for its supported subset. |
| `leaseId` | string | no | min length 1 | Optional. Lease id from acquire\_device. Leave it out: the action runs under the lease your organization holds on the device, and a gesture takes the device for you when it holds none. |
| `idempotencyKey` | string | no | pattern `^[\x20-\x7e]{1,128}$` | Client-generated request id (1-128 printable ASCII, must not start with "agent:"). Reuse the SAME value when retrying the SAME intended action; the recorded outcome is replayed instead of double-executing. |
| `observedFrame` | string | no | min length 1 | frameId from the screenshot you decided on. The action is refused (stale\_frame) if that frame is no longer current, re-observe and decide again. |
| `refuseIfOthersActed` | boolean | no | - | Refuse this action (stale\_frame, reason others\_acted) if another principal acted on the device after observedFrame was taken. Needs observedFrame. Default: on when you send observedFrame from an API key, a copilot or a run; off for a person acting directly, and off whenever no observedFrame is sent. You are told through `others` either way. |

**Result fields** (the `structuredContent` of a successful call)

| Field | Type | Required | Constraints | Description |
| - | - | - | - | - |
| `status` | literal | yes | always `"committed"` | - |
| `committedAt` | string | yes | - | ISO-8601, control-plane clock, taken when the backend confirmed device commit. |
| `idempotencyKey` | string | no | - | The end-to-end key this action was deduplicated under. |
| `deduplicated` | boolean | no | - | True when replayed from the receipt ledger without re-executing. |
| `others` | object | yes | - | What everybody else did on this device since you last looked. Present on every action result and every leased screenshot. |
| `others.actions` | integer | yes | - | How many actions principals other than you took on this device since your anchor. Pending and committed receipts count; failed ones do not. |
| `others.lastAt` | string or null | yes | - | When the newest of them was requested, ISO-8601 on the control-plane clock. Null when actions is 0. |
| `others.by` | enum\[] | yes | each: one of `person`, `agent` | Who they were: person (a signed-in person acting directly) and/or agent (an API key, a copilot, or a run). Each at most once, person first. |
| `frameNotes` | object | no | - | What a robot arm noticed about the frame this action was bound to. It acts anyway and reports here; you decide whether to take a new screenshot. Present only on a robot-driven phone that reports it. |
| `frameNotes.ageMs` | number | no | - | Milliseconds between the bound frame's capture and the action reaching the arm. |
| `frameNotes.newerFrames` | number | no | - | How many frames the arm's camera captured after the bound one. |
| `frameNotes.edgeMovedSince` | boolean | no | - | True when the arm moved after the bound frame for a reason that did not come through phonebase. Look again. |
| `frameNotes.settled` | boolean | no | - | False when the bound frame was taken before the screen settled after the arm's last move. |

### open\_app

Open an app by OS-level id (Android package name / iOS bundle id).

**Required scope:** `devices:act`

**Annotations:** `title` "Open an app", `readOnlyHint` false, `destructiveHint` false, `idempotentHint` true, `openWorldHint` false

**Parameters**

| Field | Type | Required | Constraints | Description |
| - | - | - | - | - |
| `deviceId` | string | yes | min length 1 | Device id from list\_devices. |
| `appId` | string | yes | min length 1 | OS-level app id (Android package name / iOS bundle id). |
| `leaseId` | string | no | min length 1 | Optional. Lease id from acquire\_device. Leave it out: the action runs under the lease your organization holds on the device, and a gesture takes the device for you when it holds none. |
| `idempotencyKey` | string | no | pattern `^[\x20-\x7e]{1,128}$` | Client-generated request id (1-128 printable ASCII, must not start with "agent:"). Reuse the SAME value when retrying the SAME intended action; the recorded outcome is replayed instead of double-executing. |
| `observedFrame` | string | no | min length 1 | frameId from the screenshot you decided on. The action is refused (stale\_frame) if that frame is no longer current, re-observe and decide again. |
| `refuseIfOthersActed` | boolean | no | - | Refuse this action (stale\_frame, reason others\_acted) if another principal acted on the device after observedFrame was taken. Needs observedFrame. Default: on when you send observedFrame from an API key, a copilot or a run; off for a person acting directly, and off whenever no observedFrame is sent. You are told through `others` either way. |

**Result fields** (the `structuredContent` of a successful call)

| Field | Type | Required | Constraints | Description |
| - | - | - | - | - |
| `status` | literal | yes | always `"committed"` | - |
| `committedAt` | string | yes | - | ISO-8601, control-plane clock, taken when the backend confirmed device commit. |
| `idempotencyKey` | string | no | - | The end-to-end key this action was deduplicated under. |
| `deduplicated` | boolean | no | - | True when replayed from the receipt ledger without re-executing. |
| `others` | object | yes | - | What everybody else did on this device since you last looked. Present on every action result and every leased screenshot. |
| `others.actions` | integer | yes | - | How many actions principals other than you took on this device since your anchor. Pending and committed receipts count; failed ones do not. |
| `others.lastAt` | string or null | yes | - | When the newest of them was requested, ISO-8601 on the control-plane clock. Null when actions is 0. |
| `others.by` | enum\[] | yes | each: one of `person`, `agent` | Who they were: person (a signed-in person acting directly) and/or agent (an API key, a copilot, or a run). Each at most once, person first. |
| `frameNotes` | object | no | - | What a robot arm noticed about the frame this action was bound to. It acts anyway and reports here; you decide whether to take a new screenshot. Present only on a robot-driven phone that reports it. |
| `frameNotes.ageMs` | number | no | - | Milliseconds between the bound frame's capture and the action reaching the arm. |
| `frameNotes.newerFrames` | number | no | - | How many frames the arm's camera captured after the bound one. |
| `frameNotes.edgeMovedSince` | boolean | no | - | True when the arm moved after the bound frame for a reason that did not come through phonebase. Look again. |
| `frameNotes.settled` | boolean | no | - | False when the bound frame was taken before the screen settled after the arm's last move. |

### long\_press

Press and hold at (x, y) in the 0-1000 grid for durationMs (default 800), returning the ActionOutcome like tap. Only on a device whose capabilities say actions.longPress: a backend that cannot hold (a robot arm reads the duration as travel time) refuses with capability\_unsupported rather than landing a brief press under this name.

**Required scope:** `devices:act` · **Added in:** 0.2.0

**Annotations:** `title` "Press and hold", `readOnlyHint` false, `destructiveHint` false, `idempotentHint` true, `openWorldHint` false

**Parameters**

| Field | Type | Required | Constraints | Description |
| - | - | - | - | - |
| `deviceId` | string | yes | min length 1 | Device id from list\_devices. |
| `x` | number | yes | min 0, max 1000 | Press x in the normalized 0-1000 grid (origin top-left, current orientation). |
| `y` | number | yes | min 0, max 1000 | Press y in the normalized 0-1000 grid (origin top-left, current orientation). |
| `durationMs` | integer | no | min 200, max 10000 | Hold duration in milliseconds (default 800, min 200, shorter is a tap, max 10000). |
| `leaseId` | string | no | min length 1 | Optional. Lease id from acquire\_device. Leave it out: the action runs under the lease your organization holds on the device, and a gesture takes the device for you when it holds none. |
| `idempotencyKey` | string | no | pattern `^[\x20-\x7e]{1,128}$` | Client-generated request id (1-128 printable ASCII, must not start with "agent:"). Reuse the SAME value when retrying the SAME intended action; the recorded outcome is replayed instead of double-executing. |
| `observedFrame` | string | no | min length 1 | frameId from the screenshot you decided on. The action is refused (stale\_frame) if that frame is no longer current, re-observe and decide again. |
| `refuseIfOthersActed` | boolean | no | - | Refuse this action (stale\_frame, reason others\_acted) if another principal acted on the device after observedFrame was taken. Needs observedFrame. Default: on when you send observedFrame from an API key, a copilot or a run; off for a person acting directly, and off whenever no observedFrame is sent. You are told through `others` either way. |

**Result fields** (the `structuredContent` of a successful call)

| Field | Type | Required | Constraints | Description |
| - | - | - | - | - |
| `status` | literal | yes | always `"committed"` | - |
| `committedAt` | string | yes | - | ISO-8601, control-plane clock, taken when the backend confirmed device commit. |
| `idempotencyKey` | string | no | - | The end-to-end key this action was deduplicated under. |
| `deduplicated` | boolean | no | - | True when replayed from the receipt ledger without re-executing. |
| `others` | object | yes | - | What everybody else did on this device since you last looked. Present on every action result and every leased screenshot. |
| `others.actions` | integer | yes | - | How many actions principals other than you took on this device since your anchor. Pending and committed receipts count; failed ones do not. |
| `others.lastAt` | string or null | yes | - | When the newest of them was requested, ISO-8601 on the control-plane clock. Null when actions is 0. |
| `others.by` | enum\[] | yes | each: one of `person`, `agent` | Who they were: person (a signed-in person acting directly) and/or agent (an API key, a copilot, or a run). Each at most once, person first. |
| `frameNotes` | object | no | - | What a robot arm noticed about the frame this action was bound to. It acts anyway and reports here; you decide whether to take a new screenshot. Present only on a robot-driven phone that reports it. |
| `frameNotes.ageMs` | number | no | - | Milliseconds between the bound frame's capture and the action reaching the arm. |
| `frameNotes.newerFrames` | number | no | - | How many frames the arm's camera captured after the bound one. |
| `frameNotes.edgeMovedSince` | boolean | no | - | True when the arm moved after the bound frame for a reason that did not come through phonebase. Look again. |
| `frameNotes.settled` | boolean | no | - | False when the bound frame was taken before the screen settled after the arm's last move. |

### wake

Turn the device's screen on and drop its lock screen. Call it when a screenshot comes back black or a live video session shows nothing: a phone whose screen is off renders nothing until it is woken, and this is the ONLY way to wake one: press\_key's `power` is a toggle, as likely to put an awake phone to sleep. Works where the device's capabilities say actions.wake: a cloud phone, an emulator or a USB Android handset; a robot-driven phone refuses with capability\_unsupported. Safe to repeat: waking an awake phone does nothing. Best-effort, so `accepted` means the device was asked, not that the screen is on; take a screenshot to see. Writes no receipt, unlike the gesture tools.

**Required scope:** `devices:act` · **Added in:** 0.6.0

**Annotations:** `title` "Wake the screen", `readOnlyHint` false, `destructiveHint` false, `idempotentHint` true, `openWorldHint` false

**Parameters**

| Field | Type | Required | Constraints | Description |
| - | - | - | - | - |
| `deviceId` | string | yes | min length 1 | Device id from list\_devices. |
| `leaseId` | string | no | min length 1 | Optional. Lease id from acquire\_device. Leave it out: the action runs under the lease your organization holds on the device, and a gesture takes the device for you when it holds none. |

**Result fields** (the `structuredContent` of a successful call)

| Field | Type | Required | Constraints | Description |
| - | - | - | - | - |
| `accepted` | literal | yes | always `true` | The wake was dispatched to the device. It does NOT mean the screen is on: take a screenshot to see. |

### get\_device

One device's current status and capabilities without enumerating the whole fleet.

**Required scope:** `devices:read` · **Added in:** 0.2.0

**Annotations:** `title` "Look at one phone", `readOnlyHint` true, `destructiveHint` false, `idempotentHint` true, `openWorldHint` false

**Parameters**

| Field | Type | Required | Constraints | Description |
| - | - | - | - | - |
| `deviceId` | string | yes | min length 1 | Device id from list\_devices. |

**Result fields** (the `structuredContent` of a successful call)

| Field | Type | Required | Constraints | Description |
| - | - | - | - | - |
| `deviceId` | string | yes | - | - |
| `backendKind` | enum | yes | one of `adb_cloud`, `adb_real`, `emulator`, `robot_hand`, `esp32_hid`, `byod_portal` | - |
| `kind` | enum | yes | one of `emulator`, `cloud_phone`, `physical`, `robot` | - |
| `status` | enum | yes | one of `online`, `offline`, `unauthorized`, `unknown` | - |
| `capabilities` | object | yes | discriminated by `fidelityTier`: `"software"`, `"hid"`, `"physical"` | - |
| `metadata` | object | yes | arbitrary keys | - |
| `displayName` | string | no | - | - |
| `info` | object | no | - | - |
| `info.model` | string | no | - | - |
| `info.androidVersion` | string | no | - | - |
| `info.osVersion` | string | no | - | - |
| `info.resolution` | object | no | - | - |
| `info.carrier` | string or null | no | - | - |
| `staffHold` | object | no | - | Present while our staff hold this device (a calibration, maintenance, a check): acquire\_device will be refused with this reason, hint and retryable. Absent when nothing holds it, and when holdsUnknown is true (then it is not known). |
| `staffHold.reason` | literal | yes | always `"leased"` | The reason acquire\_device is refused with while this hold lasts. |
| `staffHold.retryable` | boolean | yes | - | Whether trying again later can succeed. |
| `staffHold.hint` | string | yes | - | What is going on, in words to show a person. The same hint the refusal carries. |
| `holdsUnknown` | literal | no | always `true` | Present, and true, only when we could not check which devices our staff hold right now. The devices are listed as normal, but a missing staffHold then means not known rather than not held; acquire\_device still refuses a held device. Absent when the check ran, and when nothing here can be held (only a robot arm can). |

### get\_receipt

Look up the receipt for an action by its idempotencyKey, the recovery path when an action's response was lost (transport timeout, stateless HTTP has no progress channel). found:false means the action never began and the SAME key is safe to retry. This answers about ONE key you already hold; to ENUMERATE recent receipts use GET /v1/receipts on the REST API.

**Required scope:** `devices:read` · **Added in:** 0.2.0

**Annotations:** `title` "Look up what an action did", `readOnlyHint` true, `destructiveHint` false, `idempotentHint` true, `openWorldHint` false

**Parameters**

| Field | Type | Required | Constraints | Description |
| - | - | - | - | - |
| `deviceId` | string | yes | min length 1 | Device id from list\_devices. |
| `idempotencyKey` | string | yes | pattern `^[\x20-\x7e]{1,128}$` | The idempotencyKey the action was sent with (1-128 printable ASCII). Required. |

**Result fields** (the `structuredContent` of a successful call)

| Field | Type | Required | Constraints | Description |
| - | - | - | - | - |
| `found` | boolean | yes | - | - |
| `receipt` | object | no | - | - |
| `receipt.orgId` | string | yes | - | - |
| `receipt.deviceId` | string | yes | - | - |
| `receipt.idempotencyKey` | string | yes | - | - |
| `receipt.backendKind` | enum | yes | one of `adb_cloud`, `adb_real`, `emulator`, `robot_hand`, `esp32_hid`, `byod_portal` | - |
| `receipt.action` | enum | yes | one of `tap`, `swipe`, `typeText`, `pressKey`, `openApp`, `installApp`, `updateApp`, `uninstallApp` | - |
| `receipt.argsSummary` | object | yes | arbitrary keys | - |
| `receipt.observedFrame` | string | no | - | - |
| `receipt.status` | enum | yes | one of `pending`, `committed`, `failed` | - |
| `receipt.requestedAt` | string | yes | - | - |
| `receipt.committedAt` | string | no | - | - |
| `receipt.failedAt` | string | no | - | - |
| `receipt.error` | object | no | - | - |
| `receipt.reason` | enum | no | one of `interrupted_by_restart`, `outcome_unknown` | - |
| `receipt.requiresManualReview` | boolean | no | - | - |
| `receipt.actor` | object | no | - | Who acted, by kind (@since 0.7.0). `system` is phonebase itself (a schedule's timer, or phonebase driving a run), and `onBehalfOf` then says for whom: a schedule by id, a person or key by kind only. Credential ids are not on this tool; `GET /v1/receipts` carries them. |

### start\_run

Start an agent run for one natural-language goal. `executor` says who drives it. `phonebase` runs it for you and returns no url. `self_hosted` means phonebase executes nothing: nothing happens on the device until your own worker connects to the run-scoped VisualRemote base url returned once here, which is a credential, and which is why `workerConnected` is false. Returns a runId to poll with get\_run. Low-level tools stay available for deterministic control.

**Required scope:** `runs:start` · **Added in:** 0.7.0

**Annotations:** `title` "Start a run", `readOnlyHint` false, `destructiveHint` true, `idempotentHint` false, `openWorldHint` false

**Parameters**

| Field | Type | Required | Constraints | Description |
| - | - | - | - | - |
| `deviceId` | string | yes | min length 1 | Device id from list\_devices. |
| `leaseId` | string | no | min length 1 | Optional. Lease id from acquire\_device. Leave it out: the action runs under the lease your organization holds on the device, and a gesture takes the device for you when it holds none. |
| `goal` | string | yes | min length 1, max length 4000 | What to accomplish on the device, in natural language. |
| `maxSteps` | integer | no | min 1 | Ceiling on metered steps for this run. A value above the server's own limit is capped to that limit, not refused (the limit is deployment configuration, not a published number); read the run's maxSteps for the effective ceiling. |
| `deadlineMs` | integer | no | min 1000 | Wall-clock budget in ms, at least 1000 (a smaller window would be born expired). Capped by the server limit AND by the lease's own expiry. |
| `executor` | enum | no | one of `phonebase`, `self_hosted` | Who drives the run. `phonebase` runs it for you and returns no run url; `self_hosted` means your own worker drives it through the `vrxBaseUrl` returned once. Default: `self_hosted` for an API key, `phonebase` for a signed-in person. |

**Result fields** (the `structuredContent` of a successful call)

| Field | Type | Required | Constraints | Description |
| - | - | - | - | - |
| `runId` | string | yes | - | The run. Use it on every later call about this run. |
| `agentRunId` | string | yes | - | - |
| `state` | enum | yes | one of `queued`, `running`, `needs_user_control`, `paused`, `succeeded`, `failed`, `canceled` | - |
| `maxSteps` | integer | yes | - | The EFFECTIVE step ceiling: your ask, capped by the server's limit. Read this rather than assuming your ask was honoured. |
| `deadlineAt` | string | yes | - | The effective wall-clock deadline (ISO 8601): your deadlineMs, capped by the server limit and the lease's expiry. |
| `executor` | enum | yes | one of `phonebase`, `self_hosted` | Who drives the run, as decided: what you asked for, or the default for your credential. |
| `vrxBaseUrl` | string | no | - | Run-scoped VisualRemote base url for your worker, present ONLY when `executor` is `self_hosted`. Treat as a credential: it grants this one device for this one run. |
| `workerConnected` | literal | yes | always `false` | Always false here, for two different reasons. On a `self_hosted` run no worker can have connected yet, because this response is the first place `vrxBaseUrl` exists, and nothing happens on the device until yours does. On a `phonebase` run no worker ever connects at all: phonebase drives it, and its progress shows as `stepSeq` on the run rather than here. |

### get\_run

Bounded long-poll for a run's state. Returns immediately by default; with waitMs + sinceState it waits for a change and answers at the deadline regardless, it never hangs. Never echoes the run url.

**Required scope:** `runs:start` · **Added in:** 0.7.0

**Annotations:** `title` "Watch a run", `readOnlyHint` true, `destructiveHint` false, `idempotentHint` true, `openWorldHint` false

**Parameters**

| Field | Type | Required | Constraints | Description |
| - | - | - | - | - |
| `runId` | string | yes | min length 1 | The runId start\_run returned. |
| `waitMs` | integer | no | min 0, max 25000 | Bounded wait for a state change, 0-25000 ms (default 0 = answer immediately). |
| `sinceState` | enum | no | one of `queued`, `running`, `needs_user_control`, `paused`, `succeeded`, `failed`, `canceled` | Return as soon as the state differs from this one; otherwise answer at the deadline. |

**Result fields** (the `structuredContent` of a successful call)

| Field | Type | Required | Constraints | Description |
| - | - | - | - | - |
| `recovery` | object or null | no | - | - |
| `recovery.id` | string | yes | - | - |
| `recovery.since` | string | yes | - | - |
| `recovery.remainingMs` | integer | yes | min 0, max 9007199254740991 | - |
| `recovery.blockedReason` | enum or null | yes | one of `action_unresolved`, `budget_exhausted` | - |
| `runId` | string | yes | - | - |
| `agentRunId` | string | yes | - | - |
| `deviceId` | string | yes | - | - |
| `goal` | string | yes | - | What this run was asked to accomplish, in the words it was started with (@since 0.3.0). |
| `executor` | enum | yes | one of `phonebase`, `self_hosted` | Who drives this run: `phonebase` (we do) or `self_hosted` (your worker does). |
| `state` | enum | yes | one of `queued`, `running`, `needs_user_control`, `paused`, `succeeded`, `failed`, `canceled` | - |
| `stepSeq` | integer | yes | - | Metered steps issued so far (one per frame served through VRX). |
| `maxSteps` | integer | yes | - | - |
| `deadlineAt` | string or null | yes | - | - |
| `createdAt` | string | yes | - | - |
| `updatedAt` | string | yes | - | - |
| `workerFirstContactAt` | string or null | yes | - | When a self-hosted worker first reached this run, or null when none ever did. On a `self_hosted` run you supply the worker, and one that stays queued with null here is waiting for it. A `phonebase` run is driven in process and never sets this. |
| `initiator` | object | yes | - | Who started this run. Always present; see `kind` for what it can and cannot say. |
| `initiator.kind` | enum | yes | one of `api_key`, `oauth`, `dev_token`, `system`, `unattributed` | Which credential started the run. `system` is phonebase itself (a schedule's timer). `unattributed` means the record cannot say, a run older than this field, and is NOT the same as a positive claim that no key was involved. |
| `initiator.userId` | string | no | - | The authorization server's subject id of the person, when a person started it. An id, never a name. |
| `initiator.apiKeyId` | string | no | - | The key's id, when a key started it. |
| `initiator.viaCopilot` | boolean | yes | - | Whether a copilot started it for the person named above. Always present. |
| `initiator.onBehalfOf` | object | no | - | Whose work phonebase did, present exactly when the principal kind is `system`. |
| `reason` | enum | no | one of `worker_reported_failure`, `budget_exhausted`, `deadline_exceeded`, `canceled_by_caller`, `lease_lost`, `interrupted_by_restart` | - |
| `detail` | string | no | - | - |
| `lastFrame` | object or null | yes | - | The last frame this run looked at, or null when it never looked (@since 0.8.0). `GET /v1/frames/{frameId}` turns the id into a short-lived image URL, where the deployment archives frames; where it does not, the id is still minted and that route answers 404. |
| `lastFrame.frameId` | string | yes | - | - |
| `lastFrame.viewport` | object | yes | - | - |

### list\_runs

List this org's runs, newest first. Each entry carries the goal it was started with and how it ended, so a lost runId is recoverable. Page with the returned nextBefore cursor. Never echoes a run url.

**Required scope:** `runs:start` · **Added in:** 0.7.0

**Annotations:** `title` "List your runs", `readOnlyHint` true, `destructiveHint` false, `idempotentHint` true, `openWorldHint` false

**Parameters**

| Field | Type | Required | Constraints | Description |
| - | - | - | - | - |
| `limit` | integer | no | min 1, max 200 | How many runs to return, 1-200 (default 50). Newest first. |
| `before` | string | no | min length 1 | Opaque cursor from a previous page's nextBefore. Returns only runs older than it. |
| `state` | enum | no | one of `queued`, `running`, `needs_user_control`, `paused`, `succeeded`, `failed`, `canceled` | Return only runs in this state. |

**Result fields** (the `structuredContent` of a successful call)

| Field | Type | Required | Constraints | Description |
| - | - | - | - | - |
| `runs` | object\[] | yes | - | - |
| `runs[].recovery` | object or null | no | - | - |
| `runs[].runId` | string | yes | - | - |
| `runs[].agentRunId` | string | yes | - | - |
| `runs[].deviceId` | string | yes | - | - |
| `runs[].goal` | string | yes | - | What this run was asked to accomplish, in the words it was started with (@since 0.3.0). |
| `runs[].executor` | enum | yes | one of `phonebase`, `self_hosted` | Who drives this run: `phonebase` (we do) or `self_hosted` (your worker does). |
| `runs[].state` | enum | yes | one of `queued`, `running`, `needs_user_control`, `paused`, `succeeded`, `failed`, `canceled` | - |
| `runs[].stepSeq` | integer | yes | - | Metered steps issued so far (one per frame served through VRX). |
| `runs[].maxSteps` | integer | yes | - | - |
| `runs[].deadlineAt` | string or null | yes | - | - |
| `runs[].createdAt` | string | yes | - | - |
| `runs[].updatedAt` | string | yes | - | - |
| `runs[].workerFirstContactAt` | string or null | yes | - | When a self-hosted worker first reached this run, or null when none ever did. On a `self_hosted` run you supply the worker, and one that stays queued with null here is waiting for it. A `phonebase` run is driven in process and never sets this. |
| `runs[].initiator` | object | yes | - | Who started this run. Always present; see `kind` for what it can and cannot say. |
| `runs[].reason` | enum | no | one of `worker_reported_failure`, `budget_exhausted`, `deadline_exceeded`, `canceled_by_caller`, `lease_lost`, `interrupted_by_restart` | - |
| `runs[].detail` | string | no | - | - |
| `runs[].lastFrame` | object or null | yes | - | The last frame this run looked at, or null when it never looked (@since 0.8.0). `GET /v1/frames/{frameId}` turns the id into a short-lived image URL, where the deployment archives frames; where it does not, the id is still minted and that route answers 404. |
| `nextBefore` | string | no | - | Cursor for the next (older) page; absent when this page reached the end. |

### cancel\_run

End a run and destroy its VisualRemote credential. Already-terminal runs are returned unchanged, terminal is terminal.

**Required scope:** `runs:start` · **Added in:** 0.7.0

**Annotations:** `title` "Stop a run", `readOnlyHint` false, `destructiveHint` false, `idempotentHint` true, `openWorldHint` false

**Parameters**

| Field | Type | Required | Constraints | Description |
| - | - | - | - | - |
| `runId` | string | yes | min length 1 | The runId start\_run returned. |

**Result fields** (the `structuredContent` of a successful call)

| Field | Type | Required | Constraints | Description |
| - | - | - | - | - |
| `recovery` | object or null | no | - | - |
| `recovery.id` | string | yes | - | - |
| `recovery.since` | string | yes | - | - |
| `recovery.remainingMs` | integer | yes | min 0, max 9007199254740991 | - |
| `recovery.blockedReason` | enum or null | yes | one of `action_unresolved`, `budget_exhausted` | - |
| `runId` | string | yes | - | - |
| `agentRunId` | string | yes | - | - |
| `deviceId` | string | yes | - | - |
| `goal` | string | yes | - | What this run was asked to accomplish, in the words it was started with (@since 0.3.0). |
| `executor` | enum | yes | one of `phonebase`, `self_hosted` | Who drives this run: `phonebase` (we do) or `self_hosted` (your worker does). |
| `state` | enum | yes | one of `queued`, `running`, `needs_user_control`, `paused`, `succeeded`, `failed`, `canceled` | - |
| `stepSeq` | integer | yes | - | Metered steps issued so far (one per frame served through VRX). |
| `maxSteps` | integer | yes | - | - |
| `deadlineAt` | string or null | yes | - | - |
| `createdAt` | string | yes | - | - |
| `updatedAt` | string | yes | - | - |
| `workerFirstContactAt` | string or null | yes | - | When a self-hosted worker first reached this run, or null when none ever did. On a `self_hosted` run you supply the worker, and one that stays queued with null here is waiting for it. A `phonebase` run is driven in process and never sets this. |
| `initiator` | object | yes | - | Who started this run. Always present; see `kind` for what it can and cannot say. |
| `initiator.kind` | enum | yes | one of `api_key`, `oauth`, `dev_token`, `system`, `unattributed` | Which credential started the run. `system` is phonebase itself (a schedule's timer). `unattributed` means the record cannot say, a run older than this field, and is NOT the same as a positive claim that no key was involved. |
| `initiator.userId` | string | no | - | The authorization server's subject id of the person, when a person started it. An id, never a name. |
| `initiator.apiKeyId` | string | no | - | The key's id, when a key started it. |
| `initiator.viaCopilot` | boolean | yes | - | Whether a copilot started it for the person named above. Always present. |
| `initiator.onBehalfOf` | object | no | - | Whose work phonebase did, present exactly when the principal kind is `system`. |
| `reason` | enum | no | one of `worker_reported_failure`, `budget_exhausted`, `deadline_exceeded`, `canceled_by_caller`, `lease_lost`, `interrupted_by_restart` | - |
| `detail` | string | no | - | - |
| `lastFrame` | object or null | yes | - | The last frame this run looked at, or null when it never looked (@since 0.8.0). `GET /v1/frames/{frameId}` turns the id into a short-lived image URL, where the deployment archives frames; where it does not, the id is still minted and that route answers 404. |
| `lastFrame.frameId` | string | yes | - | - |
| `lastFrame.viewport` | object | yes | - | - |

### resume\_run

Resume a run that is on hold: one a person paused with pause\_run, or one in needs\_user\_control waiting for a person. If recovery is present, pass expectedRecoveryId equal to its current id and hold the device's current lease yourself; a missing lease fails with lease\_required and another person's lease fails with lease\_mismatch. Missing or stale recovery ids, blocked recovery, or exhausted recovery budget return the run unchanged without restarting it. Recovery waiting preserves progress and freezes the remaining execution time until this explicit continuation. For ordinary pauses or steps such as a login or challenge, existing resume behavior applies: a paused run needs the lease it acts under and fails with lease\_mismatch when the device has moved on. Finished and other ineligible runs are returned unchanged.

**Required scope:** `runs:start` · **Added in:** 0.7.0

**Annotations:** `title` "Let a run carry on", `readOnlyHint` false, `destructiveHint` true, `idempotentHint` true, `openWorldHint` false

**Parameters**

| Field | Type | Required | Constraints | Description |
| - | - | - | - | - |
| `runId` | string | yes | min length 1 | The runId of a run that is paused or parked in needs\_user\_control. |
| `expectedRecoveryId` | string | no | - | The current recovery.id, required to continue saved recovery waiting. |

**Result fields** (the `structuredContent` of a successful call)

| Field | Type | Required | Constraints | Description |
| - | - | - | - | - |
| `recovery` | object or null | no | - | - |
| `recovery.id` | string | yes | - | - |
| `recovery.since` | string | yes | - | - |
| `recovery.remainingMs` | integer | yes | min 0, max 9007199254740991 | - |
| `recovery.blockedReason` | enum or null | yes | one of `action_unresolved`, `budget_exhausted` | - |
| `runId` | string | yes | - | - |
| `agentRunId` | string | yes | - | - |
| `deviceId` | string | yes | - | - |
| `goal` | string | yes | - | What this run was asked to accomplish, in the words it was started with (@since 0.3.0). |
| `executor` | enum | yes | one of `phonebase`, `self_hosted` | Who drives this run: `phonebase` (we do) or `self_hosted` (your worker does). |
| `state` | enum | yes | one of `queued`, `running`, `needs_user_control`, `paused`, `succeeded`, `failed`, `canceled` | - |
| `stepSeq` | integer | yes | - | Metered steps issued so far (one per frame served through VRX). |
| `maxSteps` | integer | yes | - | - |
| `deadlineAt` | string or null | yes | - | - |
| `createdAt` | string | yes | - | - |
| `updatedAt` | string | yes | - | - |
| `workerFirstContactAt` | string or null | yes | - | When a self-hosted worker first reached this run, or null when none ever did. On a `self_hosted` run you supply the worker, and one that stays queued with null here is waiting for it. A `phonebase` run is driven in process and never sets this. |
| `initiator` | object | yes | - | Who started this run. Always present; see `kind` for what it can and cannot say. |
| `initiator.kind` | enum | yes | one of `api_key`, `oauth`, `dev_token`, `system`, `unattributed` | Which credential started the run. `system` is phonebase itself (a schedule's timer). `unattributed` means the record cannot say, a run older than this field, and is NOT the same as a positive claim that no key was involved. |
| `initiator.userId` | string | no | - | The authorization server's subject id of the person, when a person started it. An id, never a name. |
| `initiator.apiKeyId` | string | no | - | The key's id, when a key started it. |
| `initiator.viaCopilot` | boolean | yes | - | Whether a copilot started it for the person named above. Always present. |
| `initiator.onBehalfOf` | object | no | - | Whose work phonebase did, present exactly when the principal kind is `system`. |
| `reason` | enum | no | one of `worker_reported_failure`, `budget_exhausted`, `deadline_exceeded`, `canceled_by_caller`, `lease_lost`, `interrupted_by_restart` | - |
| `detail` | string | no | - | - |
| `lastFrame` | object or null | yes | - | The last frame this run looked at, or null when it never looked (@since 0.8.0). `GET /v1/frames/{frameId}` turns the id into a short-lived image URL, where the deployment archives frames; where it does not, the id is still minted and that route answers 404. |
| `lastFrame.frameId` | string | yes | - | - |
| `lastFrame.viewport` | object | yes | - | - |

### pause\_run

Put a run on hold so a person can act on the device while the agent waits. The run's deadline stops advancing and the lease it acts under is slid to a fresh window; the hold ends at manualControlUntil, after which an unresumed run is canceled. Resume it with resume\_run. Fails with lease\_mismatch when the device has already moved on.

**Required scope:** `runs:start` · **Added in:** 0.7.0

**Annotations:** `title` "Hold a run", `readOnlyHint` false, `destructiveHint` false, `idempotentHint` false, `openWorldHint` false

**Parameters**

| Field | Type | Required | Constraints | Description |
| - | - | - | - | - |
| `runId` | string | yes | min length 1 | The runId of the run to put on hold. |

**Result fields** (the `structuredContent` of a successful call)

| Field | Type | Required | Constraints | Description |
| - | - | - | - | - |
| `run` | object | yes | - | - |
| `run.recovery` | object or null | no | - | - |
| `run.runId` | string | yes | - | - |
| `run.agentRunId` | string | yes | - | - |
| `run.deviceId` | string | yes | - | - |
| `run.goal` | string | yes | - | What this run was asked to accomplish, in the words it was started with (@since 0.3.0). |
| `run.executor` | enum | yes | one of `phonebase`, `self_hosted` | Who drives this run: `phonebase` (we do) or `self_hosted` (your worker does). |
| `run.state` | enum | yes | one of `queued`, `running`, `needs_user_control`, `paused`, `succeeded`, `failed`, `canceled` | - |
| `run.stepSeq` | integer | yes | - | Metered steps issued so far (one per frame served through VRX). |
| `run.maxSteps` | integer | yes | - | - |
| `run.deadlineAt` | string or null | yes | - | - |
| `run.createdAt` | string | yes | - | - |
| `run.updatedAt` | string | yes | - | - |
| `run.workerFirstContactAt` | string or null | yes | - | When a self-hosted worker first reached this run, or null when none ever did. On a `self_hosted` run you supply the worker, and one that stays queued with null here is waiting for it. A `phonebase` run is driven in process and never sets this. |
| `run.initiator` | object | yes | - | Who started this run. Always present; see `kind` for what it can and cannot say. |
| `run.reason` | enum | no | one of `worker_reported_failure`, `budget_exhausted`, `deadline_exceeded`, `canceled_by_caller`, `lease_lost`, `interrupted_by_restart` | - |
| `run.detail` | string | no | - | - |
| `run.lastFrame` | object or null | yes | - | The last frame this run looked at, or null when it never looked (@since 0.8.0). `GET /v1/frames/{frameId}` turns the id into a short-lived image URL, where the deployment archives frames; where it does not, the id is still minted and that route answers 404. |
| `leaseId` | string | yes | - | The lease the run acts under, slid to a fresh window for the hold. |
| `manualControlUntil` | string | yes | - | When the hold ends (ISO 8601). Past it, an unresumed run is canceled. |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.