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

# Maestro

> Run a supported native flow through the Revyl driver and the genuine Maestro runtime.

The **Revyl Maestro runner** embeds Maestro's published YAML
parser and execution engine with a custom Revyl device driver. Maestro still
interprets the flow and evaluates its assertions; the driver sends supported
native operations through Revyl's authenticated device API and its existing
viewer control channel.

<Warning>
  This is a source-installed runner, not a plugin for stock `maestro test`. The
  native actions listed below have been verified against synthetic apps on
  staging Android emulators and iOS simulators. Viewer-channel text input has
  loopback integration coverage, not live-device verification. Supported and
  deployment-gated commands are distinct; this is not full Maestro
  compatibility.
</Warning>

For released device lifecycle commands, use the
[device quickstart](/infrastructure/quickstart). A session UUID or viewer URL
does not establish an ADB connection or make stock Maestro discover the device.

## How it connects

```text theme={null}
Your Maestro YAML flow
          |
Local Revyl runner + Maestro 2.10.0 runtime
          |
Revyl driver + authenticated device API / viewer control channel
          |
Existing Android emulator / iOS simulator session
```

The runner is a separate JVM application. It does not require local Android
SDK/ADB, Xcode, a Maestro device-side driver installation, or Revyl's AI test
runner. It does not provide the stock CLI's workspace selection, sharding, or
reporting features.

## Source access and session preparation

