Run Maestro flows
agent-device runs a subset of Maestro YAML flows on iOS and Android, and exports .ad scripts to Maestro YAML. This is a compatibility layer, not full Maestro support:
- Only the commands and fields in Supported subset run. Anything else fails with source context instead of being skipped.
- Flows run on iOS and Android only.
runScript,evalScript, and JavaScript conditions execute flow code on the machine running the agent-device daemon, without a security sandbox. Run only flows you trust to run there.
For recording and replaying native .ad scripts, see Replay & E2E testing.
Run a Maestro flow
Pass --maestro to replay or test:
Keep the target binding, such as --platform ios, on the replay or test command. agent-device help maestro prints the same subset and boundaries as this page.
Supported subset
- Flows:
launchApp(withclearState,permissions, and Apple-only launch arguments;permissionsapply after state clearing but before launch, and alaunchAppwithoutpermissionstouches nothing — there is no silentall: allowdefault);setPermissions(mid-flow permission grants, denials, and resets;allresolves in the backend — one simctl call on iOS, the declared permissions on Android — with specific entries overriding after it);runFlowfile/inline;runFlow.whenandrepeat.whileconditions (platform,visible,notVisible, andtrue, all re-evaluated before everyrepeatiteration);onFlowStart/onFlowComplete;repeatwithtimes,while, or both; and retry. - Interactions:
tapOn,doubleTapOn,longPressOn,inputTexton the focused element,eraseText,openLink,hideKeyboard, basicpressKey, andback; selector targets poll until available and support recursiveindex,childOf,above,below,leftOf,rightOf,containsChild,containsDescendants, points, andoptional; outer command labels are metadata, not target selectors. - Assertions and navigation:
assertVisible,assertNotVisible,assertTrue(literal values and${VAR}lookups only;"","false","0","null", and"undefined"are falsy, everything else is truthy),extendedWaitUntil,scroll,scrollUntilVisible, absolute/percentage/targetswipe,takeScreenshot,waitForAnimationToEnd,clearState,stopApp, andkillApp(system-initiated process death:am killon ADB-backed Android devices, which refuse a foreground target withandroid-kill-requires-background-app— background the app first, for examplepressKey: Home— and fail a target whose process survives withandroid-kill-process-survived; iOS, Limrun, and WebDriver-provider backends run it asstopApp). - Scripts: ordered
runScriptfile/env scripts withhttp.post,json, andoutputvariables;evalScriptinline expressions run flow-scoped JavaScript and writeoutput.*leaves for later steps.
Boundaries
- Permissions: every entry is one
settings permissioncall, applied in order withallfirst; the step stops at the first entry the selected platform refuses, earlier entries stay applied, and the error names what landed. Android’s only allow level is while-in-use, solocation: inuseandlocation: nevermeanallowanddenythere, whilelocation: alwaysandphotos: limitedare Apple-only and fail. On iOS, which service a runtime changes issimctl privacy’s own verdict: current runtimes refuse a targetednotificationschange and leave notifications untouched underall. - Runtime: iOS and Android only;
launchApp.clearStateand standaloneclearStatesupport Android and iOS simulators, launch arguments are Apple-only, and other standalone device utility/state commands are unsupported. - Expressions:
evalScriptand conditiontrue:fields (runFlow.whenandrepeat.whileshare one evaluator) are evaluated as JavaScript (flowenvand prioroutputleaves are string-typed); atrue:field that is a boolean, amaestro.platformcomparison, or plain literal text after${VAR}lookups is decided without JavaScript, with theassertTruefalsy table for literal text. Other fields stay literal or${VAR}lookup-only —assertTruesupports literals and bare lookups, and other expression-shaped payloads fail loud. - Environment: flow
envis the default,AD_VAR_*overrides it, and CLI-e KEY=VALUEwins over both. - Failure diagnostics: resolved targets and
runFlowpaths are rendered, whileinputTextpayloads remain hidden; do not place secrets in diagnostic identifiers. - Trust:
runScript,evalScript, and JavaScript conditiontrue:fields execute flow scripts in-process vianode:vm, which is not a security sandbox;runScriptmay makehttp.postnetwork requests and its output keys cannot contain a dot.evalScriptandtrue:fields that need JavaScript are refused outright for a flow accepted over the daemon’s remote HTTP surface, since that context can escape to the host. - Errors and tracking: unsupported commands and fields fail with source context when available; open a focused issue only when implementation work is planned.
- Session takeover:
--keep-sessionis a native.adreplay option and is rejected for Maestro YAML.
ADR 0015 lists the deliberate deviations from Maestro. If a missing feature matters for your suite, open a focused issue with a small flow snippet.
Resume a failed Maestro flow
A failed Maestro step returns the same replay divergence report as an .ad script, including the resume information, and replay --from <n> --plan-digest <sha256> resumes it the same way. See Resume a failed replay for the workflow.
For Maestro flows, --from counts steps in the top-level plan. A runFlow with no condition, or with
a condition that resolves before the run, is flattened into its commands (or dropped when the condition
is false). A control step decided at run time (runFlow, repeat, or retry) counts as one step, and
you cannot resume at a command nested inside it. As with .ad replay, restore any state and
environment values the skipped steps would have set before you resume.
Export .ad scripts to Maestro YAML
To run a recorded flow with Maestro, export the .ad script to Maestro YAML:
replay export only converts the file: it does not start the daemon or contact a device. Without --out, it prints the YAML to stdout.
Each open <appId> exports with an explicit launchApp.appId, so a flow can switch between apps and return to the original app. The first app remains the flow's default appId; relaunch options and app-specific deep links stay attached to their authored targets.
Deep links, including schemes without // such as tel: and mailto:, export as openLink. A standalone open tel:+15551234567 emits only the link command; open com.example.app mailto:agent@example.test emits the app launch followed by the link.
Export is strict. It writes Maestro YAML for compatible flow actions such as app launch, taps, long press, text input, keyboard dismiss/enter, back, home, text visibility assertions, coordinate swipes, basic scroll, screenshots, and .ad env directives. home exports as pressKey: Home, so flows that visit the home screen and reopen the app can be exported. Agent-only inspection or maintenance actions such as snapshot, get, record, trace, settings, and unsupported selector shapes fail with the source line and action instead of being silently dropped. Known semantic differences are reported as warnings; for example, .ad fill exports as tapOn plus inputText, which may append text in Maestro rather than replacing existing field contents. Native .ad label= selectors export as Maestro text: selectors and warn because Maestro text matching is broader than label-only matching. A strict wait absent is reported as unsupported rather than mapped to Maestro's more lenient notVisible condition.
Troubleshooting
- A flow fails on unsupported syntax:
- Check ADR 0015. If the missing feature matters to your suite, open a focused issue with a small flow snippet.
