Flows

Save a journey that worked once as a file your agent replays on every change. A mistake is refused with its line and column, and a failed run tells you how to fix the file.

Try it autonom flow check flows/login.yaml

Needs Autonom installed · a session on one target

How it works

A flow is a YAML file: a short header, one --- line, then a list of steps. It lives in your app repository, usually under .autonom/flows/, and you review it like code.

Each step names its control with a selector: what the control says, or its id, as on Read and drive the screen. Selectors are exact, so a step that matches two controls refuses to act.

flow check reads a file without a device. flow run replays it on the target your session owns. Checks on screen poll until their timeout. A tap, typed text or an opened link fires exactly once.

The exit code says what went wrong. 0 means the flow passed. 1 means a check on screen failed. 2 means the file or the machine is wrong, and the error names a stable code from Safety and errors.

A flow file is checked without a device, then run against one session. A pass exits 0. A test failure exits 1 with a repair brief, which leads to a reviewed edit and another run. A flow file is checked without a device, then run against one session. A pass exits 0. A test failure exits 1 with a repair brief, which leads to a reviewed edit and another run.
A failed run comes back with a repair brief, and the fix is always an edit you review.

A run keeps the screenshots, screen trees and logs its file asks for, as evidence. To get a flow without writing YAML, record one with Teach on App memory.

Examples

Every output here comes from a fake Android device that ships with Autonom's tests, so you can reproduce it without an emulator. Open Reproduce these outputs at the end of this section for the exact commands.

Catch a mistake before it reaches a device

Goal: find a broken flow in seconds, on any machine.

schema: autonom.dev/flow/v1 appId: com.example.app name: Login --- - launchApp - tapOn: selector: visibleText: Sign in with Email match: exactly

This flow:

  • Opens the app from a clean start (launchApp).
  • Taps the control labeled Sign in with Email (tapOn, visibleText).
  • Asks for match: exactly, which is not a match mode.
  1. Check the file.

    The check reads the file and every runFlow child, with no device.

    autonom flow check flows/login.yaml

You should see the check refuse the file and point at the typo.

{ "ok": false, "error_code": "flow_selector_invalid", "error": "/private/tmp/flows-demo/app/flows/login.yaml:9:14: unknown match mode 'exactly'", "hint": "Did you mean 'exact'? Match modes: caseInsensitiveExact, contains, exact, regex.", … "line": 9, "column": 14 }

What to look at. line 9 and column 14 point at the typo, and hint names the closest legal spelling. The command exits 2 with flow_selector_invalid, because the file is wrong, not the app.

Run the smoke suite and explain a failure

Goal: replay every smoke flow and tell a broken flow from a broken app.

  1. Start a session.

    Flows run on the one target your session owns.

    autonom session start
  2. Run the suite.

    A folder run is a suite on that target.

    autonom flow run .autonom/flows --include-tag smoke --exclude-tag flaky

    This command:

    • Runs every flow under .autonom/flows.
    • Keeps the flows tagged smoke (--include-tag).
    • Skips any flow tagged flaky (--exclude-tag).
  3. Build the report.

    It folds every run of the session into one page, failures first.

    autonom report suite

You should see one flow pass, one fail, and exit code 1.