Get the [standalone Maestro driver](https://github.com/RevylAI/maestro-revyl-driver)
and follow its README to build the `revyl-maestro` launcher with Java 21 before
loading runtime credentials. There is no package registry release yet.
Repository access is required; contact [Revyl support](mailto:support@revyl.ai)
if you need access.

```bash theme={null}
git clone https://github.com/RevylAI/maestro-revyl-driver.git
cd maestro-revyl-driver
./gradlew --no-daemon installDist
```

Select an existing, running Revyl session with your app already installed and
launched. Keep its session UUID, and provide `REVYL_API_KEY` through the
runner's environment or your secret manager. Never put a key into the YAML
flow or command-line arguments. Session ownership is checked by the backend
on attachment and every proxied operation. Text input obtains its session-bound
control connection from the authenticated backend; do not supply a viewer or
worker URL yourself.

## Write a supported flow

The [sample flow](https://github.com/RevylAI/maestro-revyl-driver/blob/main/examples/smoke.yaml) requires an app whose initial screen contains a visible
`Continue` button. Tapping it must produce the visible text `Welcome`:

```yaml theme={null}
appId: com.example.app
---
- assertVisible: "Continue"
- tapOn: "Continue"
- assertVisible: "Welcome"
- assertNotVisible: "Continue"
- takeScreenshot: smoke
```

Replace the app ID, selectors, and expected outcome with your app's actual
values. The example deliberately omits `launchApp`: the app must already be
running. A saved screenshot is supporting evidence, not a replacement for an
assertion.

From the source package directory, run the built launcher against the exact
session:

```bash theme={null}
build/install/revyl-maestro/bin/revyl-maestro \
  --session YOUR_RUNNING_REVYL_SESSION_UUID \
  --platform android examples/smoke.yaml
```

Use `--platform ios` for an iOS session. This command interacts with a real
device when configured with real credentials. Keep the runner's exit code in
CI; parse errors, unsupported commands, failed assertions, and transport
failures must fail the job. Keep screenshots private.

## Supported native subset

The runner supports hierarchy-backed `assertVisible` and
`assertNotVisible`, single selector or coordinate taps, and PNG screenshots.
`extendedWaitUntil` polls an assertion, while `waitForAnimationToEnd` performs
a bounded screenshot comparison rather than proving a specific final state.
Assertions use the native accessibility hierarchy, not an independent visual
or occlusion check. Native resource IDs and text can differ between platforms;
adapt selectors rather than assuming an Android ID is an iOS accessibility ID.

Screenshot names must be unique simple names without paths or extensions.
`takeScreenshot: smoke` writes a private `.revyl-maestro/smoke.png` file;
existing files are rejected rather than overwritten.

| Command                              | Behavior and limits                                                                                                                                                                                              |
| ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `longPressOn`                        | A 3000-ms hold at an element center or native/percent point.                                                                                                                                                     |
| `tapOn` with `repeat`, `doubleTapOn` | An intentional sequence of individually acknowledged taps. Counts are 1–100; delays are 0–10000 ms, with at most 120000 ms total requested delay per sequence. This does not guarantee native double-tap timing. |
| `pressKey`                           | `Enter`, `Backspace`, and `Home`; `Back` and the `back` command are Android-only.                                                                                                                                |
| `eraseText`                          | Sends 1–100 Backspaces to current focus. Omitted count means 50 Backspaces, not a guarantee that the field is empty.                                                                                             |
| `setClipboard`, `copyTextFrom`       | Store text in Maestro's local clipboard variable, not the device's OS clipboard. Literal clipboard text is limited to 16 KiB UTF-8. See the text input limitations before using `pasteText`.                     |
| `inputText`, `pasteText`             | Use the existing viewer control channel on Android and iOS; no new runtime endpoint is required. Read the focus, completion, character, and privacy limitations below.                                           |
| `setLocation`                        | Sends finite latitude in −90…90 and longitude in −180…180 degrees. The route has loopback contract coverage; app-level live GPS behavior has not been verified.                                                  |
| `setDarkMode`                        | Explicit `enabled` or `disabled`; no toggle or appearance-state assertion.                                                                                                                                       |
| `openLink`                           | Plain absolute HTTP(S) or app-scheme links with `autoVerify: false` and `browser: false`. Uses system routing without forcing an app.                                                                            |

These platform claims concern Android emulators and iOS simulators, not physical
iOS devices. Native flows verified long press, repeated taps, focused deletion,
Enter, Home, appearance changes, and deep links on both platforms, plus Android
Back. Local clipboard commands also ran in those flows.

The complete flow is checked before attachment. The limits are 999 source
commands and 1000 expanded commands, counting each repeated tap or Backspace.
Optional commands, scripts/expressions, hooks, nested flows, general repeat/retry
blocks, `retryTapIfNoChange`, and `waitUntilVisible` are rejected. Unsupported
driver operations also fail explicitly during execution.

### Launch is intentionally narrow

Stock Maestro's default launch stops the app and changes permissions. The
current Revyl proxy does not provide that complete lifecycle contract. The
only supported form requests neither operation:

```yaml theme={null}
- launchApp:
    stopApp: false
    permissions: {}
```

Do not add clear-state/keychain flags or launch arguments. Preparing the app
before running the flow is the simpler path.

## Text input through the viewer channel

`inputText` and `pasteText` use Revyl's existing manual-input control channel
on Android emulators and iOS simulators. They do not depend on a new focused-input
endpoint or worker capability. `pasteText` sends the value from an earlier
`setClipboard` or `copyTextFrom`; it does not read the device's OS clipboard.
Literal and dynamically copied input must be nonempty valid Unicode, at most
16 KiB UTF-8. Control characters are rejected except tab, newline, and carriage
return. Passing this validation does not guarantee the device transport supports
every character.

The runner requests input at current focus without clearing or tapping. Android
uses the worker's text provider; iOS uses its paste path. These are the viewer's
existing behaviors, not a stronger native text contract:

* **Focus and selection:** the worker's `skip_tap` option permits a refocusing
  fallback when passthrough is unavailable. The adapter does not deliberately
  tap a field, but cannot guarantee caret or selection preservation on every
  worker. Focus the intended field before input and assert the resulting value.
* **Completion:** the runner waits for the matching completion acknowledgment,
  not just receipt. This means the handler returned successfully, not that the
  app's field value was verified. A missing, failed, or invalid acknowledgment
  stops the flow without replaying the input or sending later mutations.
* **Viewer interaction:** close the live viewer while running a flow with input.
  A connected viewer's WebRTC data channel can receive the acknowledgment instead
  of the runner's WebSocket. A timeout may mean text was already inserted;
  inspect the device before rerunning the flow.
* **Character support:** Android fallback transports have different character
  limits. iOS input can change the device clipboard, and the pasteboard fallback
  can show a paste-permission dialog.
  Unicode validation is not proof of reliable insertion across those paths.
* **Privacy:** the existing worker input path can log or record typed text.
  Do not use it for passwords, tokens, or other sensitive content. Adapter
  diagnostics omit text and control-connection credentials, but cannot change
  server-side recording behavior.

This adapter path has offline WebSocket and Maestro runtime coverage. Successful
live text insertion through it has not been verified; the staging checks for
other native commands do not establish text-input compatibility.

## Deployment-gated gestures

A swipe that ignores Maestro's requested duration is not an equivalent gesture.

The following commands require new worker contracts and authenticated backend
relay support. They have offline parser/runtime and installed-launcher coverage,
but **their successful execution is not verified or enabled on current staging**.

| Command  | Required health capability        | Contract                                                                                                                                   |
| -------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `swipe`  | `supports_explicit_drag_duration` | Native drag with explicit endpoints or Maestro's directional geometry; preserves a requested duration of 1–10000 ms, defaulting to 400 ms. |
| `scroll` | `supports_explicit_drag_duration` | One upward center-to-10%-height drag, lasting 400 ms on Android or 333 ms on iOS; no scroll-until-visible loop.                            |

Before any flow mutation, authenticated worker health must identify the same
connected workflow and platform and advertise every required capability as
boolean `true`. Missing, false, malformed, or unavailable capabilities reject the
whole flow, including taps or launches preceding the gated command. Current
staging checks confirmed that gated swipe flows were rejected with the preceding
tap counter and input value unchanged on both platforms.

Verified clear, app stop/kill/reset,
keychain, permissions, orientation, keyboard dismissal, media, recording, proxy,
airplane mode, AI, and WebView operations also remain unsupported.

## Evidence, retries, and cleanup

The adapter adds no HTTP retries, input replay, or Maestro recovery-tap retries. A timeout
can still mean an action executed; inspect the device before repeating it.
Existing worker-side retry behavior is unchanged, so this is not an
end-to-end exactly-once guarantee. Avoid destructive flows and concurrent
controllers.

The runner only attaches and detaches. It never allocates, installs, resets,
or stops the borrowed Revyl session. The job that owns the allocation must
release it separately, including after test failures:

```bash theme={null}
revyl device stop --session-id "$REVYL_SESSION_ID"
```

Do not submit this flow to Revyl's own YAML test runner: its schema and
execution model are different. For broader Maestro compatibility, retain your
current execution environment until a verified connection and command
contract is available.

## Next steps

* Prepare an app and session with the [device quickstart](/infrastructure/quickstart).
* Compare the [Appium driver](/infrastructure/appium).
* Check [artifact requirements](/builds/artifact-requirements) before uploading a build.
