# Androperator Documentation Compiled from the MkDocs navigation tree and assembled docs staging directory. # Migrating to Androperator 1.0.0 Androperator was formerly known as Clawperator. Its first release is 1.0.0. The new package, CLI, Android app and runtime contracts change together. This release is being prepared; do not use the new download URLs until public cutover is complete. | Surface | Before | After | | --- | --- | --- | | npm and CLI | `clawperator` | `androperator` | | Environment variables | `CLAWPERATOR_*` | `ANDROPERATOR_*` | | Local state | `~/.clawperator/` | `~/.androperator/` | | Android release app | `com.clawperator.operator` | `com.androperator.operator` | | Android development app | `com.clawperator.operator.dev` | `com.androperator.operator.dev` | | Kotlin packages | `clawperator.*` | `androperator.*` | | Result envelope | `[Clawperator-Result]` | `[Androperator-Result]` | Install `androperator@1.0.0` and its matching APK after publication. The renamed Android app installs separately; grant its accessibility and automation permissions. The release retains the existing signing key. Updating the CLI alone does not make an old Android app compatible with the new envelope or ingress names. Update scripts, MCP client commands, bundled agent skill names and environment variables. Use the new CLI and explicitly select the new Operator package. Do not reuse old daemon sockets, PIDs, or version markers. Stop the old daemon using the old CLI before starting the renamed one. Installation initializes an empty local skill registry. It no longer clones a companion skills catalog. Keep your own workspace, set `ANDROPERATOR_SKILLS_REGISTRY` to its registry, and adapt its scripts, manifests, frontmatter and result handling to the new contracts before validating and running. Git-ref catalog synchronization is no longer supported; `skills update` and `skills sync --ref main` initialize or validate the local workspace. Optional bundled example workflows are a separate follow-up. Do not copy the entire old state directory. Review any recordings, logs and user skills you need to retain, then migrate those deliberately. Git hooks use `ANDROPERATOR_BLOCKED_TERMS_FILE` or `~/.androperator/blocked-terms.txt`; until that file exists, they retain the former default terms file as a migration safeguard. The old landing site and historical release notes remain under the former name. No Clawperator 1.0.0 package or release is to be published. ## Release preparation Before tagging, verify new npm publishing authorization, the destination domains, installer routes, downloads bucket and APK redirect Worker. Cloudflare upload secrets use `ANDROPERATOR_CLOUDFLARE_*`. Android signing secrets retain their existing stored values and secret names; the workflow maps them to renamed build variables. Do not generate a new signing key. Firebase SDKs, plugins, registrations and remote task-status reporting have been removed. Crash information remains in the app-private `crash-log.txt` and logcat. Task status remains available through structured local logcat reporting with command/task correlation. No Firebase registrations are required. --- # Docs Home Androperator is a deterministic actuator tool for Android device automation. Use this page as the routing index into the authored docs. The concrete machine entry points are `llms.txt` and `llms-full.txt`. --- **Current release: [1.0.0](https://github.com/androperator/androperator/releases/tag/v1.0.0)** --- ## Version Quick Reference Check your CLI version: ```bash androperator version ``` Check CLI and APK compatibility: ```bash androperator version --check-compat --device --operator-package ``` See [Version Compatibility](troubleshooting/compatibility.md) for detailed compatibility rules and recovery. ## Agent Entry Points - [llms.txt](https://docs.androperator.com/llms.txt) - compact machine entrypoint - [llms-full.txt](https://docs.androperator.com/llms-full.txt) - full machine-readable docs corpus Verification pattern: ```bash curl -fsSL https://docs.androperator.com/llms.txt curl -fsSL https://docs.androperator.com/llms-full.txt ``` Use: - `llms.txt` when you want the compact docs index - `llms-full.txt` when you want the full assembled documentation corpus ## Setup - [Setup](setup.md) - install the CLI, prepare a device, install the Operator APK, verify readiness, and run the first snapshot - [Host Agent Orientation](host-agents.md) - canonical post-install route for choosing between `androperator skills`, `androperator mcp serve`, and direct CLI automation - [Quickstart](quickstart.md) - the observe/decide/act loop: taking snapshots, reading the hierarchy, and sending actions ## API - [Overview](api/overview.md) - execution payload, result envelope, and branching model - [CLI Reference](api/cli.md) - generated command and flag reference - [Actions](api/actions.md) - canonical action types and parameter semantics - [Selectors](api/selectors.md) - `NodeMatcher` contract and CLI selector mapping - [Snapshot Format](api/snapshot.md) - where `snapshot` XML lives and how extraction works - [Errors](api/errors.md) - public error-code contract and recovery patterns - [Devices](api/devices.md) - device discovery and deterministic targeting - [Doctor](api/doctor.md) - doctor report contract and readiness checks - [Timeouts](api/timeouts.md) - execution and action timeout budgeting - [Environment Variables](api/environment.md) - current `ANDROPERATOR_*` and runtime environment controls - [Serve API](api/serve.md) - local HTTP and SSE contract - [MCP Server](api/mcp.md) - stdio MCP server for MCP clients such as Claude Desktop - [Navigation Patterns](api/navigation.md) - composed navigation workflows for agents - [Recording Format](api/recording.md) - raw NDJSON recording schema and parsed step log ## Skills - [Overview](skills/overview.md) - registry model, discovery, and wrapper execution - [Authoring](skills/authoring.md) - scaffolded files, artifacts, and validation - [Personalized Skills](skills/personalized-skills.md) - local wrappers, privacy boundaries, and shared-skill promotion rules - [Development Workflow](skills/development.md) - local iteration loop for skills - [Device Prep and Runtime](skills/runtime.md) - runtime environment, timeout, and output rules ## Troubleshooting - [Operator App](troubleshooting/operator.md) - installation, permission, handshake, and crash recovery - [Known Issues](troubleshooting/known-issues.md) - currently verified known issues page - [Version Compatibility](troubleshooting/compatibility.md) - CLI and Operator APK version alignment Androperator is open source. If these docs help, see the [project on GitHub](https://github.com/androperator/androperator). --- # Setup ## Purpose Get from an empty host to a first successful `androperator snapshot` with one deterministic path and machine-checkable success conditions. ## Prerequisites | Requirement | Minimum | Machine check | | --- | --- | --- | | Node.js | v24+ | `node -v` | | Java | 17 or 21 | `java -version` | | adb | On `PATH` | `adb version` | | Android target | One device or emulator visible to adb | `androperator devices` | **Java note:** The installer provisions Java 17 automatically on supported platforms (macOS with Homebrew, Ubuntu/Debian, Arch). Java 17 or 21 is required as the host JDK for Android builds (AGP 8.x requirement). The Android Gradle build compiles Java and Kotlin with Java 17 settings; device compatibility is handled by Android's DEX pipeline, not by targeting an older bytecode level. ## Agent-directed setup When a user wants an outside agent to install, repair, verify, and orient Androperator, give the agent this prompt: ```text Read https://androperator.com/skill.md and get me set up with Androperator. ``` `https://androperator.com/skill.md` is the public setup skill for agents. It is the pre-install entrypoint that tells an agent when to use the shell installer, when to use direct npm install, when to run `androperator install`, which readiness checks prove setup, and which human approval boundaries must stop the agent. The public skill does not replace `androperator install`. It points the agent to `androperator install` as the canonical post-bootstrap route after the CLI exists. After install, local host-specific orientation moves to: | Path | Use | | --- | --- | | `~/.androperator/AGENTS.md` | Local guide written by host setup for the current machine. | | `~/.androperator/install-state.json` | Install metadata, registry path, APK version, and last device serial when known. | | `~/.androperator/mcp-config-snippet.json` | Generated stdio MCP configuration for hosts that choose `androperator mcp serve`. | Machine-checkable verification after an agent-directed setup: ```bash androperator doctor androperator devices androperator snapshot --device test -f ~/.androperator/AGENTS.md test -f ~/.androperator/mcp-config-snippet.json ``` Continue only when `androperator doctor` exits `0`, `criticalOk` is `true`, and the snapshot command returns a successful result envelope. Human action can still be required for OS prompts, Developer Options, USB debugging authorization, Android accessibility permission, notification permission, app installation, app sign-in, and choosing the correct device when multiple targets are connected. ## 1. Install the CLI Recommended - the installer handles Node, Java 17, adb, CLI bootstrap, and the delegated `androperator install` flow in one step: ```bash curl -fsSL https://androperator.com/install.sh | bash ``` If the installer succeeds, skip to [5. Verify readiness with doctor](#5-verify-readiness-with-doctor). When more than one adb-visible device is present, the installer reports each detected device, runs `doctor` against each ready `adb` device, installs the current release APK on any ready device that is missing or incompatible, and then finishes with explicit `--device ` guidance for later commands. Alternatively, install the CLI only via npm (Node.js 24+ required): ```bash npm install -g androperator ``` Then run the canonical post-bootstrap install flow: ```bash androperator install ``` Success conditions: - `androperator version` exits `0` and prints a version string. - If you used `install.sh`, the delegated install flow downloads the current release APK when remediation needs setup and no reusable local copy is already in place. For later manual setup or recovery, redownload from `https://androperator.com/operator.apk` or use `androperator operator download`. ### Durable host-agent artifacts from `androperator install` After shell bootstrap succeeds, `install.sh` delegates to `androperator install`. That CLI-owned post-bootstrap flow runs operator remediation, runtime-skills install, bundled-skills install, and `androperator host setup`, then writes these durable onboarding files under `~/.androperator/`: | Path | Meaning | When to read it | | --- | --- | --- | | `~/.androperator/AGENTS.md` | Local Androperator guide with runtime-skill discovery commands and current bundled-skills status | First stop for a host agent that needs to discover what Androperator can do on this machine | | `~/.androperator/install-state.json` | Durable install metadata written by `androperator host setup` during install | Use when you need the last known install facts without rerunning `doctor` | | `~/.androperator/mcp-config-snippet.json` | Paste-ready MCP config for Claude Desktop, Codex, and a generic stdio MCP consumer | Use when the host should connect through `androperator mcp serve` instead of shelling out to the CLI | Shell prerequisite failures exit before these files are written. After `install.sh` delegates to `androperator install`, later remediation or device readiness failures can still leave these files behind because host setup runs before the CLI returns its final install status. The runtime-skills registry is discovered automatically from `~/.androperator/skills/skills/skills-registry.json` after `androperator skills install`, so `install.sh` no longer writes `ANDROPERATOR_SKILLS_REGISTRY` into shell RC files. Bundled host-agent skills are installed separately from runtime skills: - `~/.androperator/bundled-skills/` is the canonical first-party bundled-skill store - `~/.claude/skills/` and the Codex skills dir receive Androperator-managed symlinks into that store - `~/.agents/skills/` receives Androperator-managed real directory copies with a `.androperator-managed` marker so generic agents can scan them without following symlinks outside their configured root - discovery directories that alias `~/.agents/skills/` share its managed copies, including Claude Code or Codex; see [bundled-skill installation](skills/authoring.md) for legacy migration and backup behavior - runtime skills from `~/.androperator/skills/` are not mirrored into shared agent discovery directories Canonical public next step after install: - read [Host Agent Orientation](host-agents.md) when you need to decide between `androperator skills`, `androperator mcp serve`, and direct CLI automation `install-state.json` currently has this shape: ```json { "schemaVersion": 1, "installedAt": "2026-04-17T08:12:34Z", "cliVersion": "1.2.3", "registryPath": "/Users//.androperator/skills/skills/skills-registry.json", "apkVersion": "1.2.3", "lastDeviceSerial": null } ``` Field rules: - `schemaVersion` and `installedAt` are always present - `cliVersion` is `null` when the installer could not run `androperator --version` - `registryPath` is `null` when the installer cannot resolve any readable runtime-skills registry path from the current install run, `ANDROPERATOR_SKILLS_REGISTRY`, prior install state, or the default installed home path - `apkVersion` is `null` when the installer does not have a known operator version - `lastDeviceSerial` is `null` when install did not pick one unambiguous device Shared-agent bridge behavior is intentionally bounded: - if `~/.agents/AGENTS.md` already exists, `androperator host setup` appends one Androperator-owned bridge block there - that bridge points back to `~/.androperator/AGENTS.md` plus the `androperator skills` discovery commands - if `~/.agents/AGENTS.md` does not exist, the installer does not create it - the installer does not copy runtime skills into shared agent skill directories Verification: ```bash ls ~/.androperator/AGENTS.md ~/.androperator/install-state.json ~/.androperator/mcp-config-snippet.json androperator skills list test ! -f ~/.agents/AGENTS.md || grep -F "ANDROPERATOR_SHARED_AGENT_BRIDGE:START" ~/.agents/AGENTS.md ``` When choosing the host-facing surface: - use `androperator skills` when you want to discover or run installed runtime skills by app, keyword, or id - no shell profile export is required for the default runtime-skills registry path - use MCP when your host already supports stdio MCP and wants registered tools such as `devices`, `snapshot`, and `execute` See [Host Agent Orientation](host-agents.md) for the post-install decision flow and the first discovery commands to try. ## 2. Prepare the Android target Required device state: 1. Enable Developer options (Settings > About phone > tap Build Number 7 times). 2. Enable USB debugging (Settings > Developer options > USB debugging). 3. Connect the device via USB, or boot an emulator via Android Studio or `androperator emulator create`. 4. Accept the adb authorization prompt if Android shows one. Emulators have USB debugging enabled by default. Physical devices require steps 1-2 and the RSA key acceptance in step 4. Success condition: ```bash androperator devices ``` Expected output shape: ```json {"devices":[{"serial":"","state":"device"}]} ``` If state is `unauthorized`, unlock the device and accept the USB debugging prompt. If state is `offline`, restart adb: ```bash adb kill-server && adb start-server ``` If more than one target is connected, record the serial you will use and pass `--device ` on every later command. ## 3. Install the Operator APK To avoid stale cached copies, always refresh the stable release APK before running setup or reinstall: ```bash mkdir -p ~/.androperator/downloads curl -fsSL https://androperator.com/operator.apk -o ~/.androperator/downloads/operator.apk ``` Canonical public APK URL: `https://androperator.com/operator.apk` Keep the CLI and Operator APK on matching releases. Run `androperator doctor` to check version compatibility and device readiness before issuing UI commands. ```bash androperator operator setup --apk ~/.androperator/downloads/operator.apk ``` With explicit device targeting: ```bash androperator operator setup --apk ~/.androperator/downloads/operator.apk --device ``` For a local debug APK instead of the release APK: ```bash androperator operator setup \ --apk \ --device \ --operator-package com.androperator.operator.dev ``` | Variant | Package name | When to use | | --- | --- | --- | | Release | `com.androperator.operator` | Default. Installed by the installer. | | Debug | `com.androperator.operator.dev` | Local development, built from source. | The CLI auto-detects which variant is installed when exactly one is present. If both are installed, pass `--operator-package` explicitly. Behavior: - Installs the APK on the device via adb. - Grants accessibility and notification permissions. - Verifies that the package is visible to the package manager. Success condition: - Command exits without a structured error object. - A follow-up `androperator doctor` no longer reports `OPERATOR_NOT_INSTALLED` for `readiness.apk.presence`. Do not use raw `adb install` for setup. The CLI setup command is the only path that performs install, permission grant, and verification as one operation. ## 4. Re-grant permissions (recovery only) ```bash androperator grant-device-permissions --device ``` Use this only after the Operator APK crashes or Android revokes accessibility / notification permissions. For the first install, use `androperator operator setup`. If you force-stop the Operator package during debugging and the next handshake or snapshot stops working, use this same recovery step before trusting the runtime again, then re-run `androperator doctor`. ## Optional video recording tools Video recording additionally requires separately installed scrcpy 3.0 or newer, ffprobe, and ffmpeg 6.1 or newer with the libx264 encoder on the host PATH. Androperator does not bundle or install them. On macOS: `brew install scrcpy ffmpeg`. `doctor` warns under `host.video.dependencies` when they are unavailable; this advisory does not block ordinary device readiness. Screenshots still require only ADB. See [video dependency setup and recovery](api/evidence.md#video-dependencies) for CLI, Node, and MCP error details. ## 5. Verify readiness with doctor ```bash androperator doctor ``` With explicit targeting: ```bash androperator doctor --device ``` ### Doctor checks Doctor checks host prerequisites, adb/device discovery, Operator APK presence, version compatibility, handshake readiness, and interactive device state. The full check order, `DoctorReport` shape, `checks[]` fields, `nextActions` behavior, and `--fix` semantics are owned by [Doctor](api/doctor.md#doctor-report-contract). ### Success conditions - Exit code `0` means all critical checks passed. - JSON has `"criticalOk": true`. - `checks[]` contains only `"pass"` or non-critical `"warn"` statuses. ### Doctor flags - `doctor --fix` can execute shell-type remediation steps from failed checks. - `doctor --check-only` uses the same readiness exit status as plain doctor: `0` only when required checks pass, otherwise `1`. Inspect `skippedChecks` when prerequisites prevent verification. See [Doctor](api/doctor.md) for the full report contract and [Errors](api/errors.md) for recovery by code. ## 6. Run the first command ```bash androperator snapshot ``` With explicit targeting: ```bash androperator snapshot --device ``` Success conditions: - Exit code `0`. - `envelope.status` is `"success"`. - `envelope.stepResults[0].actionType` is `"snapshot"`. - `envelope.stepResults[0].success` is `true`. - `envelope.stepResults[0].data.text` contains the XML hierarchy. If the snapshot step succeeds but `data.text` is missing, Node converts that step into `SNAPSHOT_EXTRACTION_FAILED`. ## Agent sequence ### Brain / hand model Androperator is the hand. The agent is the brain. The agent decides what to do, then calls the Node CLI or the local serve API with explicit commands and waits for a structured result envelope. ### Programmatic first-run sequence 1. Run `androperator doctor [--device ] [--operator-package ]`. 2. If `readiness.apk.presence` fails, run `androperator operator setup --apk ...`. 3. If `readiness.handshake` fails after a known-good install, run `androperator grant-device-permissions ...`. 4. For multiple failures, `androperator doctor --fix ...` auto-executes shell remediation steps. 5. Re-run `androperator doctor ...` and require `criticalOk: true`. 6. Run `androperator snapshot ...`. 7. Branch only on structured fields: `criticalOk`, `checks[].code`, `envelope.status`, `envelope.errorCode`, `stepResults[].success`. ### How to confirm success without a human - Treat `doctor` as ready only when `criticalOk` is `true`. - Treat a device command as successful only when `envelope.status` is `"success"` and every `stepResults[].success` is `true`. - Prefer exact codes over message matching. Examples: `NO_DEVICES`, `OPERATOR_NOT_INSTALLED`, `RESULT_ENVELOPE_TIMEOUT`. ### Common first-run failures and recovery | Code | Meaning | Recovery | | --- | --- | --- | | `NO_DEVICES` | No adb target in state `device` | Connect or boot a target, rerun `androperator devices` then `doctor`. | | `DEVICE_UNAUTHORIZED` | adb key prompt not accepted | Accept the prompt on the device screen, rerun `doctor`. | | `DEVICE_OFFLINE` | Device unreachable | `adb kill-server && adb start-server`, rerun `doctor`. | | `MULTIPLE_DEVICES_DEVICE_ID_REQUIRED` | More than one target connected | Pick a serial from `androperator devices`, pass `--device ` to all commands. | | `OPERATOR_NOT_INSTALLED` | Expected package missing | `androperator operator setup --apk [--device ]`. | | `OPERATOR_VARIANT_MISMATCH` | Release/debug package mismatch | Pass `--operator-package ` or reinstall the intended APK. | | `DEVICE_ACCESSIBILITY_NOT_RUNNING` | Handshake returned a runtime failure | `androperator grant-device-permissions [--device ]`, rerun `doctor` and `snapshot`. | | `RESULT_ENVELOPE_TIMEOUT` | Broadcast sent, no result envelope arrived | If no correlated log lines were captured, run `doctor` to check version compatibility and accessibility; otherwise re-grant permissions, rerun `snapshot --timeout 5000 --verbose`, and verify `--operator-package`. | | `VERSION_INCOMPATIBLE` | CLI and APK version mismatch | Reinstall CLI (`npm install -g androperator@latest`) or APK to align versions. | ### When to pass `--device` and `--operator-package` - `--device `: required when more than one target is connected. - `--operator-package `: required when both release and debug variants are installed on the same device. For deterministic automation, always pass both flags explicitly. ## Debugging setup issues If setup fails, use `androperator logs` to inspect what happened: ```bash # Stream logs in one terminal androperator logs # Run the failing command in another terminal androperator doctor --device --operator-package ``` Log file location: `~/.androperator/logs/androperator-YYYY-MM-DD.log` Key events to look for: - `doctor.check` - Individual doctor check results - `adb.command` / `adb.complete` - ADB operations - `preflight.apk.pass` / `preflight.apk.missing` - APK presence checks See [Logging](api/logging.md) for complete documentation. ## Related pages - [Host Agent Orientation](host-agents.md) - [Quickstart](quickstart.md) - [API Overview](api/overview.md) - [Devices](api/devices.md) - [Doctor](api/doctor.md) - [Errors](api/errors.md) - [Environment Variables](api/environment.md) - [Troubleshooting](troubleshooting/operator.md) - [Logging](api/logging.md) ## Caller-Controlled Default Application Preparation Some flows require a default browser before automation starts. Doctor verifies the selected Operator; it does not assign Android application roles or choose a default application. Provision roles separately on a dedicated test device. Inspect the shell capabilities and read the current role holder first: ```bash DEVICE_ID= APPLICATION_ID= adb -s "$DEVICE_ID" shell cmd role help adb -s "$DEVICE_ID" shell cmd role get-role-holders --user 0 android.app.role.BROWSER ``` Only if the device supports these commands and changing that default is intended, assign the role and read it back: ```bash adb -s "$DEVICE_ID" shell cmd role add-role-holder --user 0 android.app.role.BROWSER "$APPLICATION_ID" adb -s "$DEVICE_ID" shell cmd role get-role-holders --user 0 android.app.role.BROWSER ``` A nonzero assignment status or a readback that does not contain the intended application is preparation failure. Do not continue on the strength of command acceptance alone. Shell role capabilities vary by Android version and device; inspect `help` on the actual target. This recipe's read-only capability checks were verified on an API 35 emulator. That emulator printed the supported commands but returned a nonzero status for `help`; the role-holder read returned zero. Role assignment and physical-device behavior are not part of the readiness proof. ## Sensitive hierarchy access The Operator can read views Android marks as accessibility-data sensitive. Both development and release APKs declare `android:isAccessibilityTool="true"`. Android [defines this declaration](https://developer.android.com/reference/android/accessibilityservice/AccessibilityServiceInfo#attr_android:isAccessibilityTool) as identifying services used to assist users with disabilities. Androperator is distributed outside Google Play. Install the matching APK using `androperator operator setup --apk ` with the explicit device and Operator package. Wait for setup to succeed before issuing UI commands. If the accessibility service is unavailable, enable the selected Operator in Android accessibility settings and run `androperator doctor`. If it is already enabled but remains unavailable, turn it off and back on, then rerun doctor. Queries and XML snapshots work on supported Android versions. The [per-node sensitivity flag](api/actions.md#action-query-ui) is available on Android 14 (API 34) and later. On earlier versions, queries report `accessibilityDataSensitive: null` and XML omits `accessibility-data-sensitive`. This API requirement applies to the flag, not to queries or XML capture. Applications can still have no accessible hierarchy. Sensitivity metadata does not change screenshot capture, redaction, or logging behavior. For screen-off notification/media observation, use `androperator doctor --capability background-observation` after setup. The default interactive doctor can fail on a locked screen while these reads remain available. See [background readiness](api/doctor.md#background-observation-readiness). --- # Host Agent Orientation ## Purpose Choose the correct Androperator front door after install: runtime-skill discovery through `androperator skills`, installed authoring-workflow discovery through `androperator bundled-skills`, long-running tool registration through `androperator mcp serve`, or direct action work through the CLI and local API. This page also defines the zero-results route: when runtime-skill discovery finds no relevant match, start with `androperator-skill-author-by-agent-discovery` and use `androperator-skill-author-by-recording` only after discovery returns `proceed_to_recording`, or when the route is already well understood. If packaged first-party bundled skills are installed, `androperator-agent-orientation` is the first-run packaged front door for this route and should point back to this page, while `androperator-upgrade` is the packaged whole-product upgrade route. It checks `androperator --version`, verifies the installer-owned Node, npm, and Java prerequisites before choosing the CLI-first path, uses the CLI-first upgrade sequence when the host is already viable, and falls back to `install.sh` only as recovery when the CLI is not reachable or the bootstrap prerequisites still need repair. ## Adaptive execution and goal coverage After orientation, use `androperator-agent-control-loop` for bounded adaptive navigation and independently verified extraction. It checks completeness, coverage, freshness and UI relationships, executes on the explicit target, and verifies each destination. Conditional references cover ambiguous selectors, nested rows, selective screenshots, failed-step recovery and optional delegation. No model provider credential is required by this bundled guidance. Match runtime skills by requested outcome, supported inputs, outputs and evidence, not package overlap. A Settings landing-screen skill does not cover OS version and build-number extraction. Partial coverage remains a discovery gap. For an explicit orchestrated-authoring request with sufficient bounded evidence, discovery can return `proceed_to_orchestrated_authoring` with `handoff_target` `androperator-agent-control-loop`. Discovery stops before authoring; the control loop then uses the canonical authoring workflow and the Settings examples linked from [Jev integration](skills/jev.md). Recording-based authoring retains `proceed_to_recording` and its dedicated proving workflow. One-shot requests do not authorize durable skill creation. Discovery retains its five-snapshot, three-screenshot and 90-second limits. ## Public Setup Skill Use the public setup skill before this page when the host is not installed, needs repair, or has not been verified yet: ```text Read https://androperator.com/skill.md and get me set up with Androperator. ``` `https://androperator.com/skill.md` is the outside-agent setup entrypoint. It covers the installer fallback, direct npm install, `androperator install`, readiness checks, local orientation files, MCP setup handoff, and stop conditions for human approval boundaries. After `androperator install` succeeds, this page takes over as the durable post-install routing guide. Read the local host guide first when present: ```bash cat ~/.androperator/AGENTS.md cat ~/.androperator/install-state.json cat ~/.androperator/mcp-config-snippet.json ``` Use `~/.androperator/mcp-config-snippet.json` only after deciding that the host should connect through stdio MCP and `androperator mcp serve`. Use direct CLI commands or `androperator skills` when the host does not need MCP. ## When To Read This Page Use this page to orient an installed host. Select the CLI and target before checking device readiness. ### Select the CLI and target Respect an explicitly requested CLI. For installed-release workflows, use the installed `androperator`; the presence of a newer checkout is not evidence that it needs an upgrade. For repository development, build with `npm --prefix apps/node run build` and invoke `node apps/node/dist/cli/index.js` from the repository root. Replace `androperator` in the examples with the chosen invocation. Inspect its `--version`, `--help`, and command-specific help before using features that may be newer than the installed release. ```bash androperator devices ``` With zero reachable devices, stop device work and follow [Setup](setup.md). Report unauthorized or offline targets rather than substituting another device. With one reachable device, confirm it matches the intended target. With multiple devices, resolve the requested serial or ask the user. Carry that explicit `--device ` through device-specific checks and actions in every case. ### Select an installed Operator Inspect packages on the selected device before recommending installation: ```bash adb -s shell pm list packages com.androperator.operator androperator version --check-compat --device --operator-package --output json ``` The package listing is a substring search; distinguish exact names. Public releases normally use `com.androperator.operator`; repository development defaults to `com.androperator.operator.dev`. Run the compatibility check for each relevant installed candidate using the selected CLI. It reports `compatible` and the CLI/APK versions; current compatibility requires equal versions after normalizing the trailing debug `-d` suffix. See [Version Compatibility](troubleshooting/compatibility.md). A mismatched debug package does not imply an installed release package is unusable. Use a compatible package appropriate to the task, while respecting an explicitly required variant. Do not silently change the user's CLI or Operator choice. A failed package query is not proof that the package is absent. ### Check readiness for the selected pair ```bash androperator doctor --device --operator-package --output json ``` Continue device work only with exit code `0` and `criticalOk: true`. Inspect the failed check, error code, and skipped prerequisites to choose targeted recovery from [Setup](setup.md) or [Errors](api/errors.md). Do not default to reinstalling, upgrading, `--fix`, or `--full`. Doctor includes device probes and may attempt waking; it is not a passive host-only check. Keep the explicit device and package on later `snapshot`, `skills run`, and direct-action commands, or configure the same pair for MCP using [MCP Server](api/mcp.md). Host-agent readiness is separate. A missing Codex executable or unsupported model must be resolved for a route that requires it; neither establishes an Androperator transport failure. Doctor's device success does not prove a model can run. ### Interpret the first result The agent decides what to do and verifies the user outcome. Androperator executes validated actions and returns structured evidence. A completed command does not alone prove a complete observation or a verified user outcome. Check source completeness and coverage using [Snapshot](api/snapshot.md), then inspect the resulting app state. For readiness, dispatch, result-wait, and post-processing failures, use [Execution failure evidence](api/errors.md#execution-failure-evidence). Preserve requested-command and readiness-probe identities separately. Even work marked `not_dispatched` can have earlier preflight effects. Observe before retrying an uncertain mutation; retained results may show execution despite a later post-processing failure. ## First Route After Install Use this order: 1. Read this page. 2. If packaged first-party bundled skills are installed and you are unfamiliar with this host, start with `androperator-agent-orientation`. It should verify readiness, separate runtime skills from bundled skills, and end with one concrete next step. 3. If the user or calling workflow explicitly chose a whole-product refresh, use `androperator-upgrade`. 4. If you need an app-specific capability, start with `androperator skills`. 5. If your host already speaks stdio MCP and wants registered tools, use `androperator mcp serve`. 6. If you already know you need raw actions and result envelopes, continue to [Quickstart](quickstart.md). ## Choose The Front Door | Situation | Start here | Why | | --- | --- | --- | | You are unfamiliar with this host and want the packaged first-run orientation surface | `androperator-agent-orientation` | Thin packaged router that points back to this page and the canonical docs. | | The user or calling workflow explicitly chose a whole-product refresh before you trust any downstream route | `androperator-upgrade` | Checks `androperator --version`, verifies Node 24+, npm reachability, and Java 17/21, then uses `npm install -g androperator@latest`, `androperator install`, and `androperator doctor`. Uses `install.sh` only when the CLI is not reachable or the bootstrap prerequisites need repair. | | You know the Android package id and want the fastest answer to "what can this host do for this app?" | `androperator skills for-app ` | `skills for-app` is the primary app-oriented discovery surface. | | You only know user-language terms such as app name or intent | `androperator skills search --keyword ` | Search is the fallback when you do not have the package id yet. | | You already have a skill id and want the exact metadata | `androperator skills get ` | Confirms the registry entry before a run. | | You want to execute a skill through the wrapper | `androperator skills run ...` | Uses the runtime-skill wrapper and its validation gate. | | Runtime-skill discovery returned no relevant match and you need the zero-results authoring route | `androperator bundled-skills list` | Bundled skills are separate from runtime skills. Start with `androperator-skill-author-by-agent-discovery`, then use `androperator-skill-author-by-recording` only after discovery returns `proceed_to_recording`, or when the route is already well understood. | | Your host already supports stdio MCP and wants registered tools such as `devices`, `snapshot`, `execute`, and `configure` | `androperator mcp serve` | MCP is the transport surface for long-running tool registration. | | You already know the exact action payload you want to send | [Quickstart](quickstart.md) | Quickstart covers the observe / decide / act loop directly. | ## Runtime-Skill Discovery Flow Use the shortest successful path first: ```bash androperator skills for-app androperator skills search --keyword androperator skills get androperator skills run ``` Decision rules: - Start with `skills for-app` when you know the Android package id. - Use `skills search --keyword` when you only have a user-language term. - Use `skills get` before `skills run` when you need to confirm the exact id or summary. - Use `skills run` only after discovery, not as the first probe. - Do not start with `skills list` unless the real task is inventory rather than app-oriented discovery. - If discovery returns zero relevant matches and the next job is skill creation rather than raw execution, inspect installed bundled skills with `androperator bundled-skills list`, start with `androperator-skill-author-by-agent-discovery`, and continue to [Authoring](skills/authoring.md). ## Zero-Results Route Use this authoring decision table only after runtime-skill discovery found no relevant installed match. | Situation | Next surface | Expected outcome | | --- | --- | --- | | No relevant runtime skill match and the next job is choosing the truthful route | `androperator bundled-skills list` | Confirm the installed bundled-skill front doors on this host. | | You need the bounded zero-results front door | `androperator-skill-author-by-agent-discovery` | Produce one discovery artifact and choose exactly one next step. | | Discovery returns `proceed_to_recording`, or the route is already well understood | `androperator-skill-author-by-recording` | Run the proving workflow from a fresh recording and one self-test. | | You explicitly want the low-level manual scaffold instead of the installed guided workflows | `androperator skills new ` | Create a local scaffold only. | The discovery pass should stay agent-driven by default. If discovery returns `proceed_to_recording`, the next phase changes boundary: use `androperator-skill-author-by-recording` as a user-performed proving workflow rather than continuing autonomous device driving through the recording step. ## MCP Decision Rule Use `androperator mcp serve` only when the host already wants MCP. Use [MCP Server](api/mcp.md) for: - stdio MCP client setup - long-running MCP sessions - tool registration for hosts such as Claude Desktop Do not use MCP as the first discovery surface when the real question is "what runtime skills are installed for this app?". Start with `androperator skills` for that job. ## When Discovery Stalls Use this sequence: 1. Confirm the registry is readable: ```bash androperator skills list ``` 2. If registry discovery still fails, check the installed home path written by the install and sync flow: ```bash ls ~/.androperator/skills/skills/skills-registry.json ``` 3. If that file is missing or stale, reinstall the runtime skills: ```bash androperator skills install ``` 4. If the host should connect through MCP instead of shelling out to the CLI, read the installed snippet and then use `androperator mcp serve`: ```bash cat ~/.androperator/mcp-config-snippet.json androperator mcp serve ``` 5. If the registry is readable but no installed runtime skill matches the request, inspect the installed authoring-workflow helpers: ```bash androperator bundled-skills list ``` Then continue to [Authoring](skills/authoring.md), start with `androperator-skill-author-by-agent-discovery`, and move to `androperator-skill-author-by-recording` only after discovery returns `proceed_to_recording`, or when the route is already well understood. ## Durable Post-Install Files These files help a host orient after install: | Path | Meaning | Next step | | --- | --- | --- | | `~/.androperator/AGENTS.md` | Local Androperator guide written by `androperator host setup` during install | Use it as machine-local context after you read this public route. | | `~/.androperator/install-state.json` | Durable install metadata written by `androperator host setup` | Check `registryPath`, `cliVersion`, and `lastDeviceSerial` without rerunning install. | | `~/.androperator/mcp-config-snippet.json` | Paste-ready MCP config written by `androperator host setup` | Use it when you choose the MCP route. | | `~/.androperator/skills/skills/skills-registry.json` | Installed runtime-skills registry | Verify it exists when `skills list` or `skills for-app` cannot discover skills. | | `~/.androperator/bundled-skills/` | Installed first-party bundled skills | Inspect it through `androperator bundled-skills list` when runtime discovery returns no relevant match. | | `~/.agents/skills//` | Managed real directory copies for generic agent skill discovery | Generic agents such as OpenClaw can discover packaged Androperator bundled skills without following symlinks outside `~/.agents/skills`. | ## Verification Use these commands to confirm the intended surface is working: ```bash androperator --help androperator skills --help androperator bundled-skills --help androperator skills for-app com.android.settings androperator skills search --keyword settings androperator skills get com.android.settings.capture-overview androperator skills list androperator bundled-skills list test -d ~/.agents/skills/androperator-agent-orientation test ! -L ~/.agents/skills/androperator-agent-orientation ``` Check: - `androperator --help` and `androperator skills --help` name `androperator-agent-orientation` as the first-run surface for unfamiliar hosts and point zero-match users to `androperator bundled-skills list` - `androperator --help` and `androperator skills --help` name `androperator-upgrade` as the packaged whole-product refresh route and note the Node, npm, and Java prerequisite gate - `androperator bundled-skills --help` names `androperator-agent-orientation` as the first-run orientation skill, `androperator-upgrade` as the packaged whole-product upgrade route after explicit upgrade intent and prerequisite viability, `androperator-skill-author-by-agent-discovery` as the zero-results front door, and `androperator-skill-author-by-recording` as the proving workflow - `skills for-app`, `skills search`, and `skills list` return top-level `skills` and `count` - `skills get` returns a top-level `skill` - `bundled-skills list` returns top-level `skills`, `count`, and `installedDir` - `bundled-skills list` includes `androperator-agent-orientation`, `androperator-upgrade`, `androperator-skill-author-by-agent-discovery`, and `androperator-skill-author-by-recording` in `skills[].name` - packaged Androperator bundled skills under `~/.agents/skills/` are real directories, not symlinks For MCP: ```bash androperator mcp serve ``` Check: - the process starts without printing normal CLI help - the process remains attached to stdio for the MCP client ## Read Next | Topic | Page | | --- | --- | | Install and device readiness | [Setup](setup.md) | | Raw observe / decide / act loop | [Quickstart](quickstart.md) | | Runtime-skill registry and wrapper behavior | [Skills Overview](skills/overview.md) | | Authoring-workflow install and current boundaries | [Authoring](skills/authoring.md) | | MCP client setup and tool surface | [MCP Server](api/mcp.md) | --- # Quickstart ## Before You Start This page assumes a working Androperator installation: CLI installed, device connected, Operator APK installed, and `androperator doctor` returning `"criticalOk": true`. If you have not reached that state yet, complete [Setup](setup.md) first. If you are choosing between runtime-skill discovery, MCP, and direct CLI automation after install, read [Host Agent Orientation](host-agents.md) first. This page starts after that choice and focuses on direct observe / decide / act execution. --- ## The Automation Loop Androperator is a deterministic actuator. The agent reasons and decides; Androperator executes and returns structured data. Every automation follows the same three-step loop: ``` Observe -> Decide -> Act ``` 1. **Observe** - take a UI snapshot to read the current device state 2. **Decide** - parse the XML hierarchy to find the right node to target 3. **Act** - send an execution payload with the next action, wait for the result Repeat until the task is done. **Targeting rule:** do not guess selectors. If the next action needs a target, take a fresh `snapshot`, derive the selector from the current hierarchy, and then act. A guessed label failing is not an Androperator bug - it means the target was not proven from the current screen state. --- ## Step 1: Observe The canonical observation action is `snapshot`. Run it with the built-in `snapshot` command: ```bash androperator snapshot --device ``` On success, the result envelope contains the XML hierarchy in `envelope.stepResults[0].data.text`. Success conditions to check before proceeding: ```json { "envelope": { "status": "success", "stepResults": [ { "actionType": "snapshot", "success": true, "data": { "text": "\n..." } } ] } } ``` **Branch rule:** only proceed when `envelope.status == "success"` and `stepResults[0].success == true`. If `data.text` is missing, Node converts the step to `SNAPSHOT_EXTRACTION_FAILED`. See [Snapshot Format](api/snapshot.md) for recovery. --- ## Step 2: Decide Parse the XML to find the node you want to act on. The key attributes for targeting are: | Attribute | Use for | | --- | --- | | `text` | Matching visible label text | | `resource-id` | Stable structural identifiers like `android:id/title` | | `content-desc` | Accessibility labels when text is empty | | `clickable` | Only `clickable="true"` nodes can be tapped directly | | `scrollable` | Identifies containers to target for scroll actions | | `bounds` | Screen coordinates in `[x1,y1][x2,y2]` form | For example, in a Settings hierarchy the "Connections" row appears as: ```xml ``` To tap this row, target the child title text. Androperator will resolve the nearest `clickable` ancestor if the matched node is not itself clickable. Selector to use: ```json { "textEquals": "Connections", "resourceId": "android:id/title" } ``` See [Selectors](api/selectors.md) for the full `NodeMatcher` contract. --- ## Step 3: Act Send an execution payload via `androperator exec`. The payload lists one or more actions in sequence. Androperator dispatches them in order and returns a single result envelope. Example - click the "Connections" row, wait for navigation, then take a snapshot: ```json { "commandId": "nav-to-connections", "taskId": "nav-to-connections", "source": "agent-loop", "expectedFormat": "android-ui-automator", "timeoutMs": 15000, "actions": [ { "id": "click-1", "type": "click", "params": { "matcher": { "textEquals": "Connections", "resourceId": "android:id/title" } } }, { "id": "wait-1", "type": "wait_for_navigation", "params": { "expectedPackage": "com.android.settings", "timeoutMs": 5000 } }, { "id": "snap-2", "type": "snapshot" } ], "mode": "direct" } ``` Save this as `payload.json` and run: ```bash androperator exec --device payload.json ``` Success conditions: - `envelope.status == "success"` - `envelope.stepResults` has three entries, all with `success: true` - `envelope.stepResults[2].data.text` contains the Connections screen hierarchy **Branch rule:** if any step fails, the envelope still returns but the failed step has `success: false`. Check `envelope.stepResults[i].success` individually, not just the top-level status. See [Errors](api/errors.md) for recovery by code. --- ## Putting It Together A complete agent sequence for reading the Android version: ### 1. Pre-flight ```bash androperator doctor --device # Require: "criticalOk": true ``` ### 2. Open Settings and observe ```json { "commandId": "open-settings", "taskId": "open-settings", "source": "agent-loop", "expectedFormat": "android-ui-automator", "timeoutMs": 20000, "actions": [ { "id": "open-1", "type": "open_app", "params": { "applicationId": "com.android.settings" } }, { "id": "wait-1", "type": "wait_for_navigation", "params": { "expectedPackage": "com.android.settings", "timeoutMs": 5000 } }, { "id": "snap-1", "type": "snapshot" } ], "mode": "direct" } ``` ### 3. Read snapshot, find "About phone" Parse `stepResults[2].data.text`. Search for a node with `resource-id="android:id/title"` and `text="About phone"`. If it is not visible, scroll down the `recycler_view` and take another snapshot. Scroll payload (use when the target row is below the visible area): ```json { "commandId": "scroll-settings", "taskId": "scroll-settings", "source": "agent-loop", "expectedFormat": "android-ui-automator", "timeoutMs": 10000, "actions": [ { "id": "scroll-1", "type": "scroll", "params": { "direction": "down", "container": { "resourceId": "com.android.settings:id/recycler_view" } } }, { "id": "snap-2", "type": "snapshot" } ], "mode": "direct" } ``` ### 4. Navigate and read the value Once "About phone" is visible, click it and snapshot the next screen to find the "Android version" row: ```json { "commandId": "read-android-version", "taskId": "read-android-version", "source": "agent-loop", "expectedFormat": "android-ui-automator", "timeoutMs": 20000, "actions": [ { "id": "click-about", "type": "click", "params": { "matcher": { "textEquals": "About phone", "resourceId": "android:id/title" } } }, { "id": "wait-about", "type": "wait_for_navigation", "params": { "expectedPackage": "com.android.settings", "timeoutMs": 5000 } }, { "id": "snap-3", "type": "snapshot" } ], "mode": "direct" } ``` Parse `stepResults[2].data.text` for the "Android version" row. The value appears as the `text` attribute of the summary node adjacent to an `android:id/title` node with `text="Android version"`. --- ## What to Read Next | Topic | Page | | --- | --- | | All action types and parameters | [Actions](api/actions.md) | | Selector syntax for node matching | [Selectors](api/selectors.md) | | Snapshot XML format and annotated example | [Snapshot Format](api/snapshot.md) | | Full result envelope contract | [API Overview](api/overview.md) | | Error codes and recovery steps | [Errors](api/errors.md) | | Scrolling and multi-step navigation patterns | [Navigation Patterns](api/navigation.md) | | Environment variable controls | [Environment Variables](api/environment.md) | --- # API # API Overview ## Purpose Define the smallest end-to-end contract an agent needs to use Androperator correctly: the execution payload, the CLI success wrapper, the `[Androperator-Result]` envelope, and the exact fields to branch on. ## What Androperator Is Androperator is a deterministic Android actuator. The agent is the planner. Androperator accepts an explicit execution payload, validates it, resolves one target device and Operator package, dispatches the actions, and returns a structured result. This page is intentionally narrow: - Use [Actions](actions.md) for action-by-action parameter semantics - Use [Selectors](selectors.md) for `NodeMatcher` and selector flags - Use [Errors](errors.md) for exact error codes and recovery - Use [Devices](devices.md) for target selection rules - Use [Daemon](daemon.md) for background daemon lifecycle commands ## Surface Terminology Androperator exposes several public surfaces. Use the precise surface name when you document, call, or parse behavior: | Surface | What it is | Canonical owner | | --- | --- | --- | | CLI command | A top-level shell command such as `snapshot`, `click`, or `skills`. | [CLI Reference](cli.md) for command lookup; behavior lives on the linked owner page. | | CLI subcommand | A nested shell command such as `skills run`, `recording export`, or `emulator provision`. | The subsystem page, such as [Skills CLI](../skills/cli.md) or [Recording](recording.md). | | Execution action | A JSON action inside `ExecutionInput.actions[]`, such as `click` or `open_app`. | [Actions](actions.md). | | Node contract | A TypeScript-backed data shape accepted or returned by the Node package. | This page for execution payload and [result envelope](#result-envelope); feature pages for narrower contracts. | | Serve endpoint | An HTTP or SSE route exposed by `androperator serve`, such as `POST /execute`. | [Serve API](serve.md). | | MCP tool | A stdio MCP tool exposed by `androperator mcp serve`, such as `snapshot` or `execute`. | [MCP Server](mcp.md). | | Selector | A `NodeMatcher` object or CLI selector flags used to find UI nodes. | [Selectors](selectors.md). | | Error code | A stable Node-side code from `apps/node/src/contracts/errors.ts`, or a documented feature-specific code. | [Errors](errors.md) for Node codes; feature pages for feature-specific codes. | | Result envelope | The `[Androperator-Result]` terminal envelope emitted by the Operator and wrapped by Node. | [Result Envelope](#result-envelope). | ## CLI Output Format CLI commands return JSON by default. Agents should parse stdout directly with `JSON.parse()` unless they intentionally requested `--output pretty` for a human-readable rendering. Accepted output-format forms: | Form | Behavior | | --- | --- | | no output flag | JSON output | | `--output json` | preferred explicit JSON output | | `--format json` | alias for `--output json` | | `--json` | alias for `--output json` | | `--output pretty` | pretty-printed human-readable output | ## Readiness Gate Before treating a target as ready for interactive automation, use [Doctor](doctor.md) and require: - exit code `0` - `criticalOk == true` - `readiness.device.interactive.status == "pass"` That doctor check exposes structured evidence: - `deviceLocked` - `screenOn` - `userUnlocked` If doctor reports `DEVICE_NOT_INTERACTIVE`, recover the device state first and rerun doctor before moving on to execution commands. ## Execution Payload Authoritative source: `apps/node/src/contracts/execution.ts` Top-level execution fields: | Field | Type | Meaning | | --- | --- | --- | | `commandId` | `string` | Caller-generated correlation id for the whole run. | | `taskId` | `string` | Caller-generated task id. | | `source` | `string` | Caller label, such as `serve-api` or `androperator-action`. | | `expectedFormat` | `"android-ui-automator"` | Required constant. | | `timeoutMs` | `number` | Execution-level timeout for the whole payload. Current Node limits require `1000 <= timeoutMs <= 120000`. | | `actions` | `ExecutionAction[]` | Ordered action list. | | `mode` | `"artifact_compiled" | "direct"` | Optional runtime mode marker. | Each action has: | Field | Type | Meaning | | --- | --- | --- | | `id` | `string` | Step correlation id. | | `type` | `string` | Canonical action type such as `click` or `snapshot`. | | `params` | `ActionParams` | Optional action-specific parameters. | Minimum valid payload example: ```json { "commandId": "cmd-001", "taskId": "task-001", "source": "agent-loop", "expectedFormat": "android-ui-automator", "timeoutMs": 30000, "actions": [ { "id": "snap-1", "type": "snapshot" } ], "mode": "direct" } ``` Success conditions for a valid payload before dispatch: - `expectedFormat` is exactly `"android-ui-automator"` - `timeoutMs` is between `1000` and `120000` milliseconds - `actions` is non-empty - every `actions[i].type` is a supported canonical action type after alias normalization; the only accepted on-screen log aliases are exact lower-case `on_screen_log_set` and `on_screen_log_clear` - on a live execution path, `commandId` and `taskId` are echoed back in the result envelope for correlation ## Input Normalization Stored payloads should use canonical field names and canonical action types. On input, Node normalizes a small alias set before schema validation. Accepted top-level execution key aliases: | Alias | Canonical field | | --- | --- | | `command_id` | `commandId` | | `task_id` | `taskId` | | `expected_format` | `expectedFormat` | | `timeout_ms` | `timeoutMs` | For action-type aliases and parameter aliases such as `package` -> `applicationId`, `url` -> `uri`, and `selector` -> `matcher`, use [Actions](actions.md). The Node input boundary accepts exact lower-case `on_screen_log_set` and `on_screen_log_clear` aliases for the canonical on-screen log action types, but does not accept case or whitespace variants or parameter aliases for those actions. For raw matcher-field aliases such as `resource_id` and `content_desc`, use [Selectors](selectors.md). Verification pattern: ```bash androperator exec --validate-only --payload '{"command_id":"cmd-001","task_id":"task-001","source":"docs","expected_format":"android-ui-automator","timeout_ms":30000,"actions":[{"id":"snap-1","type":"snapshot"}]}' ``` Success condition: - exit code `0` - top-level `ok == true` - `execution.commandId == "cmd-001"` - `execution.taskId == "task-001"` - `execution.expectedFormat == "android-ui-automator"` - `execution.timeoutMs == 30000` - `execution.actions[0].type == "snapshot"` CLI payload-source aliases accepted by `androperator exec`: - `--payload` is canonical - `--execution`, `--input`, and `--file` are accepted aliases for the same argument ## Execution Limits Current Node-side validation limits are: - at most `50` actions per execution - at most `64000` serialized payload bytes - `timeoutMs` must stay in `1000..120000` If validation fails on limits, shorten the action list, split a large workflow into multiple executions, or avoid embedding oversized inline artifacts in the payload. ## Result Envelope Authoritative source: `apps/node/src/contracts/result.ts` The Android runtime emits a `[Androperator-Result]` envelope. The Node CLI wraps that envelope in a top-level success object for most device commands: ```json { "envelope": { "commandId": "cmd-001", "taskId": "task-001", "status": "success", "stepResults": [ { "id": "snap-1", "actionType": "snapshot", "success": true, "data": { "text": "" } } ], "error": null }, "deviceId": "", "terminalSource": "androperator_result", "isCanonicalTerminal": true } ``` Inside that wrapper, the canonical envelope shape is: ```json { "commandId": "", "taskId": "", "status": "success", "stepResults": [ { "id": "", "actionType": "", "success": true, "data": {} } ], "error": null, "errorCode": null, "hint": "" } ``` Field meanings: Stable field anchors: - `commandId` - `taskId` - `status` - `stepResults` - `stepResults[].id` - `stepResults[].actionType` - `stepResults[].success` - `stepResults[].data` - `error` - `errorCode` - `hint` | Field | Meaning | | --- | --- | | `status` | Top-level outcome after Node post-processing. `"failed"` means at least one step failed or the runtime returned a top-level failure. | | `stepResults[].success` | Per-step success bit. | | `stepResults[].data` | Action-specific fields remain strings. Host-added `extractionDiagnostics` is a bounded structured object; see [snapshot diagnostics](snapshot.md#extraction-diagnostics-and-recovery). | | `error` | Human-readable top-level failure summary. | | `errorCode` | Stable top-level code when available. Prefer this over matching `error`. Current runtime envelopes may also surface Android-emitted strings such as `SERVICE_UNAVAILABLE`, so do not assume this field is limited to the Node enum in `errors.ts`. | | `hint` | Optional recovery hint injected by Node. | Failure wrapper example from the CLI: ```json { "code": "EXECUTION_VALIDATION_FAILED", "message": "wait_for_navigation requires params.timeoutMs > 0", "details": { "path": "actions.0.params.timeoutMs", "actionId": "wait-1", "actionType": "wait_for_navigation" } } ``` Non-runtime success wrappers from `androperator exec`: ```json { "ok": true, "validated": true, "execution": { "commandId": "cmd-001", "taskId": "task-001", "source": "agent-loop", "expectedFormat": "android-ui-automator", "timeoutMs": 30000, "actions": [ { "id": "snap-1", "type": "snapshot" } ] } } ``` ```json { "ok": true, "dryRun": true, "plan": { "commandId": "cmd-001", "timeoutMs": 30000, "actionCount": 1, "actions": [ { "id": "snap-1", "type": "snapshot" } ] } } ``` ## Wrapper Differences Use the wrapper shape that matches the surface you called: | Surface | Success shape | Result-envelope rule | | --- | --- | --- | | CLI device execution command | `{ "envelope": ..., "deviceId": "...", "terminalSource": "androperator_result", "isCanonicalTerminal": true }` | Read `envelope` with the [result-envelope](#result-envelope) rules. | | `androperator exec --validate-only` | `{ "ok": true, "validated": true, "execution": ... }` | Pre-dispatch only; no result envelope exists. | | `androperator exec --dry-run` | `{ "ok": true, "dryRun": true, "plan": ... }` | Pre-dispatch only; no result envelope exists. | | Serve execution endpoint | `{ "ok": true, "deviceId": "...", "terminalSource": "...", "envelope": ... }` | Read `envelope` with the same [result-envelope](#result-envelope) rules. | | MCP execution-backed tool | Tool-specific `structuredContent` containing action output and usually `envelope`. | When present, read `envelope` with the same [result-envelope](#result-envelope) rules. | | Skills CLI command | Skill wrapper JSON with `skillResult`, `durationMs`, or feature-specific error fields. | Not a `[Androperator-Result]` envelope unless a skill chooses to expose one in its own result. | `isCanonicalTerminal` is a CLI wrapper field. HTTP serve execution responses do not include it. ## How To Branch On Results Branch in this order: 1. If the CLI returned a top-level object with `code` and no `envelope`, treat it as a host-side failure. Do not retry unchanged. 2. If `envelope.status == "failed"` and `envelope.errorCode` is present, branch on `envelope.errorCode`. 3. If `envelope.status == "failed"` and `envelope.stepResults` contains a failed step, branch on the first `stepResults[i].success == false` and inspect `stepResults[i].data.errorCode`, falling back to the legacy `data.error` code. 4. If `envelope.status == "success"` and every `stepResults[i].success == true`, treat the command as successful. 5. If the top-level object is `{ ok: true, validated: true, ... }` or `{ ok: true, dryRun: true, ... }`, treat it as a pre-dispatch contract success, not a device execution result. Exact machine-checkable success condition for most CLI device commands: - process exit code `0` - top-level JSON contains `envelope` - `envelope.status == "success"` - every `envelope.stepResults[i].success == true` - `terminalSource == "androperator_result"` - `isCanonicalTerminal == true` ## How `status` and `stepResults` Relate - A typed runtime terminal failure is authoritative. Node preserves its status, error code, original message, and collected steps, including a timeout between steps. - Otherwise, if any `stepResults[].success` is `false`, Node reconciles `status` to `"failed"` and sets `error` from the first failed step. - Otherwise, if all steps succeed, Node reconciles `status` to `"success"`, clears top-level error state, and removes `hint`. - Thrown failures stop the sequence and retain prior steps plus the failed step. Returned failed steps preserve the existing continue-sequence policy. - [Action receipts](actions.md#action-receipts-and-failure-evidence) report dispatch acceptance. Verify application postconditions with separate observations. - A top-level failure can also arrive with zero steps, for example when Android returns a failure envelope before a normal step list exists, such as `SERVICE_UNAVAILABLE`. - Android command timeout returns a failed envelope with `COMMAND_TIMEOUT` and collected steps. If Node cannot obtain that terminal envelope, the host may instead return `RESULT_ENVELOPE_TIMEOUT`; that transport uncertainty does not prove whether a mutation occurred. - Node may modify step data after the runtime returns. Examples: - `snapshot` success steps get `data.text` attached from extracted log output - missing snapshot text is converted into `SNAPSHOT_EXTRACTION_FAILED` - successful `take_screenshot` steps get `data.path` - successful pre-flight `close_app` steps are normalized to `data.application_id` ## Execution Flow 1. Agent constructs an execution payload with stable `commandId`, `taskId`, and ordered `actions`. 2. Node validates the payload size and action schema before any adb dispatch. 3. Node resolves one target device and one Operator package. 4. Node sends the payload to Android and waits for a `[Androperator-Result]` envelope. 5. Node post-processes known cases such as snapshot extraction, screenshot capture, settle warnings, and `close_app` normalization. 6. CLI commands return a JSON wrapper containing the envelope, `deviceId`, `terminalSource`, and `isCanonicalTerminal`. ## Worked Example Command: ```bash androperator snapshot --device ``` Success output shape: ```json { "envelope": { "commandId": "snapshot-1700000000000-abcd123", "taskId": "snapshot-1700000000000-abcd123", "status": "success", "stepResults": [ { "id": "snap", "actionType": "snapshot", "success": true, "data": { "text": "..." } } ], "error": null }, "deviceId": "", "terminalSource": "androperator_result", "isCanonicalTerminal": true } ``` Agent-side success test: - `envelope.status == "success"` - `envelope.stepResults[0].actionType == "snapshot"` - `envelope.stepResults[0].success == true` - `"text" in envelope.stepResults[0].data` ## Related Pages - [Actions](actions.md) - [Selectors](selectors.md) - [Errors](errors.md) - [Devices](devices.md) - [Doctor](doctor.md) - [Serve API](serve.md) - [Setup](../setup.md) --- # Actions For a complete discovery, strict selection, scroll, assertion, and capture workflow, see [scoped selection walkthrough](scoped-selection.md). ## Purpose Define the canonical `ExecutionAction.type` values, the exact parameters each action accepts, which values are validated by Node, and what success and failure data an agent can rely on. ## Sources - Canonical action types: `apps/node/src/contracts/aliases.ts` - Shared parameter shape: `apps/node/src/contracts/execution.ts` - Validation rules: `apps/node/src/domain/executions/validateExecution.ts` - CLI-built payload defaults: `apps/node/src/domain/actions/` and `apps/node/src/domain/observe/` - Android payload parsing: `apps/android/shared/data/operator/src/main/kotlin/androperator/operator/agent/AgentCommandParser.kt` - Android action/result behavior: `apps/android/shared/data/task/src/main/kotlin/androperator/task/runner/UiAction.kt` and `UiActionEngine.kt` - Android text-entry runtime behavior: `apps/android/shared/data/uitree/src/main/kotlin/androperator/uitree/UiTreeManagerAndroid.kt` ## General Rules | Rule | Meaning | | --- | --- | | Canonical action names only | Stored payloads should use canonical types such as `open_uri`, `wait_for_node`, and `take_screenshot`. Input aliases are normalized before validation. The on-screen log aliases are exact input values, while their parameter keys remain canonical-only. | | Canonical payload keys still win | Node accepts common input aliases such as snake_case top-level keys, `package` for `applicationId`, `url` for `uri`, `selector` for `matcher`, and `value` for `text`, but the normalized payload always uses the canonical field names. The on-screen log actions intentionally reject these parameter aliases. | | `params` is optional at the schema level | Action-specific validation then decides whether it is actually required. | | Selectors live on a separate page | `matcher`, `container`, `expectedNode`, and `labelMatcher` all use the [Selectors](selectors.md) `NodeMatcher` contract. | | `StepResult.data` is a string map | Node may attach known keys such as `text`, `path`, `warn`, `application_id`, `error`, or `message`, but most actions do not have a richer static success schema. | | CLI coverage is narrower than raw JSON | Some advanced fields in `ActionParams` are accepted only through `androperator exec` JSON, not through flat CLI flags. | | Runtime details are not always Node guarantees | When this page calls out Android-returned success keys, treat them as current runtime behavior verified from Android code, not as a stricter Node-side schema guarantee. | ## Action receipts and failure evidence An accepted click, text operation, swipe, drag, or scroll dispatch is evidence of the Android attempt. It does not verify navigation, persisted state, or any application postcondition. Follow it with a wait, query, read, or snapshot that checks the specific expected state. A wait for a label already present before the click cannot prove navigation. With the v0.10 Operator, selector-targeted click, text, and scroll actions add these string-valued fields to `data`: | Field | Meaning | | --- | --- | | `target` | Serialized [NodeSummary](selectors.md) for the actual dispatch node, from that attempt's capture. Omitted when no target was resolved. | | `matched_target` | Originally selected NodeSummary when click fallback dispatches to an ancestor or uses a coordinate gesture. | | `candidate_count` | Base-10 count from the selector resolution used for dispatch. | | `dispatch_method` | `accessibility_action` for Android accessibility operations (including service text-input APIs), `coordinate_gesture` for a gesture, or `none` before dispatch. | | `dispatch_accepted` | `"true"` or `"false"`. Gesture acceptance is recorded when Android accepts dispatch, before the asynchronous completion callback. | | `elapsed_ms` | Base-10 elapsed milliseconds from the Android monotonic clock, including resolution and settling. | Coordinate clicks report `coordinate` as serialized JSON `{ "x": 100, "y": 200 }` and omit `target` and `candidate_count`. A failed pre-dispatch action reports `dispatch_method: "none"` and `dispatch_accepted: "false"`. Receipts do not add a copy of the entered text. For bounded scroll searches, the receipt describes the last dispatch; `scrolls_executed` counts the loop's gestures. If the target is already visible, no dispatch is claimed. Thrown action failures stop the sequence and retain all completed steps plus one failed step with its original `id` and `actionType`. Failed-step `errorCode` and top-level `errorCode` identify the failure; `error` preserves its message. Command timeout or cancellation retains collected evidence and emits one terminal result. Cancellation still stops execution. Existing actions that *return* a failed step continue to subsequent actions; Node still reports the execution as failed. Returned failed steps add `data.errorCode` while retaining their legacy `data.error` code. This sequence policy is unchanged. Missing application hierarchies include serialized `diagnostics` JSON with `serviceAvailable`, `rootAvailable`, `windowCount`, and `foregroundPackage`. Unavailable service/window metadata is `null`; a known missing root is `false`. These observations do not require an application root or select another window. Raw on-screen log actions remain usable without an application hierarchy. Migration: receipts require the matching v0.10 Operator. Parse JSON fields explicitly; `StepResult.data` remains a string map. Coordinate receipt consumers must parse the new JSON object rather than the older coordinate display string. Handle the new scroll outcomes below instead of assuming unchanged content is an edge. No mutation is replayed to obtain a receipt or recover from a failed post-dispatch observation. ## Retry Object Shape Several actions accept `retry`, `scrollRetry`, or `clickRetry` objects in raw `androperator exec` JSON. Node accepts these fields as part of `ActionParams`, and Android parses them into a retry policy with these keys: ```json { "maxAttempts": 4, "initialDelayMs": 400, "maxDelayMs": 2000, "backoffMultiplier": 2, "jitterRatio": 0.15 } ``` Meaning: - `maxAttempts` counts the initial attempt, so `1` means no retry. - `initialDelayMs` is the delay before the first retry. - `maxDelayMs` caps exponential backoff growth. - `backoffMultiplier` must be `>= 1.0`. - `jitterRatio` must be in `[0.0, 1.0]`. - Android clamps `maxAttempts` to `1..10`. - Android clamps `initialDelayMs` to `0..30000`. - Android clamps `maxDelayMs` to `initialDelayMs..60000`. - Android clamps `backoffMultiplier` to `1.0..5.0`. - Android clamps `jitterRatio` to `0.0..1.0`. - if you omit a retry object, Android applies an action-specific default such as `UiReadiness`, `UiScroll`, `AppLaunch`, `AppClose`, or `None`. ## Canonical Types And Input Aliases Canonical public action types: ```text open_app open_uri close_app start_recording stop_recording wait_for_node click scroll_and_click scroll scroll_until read_text query_ui enter_text snapshot take_screenshot sleep press_key wait_for_navigation read_key_value_pair set_on_screen_log clear_on_screen_log show_toast cancel_toast ``` Input aliases normalized by Node before validation: | Alias | Canonical type | | --- | --- | | `open_url` | `open_uri` | | `tap` | `click` | | `press` | `click` | | `wait_for`, `find`, `find_node` | `wait_for_node` | | `read` | `read_text` | | `snapshot_ui` | `snapshot` | | `screenshot`, `capture_screenshot` | `take_screenshot` | | `type_text`, `text_entry`, `input_text` | `enter_text` | | `key_press` | `press_key` | | `on_screen_log_set` | `set_on_screen_log` | | `on_screen_log_clear` | `clear_on_screen_log` | Common payload-key aliases also accepted on input: - top-level execution keys: `command_id`, `task_id`, `expected_format`, `timeout_ms` - app/package fields: `package`, `package_id`, `application_id`, `app`, `app_id` -> `applicationId` - URI field: `url` -> `uri` - matcher fields: `selector`, `node`, `element` -> `matcher` - raw matcher-object fields: `id`, `resource_id`, `text`, `text_contains`, `content_desc`, `content_desc_contains`, `description`, `description_contains`, `accessibility_label`, `accessibility_label_contains` - text-entry field: `value` -> `text` - screenshot path fields: `file`, `filePath`, `output_path` -> `path` - navigation fields: `expected_package`, `expected_node`, `timeout_ms` - open_app fields: `skip_navigation_wait`, `navigation_timeout_ms` - label selector fields: `label_matcher`, `label_selector` The on-screen log actions have a deliberately narrow input-alias rule: - Stored payloads and result `actionType` values use canonical `set_on_screen_log` and `clear_on_screen_log`. - At the Node input boundary, exact lower-case `on_screen_log_set` and `on_screen_log_clear` normalize to those canonical types before validation and dispatch. - Case changes and surrounding whitespace are rejected for both canonical types and aliases. - Their `params` objects accept only the fields documented below and do not translate generic keys such as `value` to `text`. ## `query_ui` Read-only structured inspection from one fresh Android tree capture. The CLI is `androperator query`; the named MCP tool is `query_ui`. All use the Android resolver shared with existing node-targeted actions. | Parameter | Default | Contract | | --- | --- | --- | | `matcher` | omitted | Optional [NodeMatcher](selectors.md); omit to match all eligible nodes. An explicit empty object is invalid. | | `visibility` | `"on_screen"` | `"on_screen"` or `"all"` | | `limit` | `100` | Integer from `1` through `1000` | Queries do not wait for navigation to settle. After a navigation action, use `androperator wait` with the expected destination selector (MCP: `wait`; raw: `wait_for_node`), then query. Zero matches describe that capture only; they do not prove that a destination has finished loading. A wait is also a separate capture, so callers must still inspect the subsequent query result. If Android supplies no hierarchy, the envelope fails with `errorCode="UI_TREE_UNAVAILABLE"`. Completed steps and the failed `query_ui` step are retained, and later actions do not run. Failed-step data contains `errorCode`, a human-readable `error`, and serialized JSON `diagnostics` with `serviceAvailable`, `rootAvailable`, `windowCount`, and `foregroundPackage`. Unknown facts are `null`; `rootAvailable` is `false` for the failed capture. No `data.query` is emitted. A screenshot can remain available when accessibility hierarchy access is unavailable. This error does not identify the platform cause or promise that retrying will expose a restricted screen. Named MCP returns the same error code and envelope. Zero, one, or multiple matches all succeed. `data.query` is a serialized JSON string with this shape: ```json { "schemaVersion": 1, "snapshotId": "observation-local-id", "capturedAt": "2026-01-01T00:00:00Z", "totalMatches": 1, "returnedCount": 1, "truncated": false, "nodes": [{ "nodePath": "0.2", "parentPath": "0", "resourceId": "example:id/switch", "className": "android.widget.Switch", "role": "switch", "label": "", "contentDescription": null, "bounds": {"left": 10, "top": 30, "right": 110, "bottom": 130}, "visibleToUser": true, "onScreen": true, "enabled": true, "clickable": true, "checkable": true, "checked": false, "selected": false, "scrollable": false, "accessibilityDataSensitive": false }] } ``` `totalMatches` counts nodes before the limit, including nodes with blank labels. `returnedCount` is the array length. `truncated` means the limit omitted whole nodes. Unavailable state stays `null`, distinct from `false`. `clickable` reports the platform node's clickability when captured, rather than inherited ancestor clickability used by legacy action dispatch. `accessibilityDataSensitive` reports Android's per-node accessibility-data flag at capture time. The flag is available on Android 14 (API 34) and later: | Value | Meaning | | --- | --- | | `true` | Android reports the node's accessibility data as sensitive. | | `false` | Android reports the node's accessibility data as non-sensitive. | | `null` | The device runs an earlier Android version, the node is a fallback, or the flag could not be read. | Queries emit this field even when it is null. When consuming a payload with the field absent, treat it as unknown. Queries and XML capture do not require API 34; only this flag does. A filtered query reports only its returned nodes, so use an unfiltered query to inspect root sensitivity. Raw XML emits `accessibility-data-sensitive="true"` or `"false"` when known, and omits the attribute when unknown. XML and queries capture independently; compare stable fixture nodes, not observation paths. Sensitivity does not change matching, success, redaction, or export behavior. It does not establish private browsing, screenshot protection, password status, or whether content is safe to share. Verify browser-mode indicators and behavior separately. Nodes are in preorder. Paths use child indices in the captured `UiNode` tree, rooted at `"0"`, and retain their original indices across visibility filtering. The root's `parentPath` is `null`. Paths and snapshot IDs are observation-local; they are neither stable cross-capture IDs nor valid action targets. `capturedAt` is the APK's UTC timestamp immediately after tree capture. XML is a separate capture with no guaranteed shared node identity. `visibleToUser` is the platform flag. `onScreen` follows the existing action eligibility rule: positive normalized bounds, platform visibility, screen intersection, and ancestor pruning. The existing root-retention exception remains: the root is retained even when ineligible, with its descendants pruned. Neither flag proves visual non-occlusion. `all` includes offscreen and hidden captured nodes; their `onScreen` value still reports the same action eligibility. A UTF-8 `data.query` payload above 256 KiB fails with `PAYLOAD_TOO_LARGE`; JSON is never cut to fit. Reduce `limit` or narrow the matcher. This response guard is separate from the execution request size limit. Raw XML snapshots remain available and add `visible-to-user` without restructuring the hierarchy. ```bash androperator query --device --operator-package com.androperator.operator.dev --visibility all --limit 100 androperator query --matcher-json '{"descendant":{"textEquals":"Display"}}' ``` The CLI accepts each of `--limit` and `--visibility` at most once. Repeating either flag, even with the same value, returns a structured `USAGE` error and exit code `1` before device execution. ### Runnable Node consumer From a repository checkout, build and run the tested [query consumer example](https://github.com/androperator/androperator/blob/main/apps/node/src/examples/query-consumer.ts): ```bash npm --prefix apps/node ci npm --prefix apps/node run build node apps/node/dist/examples/query-consumer.js --device --operator-package com.androperator.operator.dev --visibility all --limit 1000 ``` The example invokes the CLI built in that checkout. It accepts query flags and omits the matcher by default for all-node discovery. An explicit `--matcher-json '{}'` (or `--selector '{}'`) is invalid; remove that flag and its value to discover all eligible nodes. Other node-targeted actions still require an appropriate [selector](selectors.md). Before returning an inventory, the consumer checks the process exit, signal and spawn error; canonical terminal evidence; successful envelope and every step; and exactly one `query_ui` step with ID `query`. It parses that step's string `data.query`, validates schema version 1 and node field types, checks count consistency, and rejects truncation. `consumeQuery(output, queryStepId)` can select an explicitly named step when adapting the local example to a multi-step response. It is example-local validation, not an exported SDK accessor. Success prints `commandId`, `taskId`, the decoded `query`, and the original process output in `diagnostics`. Failure exits with code 1 and prints a message and the original output to stderr, including any available envelope and IDs. Preserve those diagnostics when investigating failures. Unknown nullable states remain null; an omitted `accessibilityDataSensitive` remains unknown. To observe refusal of a partial inventory on a screen with multiple nodes: ```bash node apps/node/dist/examples/query-consumer.js --device --operator-package com.androperator.operator.dev --visibility all --limit 1 ``` A truncated inventory cannot prove absence or uniqueness. Increase the limit (up to 1000) or narrow the matcher, recognizing that a filtered result only covers that filter. Even a complete result describes one capture and its visibility scope, not future state or completion of navigation. Zero matches are valid for that capture. The original response remains available on both success and failure; the canonical envelope and string payload are unchanged. `--matcher-json` and `--selector` name the same JSON input and are mutually exclusive with simple selector flags (`--text`, `--text-contains`, `--id`, `--desc`, `--desc-contains`, `--role`). Omitting all selector flags matches all eligible nodes. ## Full Payload Example ```json { "commandId": "open-settings-and-snapshot", "taskId": "open-settings-and-snapshot", "source": "agent-loop", "expectedFormat": "android-ui-automator", "timeoutMs": 30000, "actions": [ { "id": "open-1", "type": "open_app", "params": { "applicationId": "com.android.settings" } }, { "id": "wait-1", "type": "wait_for_navigation", "params": { "expectedPackage": "com.android.settings", "timeoutMs": 5000 } }, { "id": "snap-1", "type": "snapshot" } ], "mode": "direct" } ``` Success condition for that payload: - `envelope.status == "success"` - every `envelope.stepResults[i].success == true` - `envelope.stepResults[2].actionType == "snapshot"` - `"text" in envelope.stepResults[2].data` ## Action Reference Non-strict first-match selection adds `data.selection_warning` when duplicate candidates are observed, with a hint to use `--strict` (`params.strict=true`). This advisory field does not change action success. See [duplicate-selection hints](selectors.md#duplicate-selection-hints). All node-targeted actions below support optional boolean `params.strict` and an optional `params.container` matcher: `click`, `enter_text`, `read_text`, `wait_for_node`, `scroll`, `scroll_until`, and `scroll_and_click`. See [strict selection](selectors.md#strict-action-selection) for action-specific absence, ambiguity, container, and compatibility rules. Coordinate clicks cannot use strict mode or a container. Strict failures return string-valued `data.error`, `data.candidate_count`, and serialized JSON in `data.candidates`. ### `click` | Field | Valid values | | --- | --- | | Required | exactly one of `params.matcher` or `params.coordinate` | | `matcher` | any non-empty `NodeMatcher` | | `coordinate` | `{ "x": = 0>, "y": = 0> }` | | `clickType` | optional string; CLI builders use `"default"`, `"long_click"`, or `"focus"` | | `retry` | optional retry object in raw `exec` JSON; Android defaults to `UiReadiness` | Rules: - `matcher` and `coordinate` are mutually exclusive. - `clickType = "focus"` is invalid with `coordinate`. - CLI defaults to `"default"` and omits the field from the payload. Success data: - no Node-guaranteed success keys Common failures: - `EXECUTION_VALIDATION_FAILED` for missing selector, dual selector modes, invalid coordinates, or unsupported `clickType` combinations - runtime step failures such as `NODE_NOT_FOUND`, `NODE_NOT_CLICKABLE`, `GESTURE_FAILED` Example: ```json { "id": "click-1", "type": "click", "params": { "matcher": { "textEquals": "Settings" }, "clickType": "long_click" } } ``` ### `swipe` Move one finger immediately along a straight line between two screen coordinates, then release. This does not require a UI node or scrollable container. It has no initial hold and does not perform drag and drop. Use [drag](#action-drag) when the app needs a long press before movement. | Field | Valid values | | --- | --- | | `start` | required object with only integer `x` and `y`, each in `[0, 2147483647]` | | `end` | required object with only integer `x` and `y`, each in `[0, 2147483647]`; must differ from `start` | | `durationMs` | required integer in `[1, 10000]`; no default | Coordinates are physical screen pixels on the default display in its current orientation, with origin at the top left. Both endpoints must be inside the current display (`x < width`, `y < height`); Android checks these bounds before dispatch. The action accepts no selector, container, retry, or additional params. Gesture injection requires Android 7.0 (API 24) or later and an available accessibility service. ```bash androperator swipe --start 100 500 --end 800 500 --duration-ms 300 ``` Raw execution action (also usable through HTTP `POST /execute`): ```json { "id": "swipe-1", "type": "swipe", "params": { "start": { "x": 100, "y": 500 }, "end": { "x": 800, "y": 500 }, "durationMs": 300 } } ``` Success means Android's gesture completion callback fired. It does not prove that a snackbar was dismissed or content moved; inspect the resulting app state with a query or snapshot. The action dispatches once without automatic replay. Successful step data includes `start` and `end` as JSON-encoded coordinate objects, `duration_ms` as a string, and the standard `dispatch_method`, `dispatch_accepted`, and `elapsed_ms` receipt fields. A dispatched swipe uses `dispatch_method: "coordinate_gesture"`. Missing, invalid, or extra parameters fail Node validation with `EXECUTION_VALIDATION_FAILED`. Android reports `GESTURE_FAILED` if coordinates are outside the display, gesture injection is unavailable, or the gesture is rejected or cancelled. A gesture accepted and later cancelled retains `dispatch_accepted: "true"` on the failed step. Command timeout/cancellation retains dispatch evidence and does not replay the gesture; a gesture already accepted by Android may finish after the caller stops waiting. ### `drag` Press at a screen coordinate, hold without moving, move in a straight line while keeping the same pointer down, then release. Requires Android 8 (API 26) or later and an available accessibility service. Unlike `swipe`, this action has an explicit initial hold. Choose a hold long enough for the target app to enter its drag state. The required hold duration depends on the target app. | Field | Valid values | | --- | --- | | `start` | required object with only integer `x` and `y`, each in `[0, 2147483647]` | | `end` | required object with only integer `x` and `y`, each in `[0, 2147483647]`; must differ from `start` | | `holdDurationMs` | required integer in `[1, 10000]`; no default | | `moveDurationMs` | required integer in `[1, 10000]`; no default | Coordinates are physical screen pixels on the current default display, with origin at the top left. Android rejects endpoints outside its bounds before dispatch. No selector, grid position, path waypoints, retry, or extra params are accepted. Find the source item with a snapshot and start inside its bounds. Choosing a destination and interpreting the result belong in the agent or app-specific skill. ```bash androperator drag --start 600 1600 --end 200 1000 \ --hold-duration-ms 1200 --move-duration-ms 800 --device ``` The flat CLI defaults to a 30000 ms execution budget; `--timeout ` overrides it. Budget for the hold, movement, and scheduling overhead. In a multi-action execution, `timeoutMs` covers the entire sequence, not each gesture separately. For a local development Operator, also pass `--operator-package com.androperator.operator.dev` consistently on every command. Raw execution action, also usable through HTTP `POST /execute` and the MCP `drag` tool with the same four parameter fields: ```json { "id": "drag-1", "type": "drag", "params": { "start": { "x": 600, "y": 1600 }, "end": { "x": 200, "y": 1000 }, "holdDurationMs": 1200, "moveDurationMs": 800 } } ``` Success means Android completed the gesture, including pointer release. It does not prove a successful drop. Query or snapshot the resulting app state; check that the intended item reached the destination. This action provides a straight same-screen gesture; app-specific drop behavior is not guaranteed. Successful step data includes JSON-encoded `start` and `end`, string-valued `hold_duration_ms` and `move_duration_ms`, plus `dispatch_method`, `dispatch_accepted`, and `elapsed_ms`. A dispatched drag uses `dispatch_method: "coordinate_gesture"`. Once the hold is accepted, `dispatch_accepted` stays `"true"` even if movement fails. Invalid parameters produce `EXECUTION_VALIDATION_FAILED` at the Node boundary. Android reports `GESTURE_UNSUPPORTED` below API 26, or `GESTURE_FAILED` for out-of-display coordinates, an unavailable service, rejection, or platform cancellation. The execution deadline bounds missing or delayed callbacks and reports `COMMAND_TIMEOUT`. The action is never automatically replayed. On cancellation before movement, Android attempts to release the held pointer without moving it. Cleanup is best effort if the service or platform is unavailable. An already accepted movement can finish and release at its endpoint after command cancellation. Timeout and cancellation do not undo application effects; inspect current state before deciding whether to act again. ### `scroll` | Field | Valid values | | --- | --- | | Required | none | | `direction` | optional string in `down`, `up`, `left`, `right` | | `container` | optional `NodeMatcher` | | `distanceRatio` | optional number in `[0.0, 1.0]` | | `settleDelayMs` | optional number in `[0, 10000]` | | `findFirstScrollableChild` | optional boolean in raw `exec` JSON; Android defaults to `true` | | `retry` | optional retry object in raw `exec` JSON; Android defaults to `None` for plain scroll | Semantics: - if `direction` is omitted in raw JSON, Node validation allows omission - Android defaults omitted `direction` to `down` - the flat CLI always sets a direction explicitly - `container` scopes the scroll to a matched scrollable container - `distanceRatio` and `settleDelayMs` are advanced tuning fields for raw JSON execution - if `findFirstScrollableChild == true` and the matched container is not itself scrollable, Android walks down to the first scrollable descendant; strict mode requires that eligible descendant to be unique - `retry` covers pre-dispatch container resolution; an exception after dispatch does not replay the gesture Success and progress data: - `scroll_outcome`, `direction`, `distance_ratio`, `settle_delay_ms`, and optional `resolved_container` retain their existing names; dispatch receipts are described above. - `progress` is serialized JSON with `beforeSignature`, `afterSignature`, `comparable`, and `reason`. Available signatures are bounded SHA-256 hashes; raw node text is not included. Missing signatures are `null`. - Comparison re-resolves the same scoped container. Ambiguous or changed identity is not comparable, even if the screen appears to have moved. A container that remains identifiable but stops reporting `scrollable` can still produce comparable progress; eligibility loss alone is not `container_lost`. | `scroll_outcome` | Observation | Step success | | --- | --- | --- | | `moved` | Comparable signatures changed | `true` | | `no_movement` | Comparable signatures are unchanged | `true` | | `unknown` | Missing signatures or an ambiguous/incomparable container | `true` | | `container_lost` | Container or hierarchy disappeared after the gesture | `false` | | `gesture_failed` | Gesture was rejected or did not complete successfully | `false` | | `edge_reached` | Reserved for explicitly instrumented platform boundary evidence; the current runtime does not emit it | n/a | An accepted gesture may later be cancelled by Android. In that case `dispatch_accepted` remains `"true"`, while `scroll_outcome` is `gesture_failed`. Neither `no_movement` nor `unknown` proves the container is at an edge. Common failures: - `EXECUTION_VALIDATION_FAILED` for invalid `direction`, `distanceRatio`, or `settleDelayMs` - runtime step failures such as `CONTAINER_NOT_FOUND`, `CONTAINER_NOT_SCROLLABLE`, `GESTURE_FAILED` Example: ```json { "id": "scroll-1", "type": "scroll", "params": { "direction": "down", "container": { "resourceId": "android:id/list" }, "distanceRatio": 0.7, "settleDelayMs": 250 } } ``` ### `scroll_until` | Field | Valid values | | --- | --- | | Required | none at schema level; `matcher` becomes required when `clickAfter == true` | | `direction` | optional string in `down`, `up`, `left`, `right` | | `matcher` | optional `NodeMatcher` | | `container` | optional `NodeMatcher` | | `clickAfter` | optional boolean | | `distanceRatio` | optional number in `[0.0, 1.0]` | | `settleDelayMs` | optional number in `[0, 10000]` | | `maxScrolls` | optional integer in `[1, 200]` | | `maxDurationMs` | optional number in `[0, 120000]` | | `noPositionChangeThreshold` | optional integer in `[1, 20]` | | `findFirstScrollableChild` | optional boolean in raw `exec` JSON; Android defaults to `true` | | `clickType` | optional string in raw `exec` JSON; Android parses the same click types used by `click` | Semantics: - without `clickAfter`, the action scrolls until the target becomes visible or the loop terminates - with `clickAfter: true`, the same action requires `matcher` and turns into “scroll then click” - the flat CLI exposes only the core controls; advanced tuning requires raw JSON via `androperator exec` - Android defaults omitted `direction` to `down`, `distanceRatio` to `0.7`, `settleDelayMs` to `250`, `maxScrolls` to `20`, `maxDurationMs` to `10000`, `noPositionChangeThreshold` to `3`, and `findFirstScrollableChild` to `true` - `maxScrolls` is the hard cap on how many scroll steps Android will attempt - `maxDurationMs` is checked against monotonic elapsed time before each gesture; the current gesture and bounded settle/target checks may finish after that threshold, while the command timeout cancels execution - `noPositionChangeThreshold` stops the loop after that many consecutive `no_movement`, signature-only `unknown`, or rejected gestures; loss of the original container identity terminates with `CONTAINER_LOST` - after choosing a scroll container, target observations and the requested click stay within that original container's descendants, including for legacy unscoped searches; an exhausted search cannot be changed to success by a target outside that scope - an identifiable container that stops reporting `scrollable` still allows a revealed target to satisfy the search and the requested click to run once; if the target remains absent after bounded observation, the search terminates with `CONTAINER_NOT_SCROLLABLE` without scrolling a different container - initially visible targets retain legacy unscoped matching when neither strict selection nor a container is requested Success data: - no Node-guaranteed success keys Common failures: - `EXECUTION_VALIDATION_FAILED` for invalid direction or out-of-range tuning fields - runtime step failures such as `NODE_NOT_FOUND`, `CONTAINER_NOT_FOUND`, `CONTAINER_NOT_SCROLLABLE` Example: ```json { "id": "scroll-until-1", "type": "scroll_until", "params": { "direction": "down", "matcher": { "textEquals": "About phone" }, "maxScrolls": 25, "maxDurationMs": 10000, "noPositionChangeThreshold": 3 } } ``` ### `scroll_and_click` | Field | Valid values | | --- | --- | | Required | `matcher` | | `direction` | optional string in `down`, `up`, `left`, `right` | | `matcher` | required `NodeMatcher` | | `container` | optional `NodeMatcher` | | `clickAfter` | optional boolean in raw `exec` JSON; Android defaults it to `true` | | `maxSwipes` | optional integer in raw `exec` JSON; Android defaults it to `10` and clamps it to `[1, 50]` | | `distanceRatio` | optional number in raw `exec` JSON; Android defaults it to `0.7` and clamps it to `[0.0, 1.0]` | | `settleDelayMs` | optional number in raw `exec` JSON; Android defaults it to `250` and clamps it to `[0, 10000]` | | `findFirstScrollableChild` | optional boolean in raw `exec` JSON; Android defaults it to `true` | | `clickType` | optional string in raw `exec` JSON; Android parses the same click types used by `click` | | `scrollRetry` | optional retry object in raw `exec` JSON; Android defaults to `UiScroll` | | `clickRetry` | optional retry object in raw `exec` JSON; Android defaults to `UiReadiness` | Semantics: - this is the canonical action type produced by `scroll-until --click` and `scroll-and-click` - unlike raw `scroll_until`, this action is optimized for “scroll to target, then click target” - `maxSwipes` is the safety cap on how many swipes Android performs before failing - scroll and view refresh remain bounded by `maxSwipes`; mutations are not replayed after a post-dispatch failure - target observation, eligibility transitions, and final click scoping follow the same rules as [`scroll_until`](#action-scroll-until) - `clickRetry` applies only to the final click after the target is visible - setting `clickAfter: false` is accepted in raw `exec` JSON and makes Android stop after revealing the target, but the flat CLI does not emit that variant for `scroll_and_click` Success data: - no Node-guaranteed success keys Common failures: - `EXECUTION_VALIDATION_FAILED` if `matcher` is absent - runtime scroll or click failures, including `NODE_NOT_FOUND` Example: ```json { "id": "scroll-click-1", "type": "scroll_and_click", "params": { "matcher": { "textEquals": "Submit" }, "direction": "down" } } ``` ### `read_text` | Field | Valid values | | --- | --- | | Required | `matcher` | | `matcher` | required `NodeMatcher` | | `all` | optional boolean; when `true`, request all matches instead of the first match | | `container` | optional `NodeMatcher` | | `validator` | optional string; current validation adds special behavior only for `"regex"` | | `validatorPattern` | required non-empty valid regex string when `validator == "regex"` | | `retry` | optional retry object in raw `exec` JSON; Android defaults to `UiReadiness` | Semantics: - if `validator` is omitted, no validator-specific Node rule runs - if `validator == "regex"`, `validatorPattern` must exist and compile as a regex - other validator strings are accepted by the current Node schema, but this repo does not add extra Node-side validation semantics for them - current Android parser accepts only `temperature`, `version`, and `regex`; any other validator string is rejected at runtime - `all: true` asks Android to return all matching text values instead of only the first match Success data: - no Node-guaranteed success keys Common failures: - `EXECUTION_VALIDATION_FAILED` for missing `matcher` or invalid regex configuration - runtime failures such as `NODE_NOT_FOUND` Example: ```json { "id": "read-1", "type": "read_text", "params": { "matcher": { "textContains": "Order" }, "validator": "regex", "validatorPattern": "^ORD-[0-9]{6}$", "all": false } } ``` ### `read_key_value_pair` | Field | Valid values | | --- | --- | | Required | `labelMatcher` | | `labelMatcher` | required `NodeMatcher` | | `all` | optional boolean | | `retry` | optional retry object in raw `exec` JSON; Android defaults to `UiReadiness` | Semantics: - built by the flat `read-value` CLI command - uses a label matcher rather than a generic element matcher Success data: - no Node-guaranteed success keys Common failures: - `EXECUTION_VALIDATION_FAILED` when `labelMatcher` is absent Example: ```json { "id": "read-value-1", "type": "read_key_value_pair", "params": { "labelMatcher": { "textEquals": "Battery" }, "all": false } } ``` ### `enter_text` | Field | Valid values | | --- | --- | | Required | `matcher`, `text` | | `matcher` | required `NodeMatcher` | | `text` | required non-empty string | | `clear` | optional boolean | | `submit` | optional boolean | | `retry` | optional retry object in raw `exec` JSON; Android defaults to `UiReadiness` | Semantics: - `submit` defaults to `false` in the built-in CLI builders - `clear` defaults to `false` in the built-in CLI builders - Android uses an internal first-match-wins text-entry ladder while keeping the public `enter_text` shape unchanged - Android prefers the editable node `ACTION_SET_TEXT` route when it is available because that path already matches current replace-text semantics - on that `ACTION_SET_TEXT` route, `clear == true` first dispatches `ACTION_SET_TEXT("")`, then dispatches `ACTION_SET_TEXT` with the requested `text` - on that same `ACTION_SET_TEXT` route, `clear == false` or omitted keeps the existing single `ACTION_SET_TEXT` behavior - if the requested clear step fails on the `ACTION_SET_TEXT` route, Android stops before the real text set for that legacy strategy; on Android 13+ it can still continue to the accessibility input-connection fallback when that route is available, otherwise the action fails - on Android 13+ (`Build.VERSION_CODES.TIRAMISU`) when the legacy `ACTION_SET_TEXT` route is unavailable or does not complete successfully, Android can fall back to the accessibility input-connection path for custom editors - that API 33 fallback still preserves replace-style behavior by moving the cursor to the end, deleting preceding text, then committing the replacement text - that API 33 replace sequence also preserves `clear == true` semantics even though there is no separate public strategy flag - `submit == true` is best effort after successful text entry - on the legacy route, Android prefers `ACTION_IME_ENTER` when the node exposes it and falls back to a click when it does not - on the API 33 input-connection route, Android prefers `performEditorAction(...)` - if text entry succeeds but no truthful submit action is available, the step still succeeds and `submit` does not become a new hard-failure condition Success data: - Node does not declare a richer static schema here - `data.text`, `data.clear`, and `data.submit` retain the requested values; `submit` is a request, not an outcome - `data.text_entry` is `"accepted"` after Android accepts text entry - `data.submission` is `"not_requested"`, `"accepted"`, or `"unavailable"` - `data.submit_method` is `"not_requested"`, `"ime_action"`, `"click_fallback"`, or `"submit_unavailable"` `submission: "accepted"` pairs with `ime_action` or `click_fallback` and means Android accepted that action. A fallback click can merely focus the field. `submission: "unavailable"` pairs with `submit_unavailable` when no supported submission path succeeds, including rejected actions; text entry still succeeds. No submission request produces `not_requested` in both fields. These fields report action acceptance, not read-back verification of text or proof of navigation. After typing a URL or search query, the agent or browser skill must observe the destination before declaring navigation complete. Do not repeat text entry solely because submission is unavailable. Older Operators may omit these additive fields; absence means unknown, not submission success. Common failures: - `EXECUTION_VALIDATION_FAILED` for missing matcher or blank text - runtime failures such as `NODE_NOT_FOUND` - Android task-status failure payloads use `failure_point = set_text_failed` when the clear or text-set step cannot be completed Example: ```json { "id": "type-1", "type": "enter_text", "params": { "matcher": { "resourceId": "com.example:id/search" }, "text": "hello world", "clear": true, "submit": false } } ``` Verification pattern: ```bash androperator type "battery" --id "com.android.settings:id/search_src_text" --clear ``` Success conditions: - exit code `0` - `envelope.status == "success"` - `envelope.stepResults[0].actionType == "enter_text"` - `envelope.stepResults[0].success == true` - `envelope.stepResults[0].data.clear == "true"` Android live-route verification: - when validating against the debug operator on device, operator logs include `enter_text strategy= submit_method=` - on the API 33 route, warning-level logs can also include `enter_text strategy=api33_input_connection partial_failure reason=` when the fallback delete step succeeds but the final `commitText(...)` does not - current shipped strategy names are `legacy_action_set_text` and `api33_input_connection` ### `press_key` | Field | Valid values | | --- | --- | | Required | `key` | | `key` | case-insensitive string in `back`, `home`, `recents` | | `retry` | optional retry object in raw `exec` JSON; Android defaults to `None` | Success data: - no Node-guaranteed success keys Common failures: - `EXECUTION_VALIDATION_FAILED` for missing or unsupported key Example: ```json { "id": "press-1", "type": "press_key", "params": { "key": "back" } } ``` ### `wait_for_node` | Field | Valid values | | --- | --- | | Required | `matcher` | | `matcher` | required `NodeMatcher` | | `timeoutMs` | optional number; the current Android parser clamps a provided value to `1..120000`; when built by the CLI, it comes from `--timeout` | | `retry` | optional retry object in raw `exec` JSON; Android defaults to `UiReadiness` | Semantics: - the action-level `timeoutMs` is distinct from the execution-level `timeoutMs` - current Node validation does not add a stricter positivity check for this field - the builder inflates the execution timeout to `max(actionTimeout + 5000, 30000)` so the envelope does not expire before the wait finishes Success data: - no Node-guaranteed success keys Common failures: - `EXECUTION_VALIDATION_FAILED` when `matcher` is missing - runtime failure when the target never appears Example: ```json { "id": "wait-1", "type": "wait_for_node", "params": { "matcher": { "textEquals": "Settings" }, "timeoutMs": 5000 } } ``` ### `wait_for_navigation` | Field | Valid values | | --- | --- | | Required | at least one of `expectedPackage` or `expectedNode`, plus `timeoutMs` | | `expectedPackage` | optional non-empty string up to matcher-length limits | | `expectedNode` | optional `NodeMatcher` | | `timeoutMs` | required number in `(0, 30000]` | Semantics: - at least one navigation target must be present - the CLI builder inflates execution timeout to `max(timeoutMs + 5000, 30000)` Success data: - no Node-guaranteed success keys Common failures: - `EXECUTION_VALIDATION_FAILED` for missing target, missing timeout, or timeout above `30000` Example: ```json { "id": "wait-nav-1", "type": "wait_for_navigation", "params": { "expectedPackage": "com.android.settings", "timeoutMs": 5000 } } ``` ### `snapshot` | Field | Valid values | | --- | --- | | Required | none | | `retry` | optional retry object in raw `exec` JSON; Android defaults to `UiReadiness` | Semantics: - the old `format` parameter is explicitly rejected as removed - built-in builders set execution timeout to `30000` unless overridden - snapshots carry XML in canonical result step data with verified chunk transport for large envelopes; older Operators use command-tagged log extraction as described in [Snapshot Format](snapshot.md) Success data: - `data.text` contains the extracted XML hierarchy - `data.warn` may be added when a snapshot immediately follows `click` or `scroll_and_click` without an intervening sleep Common failures: - `SNAPSHOT_EXTRACTION_FAILED` - `RESULT_ENVELOPE_TIMEOUT` Example: ```json { "id": "snap-1", "type": "snapshot" } ``` ### `show_toast` Request a native Android text toast from the Operator. Use it for brief announcements such as starting a test run. Each call cancels the previous API-requested toast before submitting its replacement. The current API toast is shared across callers for that Operator instance and is separate from incidental Operator messages. | Parameter | Accepted values | Default | | --- | --- | --- | | `text` | Required string, 1-2048 UTF-16 code units, including a non-whitespace character | None | | `duration` | Exactly `"short"` or `"long"` | `"short"` | Text is preserved verbatim. Unknown fields, parameter aliases, blank text, null values, and numeric durations are rejected with `EXECUTION_VALIDATION_FAILED` at the Node boundary. There are no toast IDs, queue controls, custom styles, positions, buttons, or millisecond durations. Use [on-screen logs](on-screen-logs.md) for persistent information. ```json { "id": "announce-start", "type": "show_toast", "params": { "text": "Starting test run", "duration": "short" } } ``` Success means the Operator submitted the request to Android on its main thread. It does not prove visibility and does not wait for dismissal. Success step data contains exactly `{"submitted":"true","duration":"short"}` (or `"long"`), without echoing text. An Android submission exception fails the action with `ACTION_FAILED`. Android controls the actual duration, layout, and display. Background toasts are rate-limited; on Android 12 and newer with current target SDKs, text toasts show the app icon and at most two lines. Long input may be truncated. See the [Android Toast reference](https://developer.android.com/reference/android/widget/Toast) and [toast guidance](https://developer.android.com/guide/topics/ui/notifiers/toasts). CLI examples: ```bash androperator toast "Starting test run" androperator toast "Test run complete" --duration long androperator toast --cancel ``` Common flags include `--device `, `--operator-package `, `--timeout `, `--output json|pretty`, and `--no-daemon`. For local development, use `--operator-package com.androperator.operator.dev`. To send text beginning with `-`, put options first and use `toast -- "--literal text"`. The CLI validates before dispatch and does not automatically replay an uncertain dispatch. Raw `exec`, HTTP `/execute`, and MCP `execute` accept these actions in the usual execution envelope, retaining `commandId`, `taskId`, and action IDs. A toast can be the first action in a test sequence; later actions do not wait for it to disappear. ### `cancel_toast` Cancel the current API-requested toast, including one pending display. Omit `params` or pass exactly `{}`. `null` and all parameter fields are invalid. Cancellation is idempotent and succeeds when no API toast exists. It does not cancel another app's toast or an incidental Operator message. API toast ownership lasts for the Operator process; it does not survive a process restart. ```json { "id": "dismiss-announcement", "type": "cancel_toast" } ``` The CLI form is `androperator toast --cancel`, exclusive with text and `--duration`. Success step data is exactly `{"submitted":"true"}`. This acknowledges completion of the cancellation request, not observation that the toast has disappeared. ### `set_on_screen_log` Use this raw action to show one noninteractive diagnostic panel owned by the connected Operator accessibility service. Supply literal `text` or a live Android-resolved `template`. CLI conveniences are `on-screen-log set --text ` and `on-screen-log set --template