{ "ok": true, "status": "failed", "flows": 2, "failed": 1, "runs": [ { "status": "failed", … "error_code": "flow_assertion_timeout", "failure_class": "test_failure", …

What to look at. "ok": true means the suite ran; status and exit code 1 say it failed. The failed run’s flow_assertion_timeout comes with failure_class: test_failure, which means the asserted state never held: either the app changed or the flow's selector is wrong. Here the repair block's top candidate shows the label in the description, so the selector is wrong.

Show full output
{ "ok": true, "status": "failed", "flows": 2, "failed": 1, "runs": [ { "status": "failed", "run_id": "fr_0e7e0c6ed7", "flow": "/private/tmp/flows-demo/app/.autonom/flows/sign-in.yaml", "name": "Sign in with email", "steps": […], "events": "/tmp/flows-demo/.autonom/sessions/s_1ce5b92616/flows/fr_0e7e0c6ed7/events.ndjson", "sensitive": false, "failure": { "step_index": 4, "command": "assertVisible", "line": 25, "flow": "/private/tmp/flows-demo/app/.autonom/flows/sign-in.yaml", "error_code": "flow_assertion_timeout", "failure_class": "test_failure", "error": "{'text': 'Sign in to access all features', 'match': 'exact'} was not visible within 1500 ms" }, "repair": { "step_index": 4, "command": "assertVisible", "line": 25, "flow": "/private/tmp/flows-demo/app/.autonom/flows/sign-in.yaml", "root_flow": "/private/tmp/flows-demo/app/.autonom/flows/sign-in.yaml", "selector": { "text": "Sign in to access all features", "match": "exact" }, "until_step": 3, "commands": [ "autonom flow run /private/tmp/flows-demo/app/.autonom/flows/sign-in.yaml --until-step 3", "autonom ui tree", "autonom ui find --text 'Sign in to access all features' --mode contains --all", "autonom screenshot --label 'repair assertVisible line 25'", "autonom flow check /private/tmp/flows-demo/app/.autonom/flows/sign-in.yaml", "autonom flow run /private/tmp/flows-demo/app/.autonom/flows/sign-in.yaml" ], "advice": "The element the step targets was not on screen within timeoutMs. Reconstruct the state with --until-step, dump the tree, find the label or identifier it carries now (candidates ranks what was on screen), and update the selector — prefer id, then visibleText, which matches the visible label wherever the platform stores it (text on native Android views, the accessibility label on Flutter and iOS). Raise timeoutMs only when the tree proves the element arrives late.", "note": "The corrected flow is a reviewed edit, never an automatic rewrite.", "candidates": [ { "selector": { "description": "Sign in to access all features" }, "command": "autonom ui find --desc 'Sign in to access all features' --mode exact --case-sensitive", "score": 1.0, "matched_field": "description", "node": { "ref": "n5", "role": "view", "desc": "Sign in to access all features", "bounds": [ 95, 1403, 985, 1453 ] } }, { "selector": { "description": "Sign in with Email" }, "command": "autonom ui find --desc 'Sign in with Email' --mode exact --case-sensitive", "score": 0.5, "matched_field": "description", "node": { "ref": "n8", "role": "view", "desc": "Sign in with Email", "bounds": [ 95, 1631, 985, 1757 ] } } ], "evidence": "/tmp/flows-demo/.autonom/sessions/s_1ce5b92616/flows/fr_0e7e0c6ed7/events.ndjson" } }, { "status": "passed", "run_id": "fr_f3ea5b88a3", "flow": "/private/tmp/flows-demo/app/.autonom/flows/welcome.yaml", "name": "Welcome screen", "steps": […], "events": "/tmp/flows-demo/.autonom/sessions/s_1ce5b92616/flows/fr_f3ea5b88a3/events.ndjson", "sensitive": false } ], "platform": "android", "target_id": "emulator-5554", "serial": "emulator-5554" }
The suite report: 2 flows, 1 passed, 1 failed. The failed flow, Sign in with email, lists four steps; step 4, assertVisible, failed with flow_assertion_timeout after 1500 ms.
The suite report lists the failure first and opens the failed flow to its steps.

Repair a failed flow

Goal: fix sign-in.yaml with the commands its repair block printed.

  1. Replay the prefix.

    Paste the first line of repair.commands: it runs steps 1 to 3 and stops where step 4 began.

    autonom flow run /private/tmp/flows-demo/app/.autonom/flows/sign-in.yaml --until-step 3
  2. Read the screen.

    Dump the tree the failed step saw.

    autonom ui tree
  3. Confirm the top candidate.

    Its own command finds exactly one node, by its description.

    autonom ui find --desc 'Sign in to access all features' --mode exact --case-sensitive
  4. Change one line.

    Swap text for visibleText, which matches the label wherever the platform keeps it.

    - text: Sign in to access all features + visibleText: Sign in to access all features
  5. Check the flows.

    Check the whole folder, so both files pass again.

    autonom flow check .autonom/flows
  6. Run the suite again.

    Replay every flow, not just the prefix.

    autonom flow run .autonom/flows --include-tag smoke --exclude-tag flaky

You should see both files check clean and both flows pass.

autonom flow check .autonom/flows { "ok": true, "checked": 2, … autonom flow run .autonom/flows --include-tag smoke --exclude-tag flaky { "ok": true, "status": "passed", "flows": 2, "failed": 0, …

What to look at. status is passed, and the run exits 0. The fix was one line you reviewed; Autonom never rewrites a flow for you.

Bring a Maestro flow across

Goal: reuse a Maestro flow you already have, as a checked Flow v1 file.

  1. Convert the file.

    Import writes Flow v1 and leaves the Maestro file as it was.

    autonom flow import maestro.yaml --out autonom.yaml
  2. Check what it wrote.

    The check follows every runFlow child, so import each child too; paths stay relative.

    autonom flow check autonom.yaml

You should see the import succeed and the check pass, for Android only.

autonom flow import maestro.yaml --out autonom.yaml { "ok": true, "imported": "maestro.yaml", "out": "autonom.yaml" } autonom flow check autonom.yaml { "ok": true, … "platforms": [ "android" …

What to look at. platforms lists only android, because the flow's launchApp clears the app's data, a step only Android runs. Maestro's text became visibleText, and Welcome.* became an anchored match: regex.

Show autonom.yaml
schema: autonom.dev/flow/v1 appId: com.example.app name: Login tags: - smoke env: USERNAME: user@example.com --- - launchApp: clearState: true - tapOn: selector: visibleText: Username match: exact - inputText: value: ${USERNAME} - tapOn: selector: id: login_button enabled: true match: exact - waitUntil: visible: visibleText: ^(?:Welcome.*)$ match: regex timeoutMs: 15000 - assertVisible: selector: visibleText: Welcome back match: exact - swipe: direction: up - takeScreenshot: label: after-login - runFlow: file: sub/cleanup.yaml
Reproduce these outputs

From the root of an Autonom d0f7211 checkout, in bash or zsh. Ids, times and paths differ on every run.

S=$PWD W=/tmp/flows-demo rm -rf $W && mkdir -p $W/app/.autonom/flows $W/app/flows $W/app/sub && cd $W/app export AUTONOM_HOME=$W/.autonom AUTONOM_FAKE_STATE=$W/fake-state.json echo "{\"ui_dump\": \"$S/tests/fixtures/woolbox/android_sign_in.xml\"}" > $AUTONOM_FAKE_STATE autonom() { python3 $S/scripts/autonom.py --platform android --serial emulator-5554 --adb $S/tests/fakes/fake_adb.py "$@"; } cat > .autonom/flows/welcome.yaml <<'Y' schema: autonom.dev/flow/v1 id: welcome-001 appId: com.example.app name: Welcome screen tags: - smoke --- - launchApp - assertVisible: selector: visibleText: Welcome to Yarn Shop match: exact - assertVisible: selector: visibleText: Sign in with Email match: exact - takeScreenshot: label: welcome Y cat > .autonom/flows/sign-in.yaml <<'Y' schema: autonom.dev/flow/v1 id: sign-in-email-001 appId: com.example.app name: Sign in with email tags: - smoke - auth evidence: mode: always collect: - screenshot - hierarchy - logs --- - launchApp - assertVisible: selector: visibleText: Welcome to Yarn Shop match: exact - tapOn: selector: visibleText: Sign in with Email match: exact label: Open email sign in - assertVisible: selector: text: Sign in to access all features match: exact timeoutMs: 1500 Y cat > flows/login.yaml <<'Y' schema: autonom.dev/flow/v1 appId: com.example.app name: Login --- - launchApp - tapOn: selector: visibleText: Sign in with Email match: exactly Y autonom flow check flows/login.yaml 2>&1 | python3 -m json.tool --indent 2 autonom session start autonom flow run .autonom/flows --include-tag smoke --exclude-tag flaky autonom report suite autonom flow run .autonom/flows/sign-in.yaml --until-step 3 autonom ui tree autonom ui find --desc 'Sign in to access all features' --mode exact --case-sensitive perl -pi -e 's/^ text: Sign in/ visibleText: Sign in/' .autonom/flows/sign-in.yaml autonom flow check .autonom/flows autonom flow run .autonom/flows --include-tag smoke --exclude-tag flaky cp $S/tests/fixtures/maestro/login.yaml maestro.yaml printf 'appId: com.example.app\n---\n- stopApp\n' > sub/cleanup.yaml autonom flow import maestro.yaml --out autonom.yaml autonom flow import sub/cleanup.yaml --out sub/cleanup.yaml autonom flow check autonom.yaml cat autonom.yaml

Good to know

  • Exit 1 means the app or the flow; exit 2 means the file or the machine. A test failure means the asserted state never held. Read its evidence and its repair block before you retry.
  • Taps fire once; checks poll. A tap is never retried on its own, and a selector that matches two controls refuses to act. tapOn with repeat is a declared number of taps, not a retry.
  • Prefer visibleText for flows that run on both platforms. Android views keep a label in text. Flutter and iOS keep it in the accessibility description. visibleText matches either.
  • Selectors match only what is on screen. A step never checks or taps a node iOS lists at a 0x0 frame. index counts on-screen matches only, so a selector unique on Android stays unique on iOS.
  • Never put credentials in a flow. Reference ${TEST_PASSWORD} and pass --secret TEST_PASSWORD. An --env value that reaches a sensitive: true slot is treated as a secret too.
  • Wait for a still screen before you tap a sheet that slides in. waitForSettled waits until the tree holds still and never fails the flow. Check the state you need with waitUntil.
  • launchApp starts fresh. It clears the task on Android and stops the app first on iOS. Add resume: true to continue a journey.
  • The repair brief suggests; it never rewrites. Each candidate comes with the ui find command that confirms it. The edit is yours to review and commit.

Reference

Every command prints JSON. Check, format, list and import work without a device; run needs a session.

CommandWhat it does
autonom flow check <path>Checks one file or a folder, runFlow children included. A mistake comes back with its line, column and error code.
autonom flow fmt <file-or-dir>Prints the canonical form, with shorthand such as tapOn: Sign in expanded. --write saves it.
autonom flow list [path]Lists each flow's file, id, name, tags and the platforms it can run on.
autonom flow run <file-or-dir>Runs a file, or a folder as a suite, in the active session. --include-tag and --exclude-tag filter a suite.
--secret NAMEPasses a secret by name. Its value never enters the run's files.
--until-step NReplays the flow through step N and leaves the target in that state.
autonom flow create --from-session <ID>Compiles a session you walked into a checked flow, reusing selectors proven unique.
autonom flow import <path> [--out PATH]Converts a Maestro flow into Flow v1.
autonom flow export <file> --format maestroWrites a Maestro flow, and refuses anything Maestro cannot express the same way.
autonom report buildRenders one run as a self-contained report.html and a JUnit report.xml.
autonom report suiteFolds every run of the session into suite.html and suite.xml, and exits 1 when a flow failed.
autonom proof --base <REF>Picks the flows that cover a diff, runs them and writes a one-screen verdict.

Android and iOS differences

The language is the same on both platforms. A few steps, and what the device reports, differ.

CapabilityAndroidiOS
back, clearState, setOrientation, a KEYCODE_* keyRunRefused before any step runs; a suite skips the flow and lists it under skipped_flows
The focus check before inputTextWaits for the field with keyboard focusWaits for a text field on screen, because the tree has no focus flag
launchAppOpens the launcher activity on a cleared taskStops the app, then launches it
A recorded session clearCompiles into the flowSkipped with a warning; declare a reset in setup instead

Next steps

You can write, check, run and repair a flow.