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

# Read the Accessibility Tree

> Inspect a typed, secure accessibility hierarchy from active iOS Simulator and Android Emulator sessions.

Read the accessibility hierarchy that the current app exposes to assistive
technology. The CLI, TypeScript SDK, REST API, and React embed return the same
versioned tree for iOS Simulator and Android Emulator sessions.

Each read is a point-in-time snapshot. Read again after a tap, navigation, text
change, or toggle to assert the new visible state. The operation is read-only:
it cannot execute commands or reach a different device.

## Install and authenticate

Install the CLI, sign in, and verify the active account:

```bash theme={null}
npm install -g runcloud
runcloud login
runcloud whoami
```

For the TypeScript SDK, install the package and set an API key created in the
[run.cloud dashboard](https://run.cloud/dashboard):

```bash theme={null}
npm install @run-cloud/sdk
export RUN_CLOUD_API_KEY="rc_live_..."
```

API keys belong in server-side code and CI secrets. Never put one in a browser
bundle. The React package uses the signed session URL instead:

```bash theme={null}
npm install @runcloud/ui
```

## CLI

Create a session with a compatible app, keep its id, and release it when the
shell exits:

<CodeGroup>
  ```bash iOS theme={null}
  SESSION_ID=$(runcloud ios create \
    --install ./MyApp.tar.gz \
    --inactivity-timeout 3m \
    --hard-timeout 15m \
    --json | jq -r '.id')

  trap 'runcloud ios delete "$SESSION_ID" >/dev/null 2>&1 || true' EXIT
  runcloud ios accessibility-tree "$SESSION_ID" --json
  ```

  ```bash Android theme={null}
  SESSION_ID=$(runcloud android create \
    --install ./app-debug.apk \
    --inactivity-timeout 3m \
    --hard-timeout 15m \
    --json | jq -r '.id')

  trap 'runcloud android delete "$SESSION_ID" >/dev/null 2>&1 || true' EXIT
  runcloud android accessibility-tree "$SESSION_ID" --json
  ```
</CodeGroup>

Omit `--json` for an indented terminal view with roles, labels, identifiers,
values, states, bounds, and children. `accessibility` is a shorter alias for
`accessibility-tree`. Use `--timeout <milliseconds>` to set a client wait from
100 to 60,000 milliseconds; the default is 20,000.

## TypeScript SDK

`accessibilityTree` is available on both platform clients. This complete iOS
example installs an existing asset, reads the tree, changes the app, and reads
again:

```ts theme={null}
import { Client, type SimulatorAccessibilityNode } from "@run-cloud/sdk";

const assetId = process.env.RUN_CLOUD_IOS_ASSET_ID;
if (!assetId) throw new Error("RUN_CLOUD_IOS_ASSET_ID is required");

const cloud = new Client();
const session = await cloud.ios.create({
  installAssets: [assetId],
  inactivityTimeout: "3m",
  hardTimeout: "15m",
});

function flatten(nodes: SimulatorAccessibilityNode[]): SimulatorAccessibilityNode[] {
  return nodes.flatMap((node) => [node, ...flatten(node.children)]);
}

try {
  const before = await cloud.ios.accessibilityTree(session.id);
  const beforeNodes = flatten(before.roots);
  const toggle = beforeNodes.find((node) => node.role === "switch");
  if (!toggle?.bounds) throw new Error("The app did not expose its switch");

  await cloud.ios.tap(session.id, {
    x: (toggle.bounds.x + toggle.bounds.width / 2) / before.screen.width,
    y: (toggle.bounds.y + toggle.bounds.height / 2) / before.screen.height,
  });

  const after = await cloud.ios.accessibilityTree(session.id);
  const changed = flatten(after.roots).find((node) => node.id === toggle.id);
  if (changed?.states.checked === toggle.states.checked) {
    throw new Error("The switch state did not change");
  }
  console.log({ before: toggle.states.checked, after: changed?.states.checked });
} finally {
  await cloud.ios.delete(session.id);
}
```

The Android form is identical apart from the platform client and asset:

```ts theme={null}
import { Client } from "@run-cloud/sdk";

const assetId = process.env.RUN_CLOUD_ANDROID_ASSET_ID;
if (!assetId) throw new Error("RUN_CLOUD_ANDROID_ASSET_ID is required");

const cloud = new Client();
const session = await cloud.android.create({
  installAssets: [assetId],
  inactivityTimeout: "3m",
  hardTimeout: "15m",
});

try {
  const tree = await cloud.android.accessibilityTree(session.id, {
    timeoutMs: 20_000,
  });
  const roots = tree.roots.map((node) => ({
    role: node.role,
    label: node.label,
    children: node.children.length,
  }));
  console.log(tree.screen, roots);
} finally {
  await cloud.android.delete(session.id);
}
```

Use `cloud.simulators.accessibilityTree(id, { platform })` when the platform is
chosen at runtime. Passing an `AbortSignal` stops the caller from waiting and
does not release the session.

## React embed

The signed viewer can read the same tree without exposing an API key to the
browser. Keep the component and request bound to the same session URL:

```tsx theme={null}
"use client";

import { useRef, useState } from "react";
import {
  RemoteControl,
  type RemoteControlHandle,
  type SimulatorAccessibilitySnapshot,
} from "@runcloud/ui";

export function AccessiblePreview({ url }: { url: string }) {
  const control = useRef<RemoteControlHandle>(null);
  const [tree, setTree] = useState<SimulatorAccessibilitySnapshot | null>(null);

  return (
    <div style={{ width: 430, height: 760 }}>
      <RemoteControl ref={control} url={url} />
      <button
        type="button"
        onClick={() => void control.current?.accessibilityTree().then(setTree)}
      >
        Read accessibility
      </button>
      <output>{tree ? `${tree.nodeCount} accessible nodes` : "Not read"}</output>
    </div>
  );
}
```

The promise only accepts a response from the exact current iframe window and
origin. It rejects with `RemoteControlAccessibilityError` for typed simulator
failures, and with `AbortError` if its signal, iframe, or component lifecycle
cancels the wait. `onAccessibilityResult` observes validated successes and
failures.

## Response schema

Every successful read returns `SimulatorAccessibilitySnapshot`:

```json theme={null}
{
  "schemaVersion": 1,
  "platform": "ios",
  "capturedAt": "2026-08-09T02:00:00.000Z",
  "screen": { "width": 393, "height": 852, "unit": "points" },
  "nodeCount": 2,
  "truncated": false,
  "roots": [
    {
      "id": "ios:id:form",
      "path": "0",
      "role": "group",
      "label": "Account",
      "value": null,
      "identifier": "form",
      "bounds": { "x": 16, "y": 80, "width": 361, "height": 240 },
      "states": {
        "enabled": true,
        "focused": false,
        "selected": null,
        "checked": null,
        "expanded": null,
        "focusable": false,
        "clickable": false,
        "scrollable": false,
        "editable": false,
        "secure": false
      },
      "native": {
        "platform": "ios",
        "type": "AXGroup",
        "roleDescription": "group",
        "subrole": null,
        "title": "Account",
        "help": null
      },
      "children": []
    }
  ]
}
```

| Snapshot field  | Meaning                                                                                            |
| --------------- | -------------------------------------------------------------------------------------------------- |
| `schemaVersion` | `1` for this contract.                                                                             |
| `platform`      | `ios` or `android`.                                                                                |
| `capturedAt`    | ISO 8601 capture timestamp.                                                                        |
| `screen`        | Current display width, height, and coordinate unit. iOS uses points; Android uses physical pixels. |
| `nodeCount`     | Total nodes represented in `roots`, including all descendants.                                     |
| `truncated`     | `true` when the source hierarchy exceeded the response bound.                                      |
| `roots`         | Ordered top-level accessible nodes. Each node owns its ordered `children`.                         |

Every node has a stable shape:

| Node field       | Meaning                                                                                                |
| ---------------- | ------------------------------------------------------------------------------------------------------ |
| `id`             | Unique identity within this snapshot. Prefer `identifier` for app-owned assertions when it is present. |
| `path`           | Zero-based child path such as `0.2.1`; it also records the returned nesting.                           |
| `role`           | Cross-platform normalized role.                                                                        |
| `label`, `value` | Accessible name and current value, or `null`. Secure values are always `null`.                         |
| `identifier`     | Native accessibility or resource identifier, or `null`.                                                |
| `bounds`         | Display-relative rectangle in the snapshot's screen unit, or `null`.                                   |
| `states`         | Normalized state flags. Unavailable tri-state flags are `null`.                                        |
| `native`         | Platform-specific detail that cannot be normalized without losing information.                         |
| `children`       | Ordered child nodes; the hierarchy is never flattened.                                                 |

Normalized roles are `application`, `window`, `dialog`, `alert`, `group`,
`scroll-view`, `list`, `list-item`, `text`, `heading`, `button`, `link`,
`image`, `text-field`, `checkbox`, `switch`, `radio-button`, `slider`,
`progress-indicator`, `tab`, `tab-bar`, `toolbar`, `menu`, `menu-item`,
`web-view`, `keyboard`, and `other`.

`states` contains `enabled`, `focused`, `selected`, `checked`, `expanded`,
`focusable`, `clickable`, `scrollable`, `editable`, and `secure`. `checked` is
`true`, `false`, `"mixed"`, or `null`.

For iOS, `native` contains `type`, `roleDescription`, `subrole`, `title`, and
`help`. For Android, it contains `className`, `packageName`, `resourceId`,
`contentDescription`, `checkable`, `longClickable`, and `password`.

## Accessibility assertions

Assert meaningful semantics instead of snapshotting the entire JSON document.
This keeps tests useful when system containers or framework-generated nodes
change:

```ts theme={null}
const tree = await cloud.android.accessibilityTree(session.id);
const nodes = tree.roots.flatMap(function walk(node): typeof tree.roots {
  return [node, ...node.children.flatMap(walk)];
});

const submit = nodes.find((node) => node.identifier?.endsWith("/submit"));
if (submit?.role !== "button" || submit.states.enabled !== false) {
  throw new Error("Submit must be exposed as a disabled button");
}

const password = nodes.find((node) => node.identifier?.endsWith("/password"));
if (password?.role !== "text-field" || !password.states.secure) {
  throw new Error("Password must be exposed as a secure text field");
}
if (password.value !== null) throw new Error("Secure text must be redacted");
```

Use role and label assertions for user-facing semantics, identifiers for app
elements that you control, state assertions after interactions, bounds for
visual placement, and child relationships for grouping and navigation order.

## Authentication and failures

The API accepts only an active session owned by an organization available to
the authenticated credential. Released sessions, expired credentials, wrong
platform paths, and sessions owned by another organization fail without
contacting another device. A signed viewer URL is scoped to its current device
and lease; it cannot request a tree from a replacement lease or another
iframe.

SDK failures are `RunCloudError` values with stable codes such as
`active_session_not_found`, `simulator_session_ended`,
`accessibility_unavailable`, `accessibility_timeout`,
`accessibility_transport_error`, and `accessibility_invalid_response`.

The authenticated REST endpoint is:

```text theme={null}
GET /run-cloud/{platform}/{sessionId}/accessibility
```

For example:

```bash theme={null}
curl --fail-with-body -sS \
  -H "Authorization: Bearer $RUN_CLOUD_API_KEY" \
  "https://api.run.cloud/run-cloud/ios/$SESSION_ID/accessibility" | jq
```

## Limits

* A snapshot contains at most 500 nodes and 80 levels. Check `truncated`
  before treating absence as an assertion.
* The tree reflects accessible elements, not every rendered view. Hidden,
  decorative, or accessibility-disabled elements may be absent.
* Platform accessibility frameworks decide containment. UIKit commonly omits
  layout-only `UIView` and `UIStackView` containers, so visually grouped iOS
  controls can be direct children of the application node. Android often keeps
  layout containers in its source hierarchy. `children` preserves the source
  relationship; run.cloud does not infer groups from screen position.
* iOS bounds are points. Android bounds are physical display pixels. Convert
  through `screen.width` and `screen.height` before using normalized tap
  coordinates.
* Platform frameworks decide some roles and states. Keep the documented
  `native` fields when a normalized role is too broad for a platform-specific
  assertion.
* IDs are unique within a snapshot. Framework-generated `id` and `path` values
  can change when the hierarchy changes; prefer an app-owned `identifier`.
* Animated transitions and asynchronously rendered screens can produce an
  intermediate snapshot. Wait for an app-visible readiness condition, then
  read again.
* Secure text field values are redacted at capture and every public validation
  boundary. The response reports `states.secure: true` and `value: null`.
