DSL Reference

Run many steps on a device in one call: find elements by text or type, tap, type, wait, check, retry. Every action is shown as a JSON step and as a line of a .mob script.

Overview

A DSL script is a list of steps that MobAI runs on one device in a single request. Send it as JSON to POST /api/v1/devices/{id}/dsl/execute on the local API at http://127.0.0.1:8686. The execute_dsl tool of MobAI's MCP server takes the same JSON. The same steps can also be written as .mob files, one step per line, which is how saved tests are stored. Every action on this page shows the JSON step and, where one exists, the .mob line.

JSON

{
  "version": "0.2",
  "steps": [
    {"action": "open_app", "bundle_id": "com.apple.Preferences"},
    {"action": "tap", "predicate": {"text_contains": "General"}},
    {"action": "wait_for", "predicate": {"text": "About"}, "timeout_ms": 5000},
    {"action": "observe", "include": ["ui_tree"]}
  ]
}

.mob

app "com.apple.Preferences"
tap ~"General"
wait_for "About" timeout:5000
observe

How a run works

Steps run one at a time, in order. A step that fails stops the run unless its on_fail says otherwise. The response lists every step with its result, so one request tells you what happened. If the bridge is not running, MobAI starts it before the first step. One request may run for up to 5 minutes.

Script format

A script is a JSON object with a steps array. Everything else is optional.

Script keys

FieldTypeDefaultDescription
versionstring"0.2"Send "0.2". The value is not checked.
stepsarrayrequiredThe steps to run, in order.
on_failobjectabortFailure strategy for every step that has none of its own. See Failure strategies.
paramsobjectnoneVariables and their default values, used as ${name}. See Variables.
alertsstringoffaccept or dismiss. Clears system alerts (permission prompts, crash dialogs) that get in a step's way, then runs the step again once. Any other value rejects the script.
settleboolfalseTaps wait up to 500 ms for a moving target to come to rest before tapping. Useful when replaying a saved flow without looking at the screen first.
modestringexploreexplore: only the last observe in the script runs (earlier ones are skipped), and a failed step comes back with the UI tree in debug. deterministic: every observe runs and failures carry no debug.
defaultsobjectnone{"timeout_ms": N} sets the time limit for every step that has no timeout_ms, and the wait time of wait_for.

Step keys

Every step takes these keys, next to the keys of its action.

FieldTypeDefaultDescription
actionstringrequiredThe action name. See All actions.
idstringnoneYour own label for the step. It comes back as step_id in the result.
contextstring or array"native""native" for app screens, "web" for the selected web page. observe also takes ["native", "web"].
timeout_msint120000Time limit for the step. A delay gets its own length plus 10 seconds. For waits and assertions it is also how long to wait, see each action.
on_failobjectscript on_fail, else abortWhat to do when this step fails.

Coordinates

Coordinates are {"x": 120, "y": 640} in screen units, the same units as the bounds in the UI tree. Use {"x_percent": 50, "y_percent": 30} (0 to 100) for points that work on any screen size. One axis can be a percent and the other absolute. A plain pair [120, 640] is read as x and y.

Forgiving input

Action names ignore case, underscores, hyphens and spaces, so pressKey and Press-Key both mean press_key. Keys of a step, a predicate, near and coordinates can be camelCase (bundleId, clearFirst, textContains). Keys inside other objects, such as on_fail, to_element and filter, must be snake_case. coordinate, coordinates, point and position are read as coords, selector as predicate, and ms and milliseconds as timeout_ms. observe also takes "ui_tree": true style flags in place of include.

A step key MobAI does not know is ignored, so check the spelling when a setting seems to have no effect. A predicate key it does not know fails the step with VALIDATION_ERROR, because a predicate that matches nothing specific would match every element.

Action aliases

These names run the action on the left. wait is not an alias, because it could mean delay or wait_for.

ActionAlso accepted as
open_applaunch_app, start_app
kill_appterminate_app, close_app, stop_app, force_stop
open_linkopen_url, openurl, deeplink, open_deeplink, open_deep_link
taptap_on, click, touch
double_tapdouble_tap_on
long_presslong_press_on, long_tap
perform_actionsw3c_actions, perform_gesture
typeinput_text, type_text, enter_text, input, set_text
type_secrettype_password, input_secret, type_credential
clearclear_text, clear_field, erase_text, erase
hide_keyboarddismiss_keyboard
copy_textcopy_text_from
paste_textpaste
press_keykey_press
select_pickerpick, set_picker, picker_select, select_wheel
screenshottake_screenshot, capture_screenshot
wait_forextended_wait_until, wait_for_element, wait_until_visible
delaysleep, pause
assert_existsassert, assert_visible, assert_present
assert_not_existsassert_not_visible, assert_absent, assert_hidden
biometricsbiometric, face_id, touch_id, fingerprint
foldset_fold, fold_device

.mob files

A .mob file holds one step per line. Text goes in double quotes, with \" for a quote inside. Settings after the target are key:value pairs, such as timeout:5000 or on_fail:skip. Lines that start with # are comments, except for these:

LineMeaning
# Param: email = [email protected]Declares a variable with a default, like params. Leave out = value for a variable with no default.
# Alerts: acceptSame as "alerts". Takes accept or dismiss.
# ios … # endLines between run on iOS only. # android works the same way. Blocks do not nest.

run "./login.mob" email="[email protected]" exists only in .mob: it pastes another file's steps in place, with values for its # Param: lines. script "./seed.js" is the .mob form of run_script. Test runs of .mob files use deterministic mode with settle on, and on iOS a step that finds no element in the UI tree also looks at the text on screen.

All actions

Every action MobAI accepts in a script. Native actions work on app screens through the UI tree. The second column is the .mob keyword.

Action.mobWhat it does
open_appappLaunch an app
kill_appkill_appEnd an app
open_linkopen_linkOpen a URL or deep link
taptapTap an element or a point
double_tapdouble_tapDouble tap
long_presslong_pressPress and hold
two_finger_taptwo_finger_tapTap with two fingers
pinchpinchPinch to zoom in or out
swipeswipeSwipe by direction or between points
scrollscrollScroll, or scroll until an element shows
dragdragDrag from one element or point to another
drag_pathdrag_pathDrag along several points
perform_actionsJSON onlyRaw pointer and key input
typetypeType text
type_secrettype_secretType a saved credential
clearclearClear a text field
hide_keyboardhide_keyboardClose the keyboard
copy_textcopy_textCopy an element's text to the clipboard
paste_textpaste_textType the clipboard's text
press_keypress_keyPress a key or hardware button
navigatenavigateGo home, back or to recent apps
toggletoggleTurn a switch on or off
select_pickerselect_pickerTurn a picker wheel to a value
observeobserveRead the UI tree, a screenshot, OCR text and more
auditJSON onlyCheck the screen for layout and accessibility issues
screenshotscreenshotSave a screenshot to a file
record_startrecord_startStart recording the screen
record_stoprecord_stopStop recording and save the frames
wait_forwait_for, wait_stableWait for an element, or for the screen to settle
delaydelayWait a fixed time
checkpointcheckpointMark a point in the run
set_locationset_locationSet a simulated GPS location
reset_locationreset_locationGo back to the real location
set_permissionsset_permissionsGrant or revoke app permissions
biometricsbiometricsSimulate Face ID, Touch ID or a fingerprint
foldfoldFold or unfold a foldable simulator
sirisiriSend a command to Siri
select_web_contextJSON onlyPick the web page to work with
execute_jsJSON onlyRun JavaScript in the web page
assert_existsassert_existsFail unless an element is on screen
assert_not_existsassert_not_existsFail if an element is on screen
assert_countassert_countFail unless N elements match
assert_propertyJSON onlyCheck a property of an element
assert_screen_changedJSON onlyFail unless the screen changed enough
metrics_startJSON onlyStart collecting performance metrics
metrics_stopJSON onlyStop and get the metrics summary
if_existsif_existsRun steps when an element is on screen
repeatrepeatRun steps in a loop
extractextractSave text, a screenshot or a value as a variable
eval_scriptevalRun a line of JavaScript on this computer
run_scriptscriptRun a JavaScript file on this computer

request_user_input is also an action name, but it always fails in a script run.

Apps and links

open_app

Launches an app by its bundle ID (iOS) or package name (Android). With "alerts" set, a system alert that shows right after launch is cleared.

Also accepted as: launch_app, start_app

JSON

{"action": "open_app", "bundle_id": "com.example.app"}
{"action": "open_app", "bundle_id": "com.example.app", "fresh": true, "arguments": ["-UITesting"], "environment": {"API_URL": "http://localhost:3000"}}

.mob

app "com.example.app"
app "com.example.app" fresh arg:"-UITesting" env:"API_URL=http://localhost:3000"
FieldTypeDefaultDescription
bundle_idstringrequiredBundle ID or package name.
freshboolfalseEnd the app first if it is running, so it starts again from launch. Use it with arguments or environment so they reach a new process. .mob: fresh.
argumentsstring[]noneLaunch arguments for this launch only. iOS: process arguments, such as "-AppleLanguages", "(en)". Android: am start extras, such as "--ez", "UITesting", "true". .mob: arg:"...", once per argument.
environmentobjectnoneEnvironment variables for this launch only. iOS only: on Android the step fails. .mob: env:"KEY=value".
debugboolfalseLaunch with a debugger attached and write the app's output to a log file. Only for debug builds that need it. The result has log_path.
log_pathstringtemp folderFolder for the debug log. ~/ works.

The result echoes what reached the app under launch. Launching an app that is not installed fails with a message that says so.

kill_app

Ends a running app.

Also accepted as: terminate_app, close_app, stop_app, force_stop

JSON

{"action": "kill_app", "bundle_id": "com.example.app"}

.mob

kill_app "com.example.app"
FieldTypeDefaultDescription
bundle_idstringrequiredBundle ID or package name.

In .mob, kill_app never fails the run: it is skipped when it fails.

Opens a URL or a deep link. The system hands a custom scheme such as myapp:// to the app that registered it, so you can jump straight to a screen.

Also accepted as: open_url, openurl, deeplink, open_deeplink, open_deep_link

JSON

{"action": "open_link", "url": "myapp://profile/42"}
{"action": "open_link", "url": "https://example.com"}

.mob

open_link "myapp://profile/42"
FieldTypeDefaultDescription
urlstringrequiredFull URL or deep link.

Touch and gestures

Touch actions take a predicate (see Predicates) or coords. A predicate taps the center of the element it finds. After a gesture, MobAI checks the screen, and when nothing changed the step still passes but carries a warning that the target may be covered or outside its hit area.

tap

Taps an element or a point. When several elements match and exactly one of them is a control (a button among labels with the same text), that one is tapped. On Android, an element covered by a sticky bar is scrolled into the open first.

With the script's settle on, the tap waits for a moving target to stop. In the web context, tap clicks the element that css_selector finds.

Also accepted as: tap_on, click, touch

JSON

{"action": "tap", "predicate": {"text_contains": "Settings"}}
{"action": "tap", "coords": {"x": 120, "y": 640}}
{"action": "tap", "coords": {"x_percent": 50, "y_percent": 30}}

.mob

tap ~"Settings"
tap 120,640
tap 50%,30%
FieldTypeDefaultDescription
predicatePredicateone ofThe element to tap.
coordsCoordinatesone ofThe point to tap.

double_tap

Taps twice in quick succession.

Also accepted as: double_tap_on

JSON

{"action": "double_tap", "predicate": {"text": "Photo"}}

.mob

double_tap "Photo"
FieldTypeDefaultDescription
predicatePredicateone ofThe element.
coordsCoordinatesone ofThe point.

long_press

Presses and holds. Picks the control among same-text matches the way tap does.

Also accepted as: long_press_on, long_tap

JSON

{"action": "long_press", "predicate": {"text": "Message"}, "duration_ms": 1500}

.mob

long_press "Message" duration:1500
FieldTypeDefaultDescription
predicatePredicateone ofThe element.
coordsCoordinatesone ofThe point.
duration_msint500How long to hold. .mob: duration:

two_finger_tap

Taps with two fingers at once, for maps and other views that zoom out on it.

JSON

{"action": "two_finger_tap", "predicate": {"text_contains": "Map"}}

.mob

two_finger_tap ~"Map"
FieldTypeDefaultDescription
predicatePredicateone ofThe element.
coordsCoordinatesone ofThe point.

pinch

Pinches around the center of an element or a point.

JSON

{"action": "pinch", "predicate": {"text_contains": "Map"}, "scale": 0.5}

.mob

pinch ~"Map" scale:0.5
FieldTypeDefaultDescription
predicatePredicateone ofThe element.
coordsCoordinatesone ofThe point.
scalenumber2.0Above 1 zooms in, below 1 zooms out. In .mob the default is 0.5.
velocitynumber1.0Change of scale per second. JSON only.

swipe

Swipes like a finger. direction is the way the finger moves, so "up" moves content up. To bring content into view, scroll is usually simpler.

JSON

{"action": "swipe", "direction": "up", "distance": "medium"}
{"action": "swipe", "direction": "left", "from": {"predicate": {"text": "Card 1"}}}
{"action": "swipe", "from_coords": {"x": 200, "y": 700}, "to_coords": {"x": 200, "y": 200}, "duration_ms": 400}

.mob

swipe up distance:medium
swipe from:200,700 to:200,200 duration:400
FieldTypeDefaultDescription
directionstringone ofup, down, left or right. Starts at the screen center, or at from.
distancestringmediumshort (25% of the screen), medium (50%) or full (75%). small, page and large work too, and amount is read the same way. Any other word fails the step.
fromTargetElementscreen center{"predicate": {...}}: start the swipe at this element. JSON only.
from_coordsCoordinatesone ofStart point. Use with to_coords in place of direction.
to_coordsCoordinatesnoneEnd point.
duration_msint300Length of the swipe.

A swipe by direction stops at least 50 screen units from the edge.

scroll

Scrolls to see more content. direction is where you want to look, so "down" shows what is below. With to_element, it scrolls until that element shows.

The search stops with NO_MATCH and the message reached end of scrollable content when the screen stops moving, or element not found after scrolling when max_scrolls runs out while the list still moves.

JSON

{"action": "scroll", "direction": "down", "to_element": {"predicate": {"text": "Privacy"}}, "max_scrolls": 10}
{"action": "scroll", "direction": "down", "to_element": {"predicate": {"text": "Delete account"}, "fully_visible": true}}
{"action": "scroll", "direction": "right", "predicate": {"type": "scrollview"}, "amount": "small"}

.mob

scroll down to "Privacy" max_scrolls:10
scroll right in type:scrollview amount:small
FieldTypeDefaultDescription
directionstringdowndown, up, left or right.
to_elementTargetElementnone{"predicate": {...}}: keep scrolling until this element is on screen. .mob: to "Text".
to_element.fully_visibleboolfalseKeep going until the whole element shows, not just part of it. Use it before tapping something a bar may cover. If the rest cannot be uncovered, the step passes with what shows. JSON only.
predicatePredicatescreen centerThe scroll view to scroll, when the screen has more than one. .mob: in type:scrollview.
max_scrollsint10How many rounds to try before giving up.
amountstringpageSize of a single scroll without to_element: small (20% of the screen), page (50%) or full (80%). short, medium, large and the key distance work too. Not a count: use max_scrolls to scroll more.

drag

Drags from one element or point to another. Each end can be an element or coordinates, and the two can be mixed in JSON. When a predicate matches several elements, the first one is used.

JSON

{"action": "drag", "from": {"predicate": {"text": "Item 1"}}, "to_element": {"predicate": {"text": "Trash"}}}
{"action": "drag", "from_coords": {"x": 100, "y": 200}, "to_coords": {"x": 300, "y": 400}, "duration_ms": 800, "press_duration_ms": 500, "hold_duration_ms": 200}

.mob

drag "Item 1" to "Trash"
drag 100,200 to 300,400 press_duration:500 hold_duration:200 duration:800
FieldTypeDefaultDescription
fromTargetElementone ofStart element, {"predicate": {...}}.
from_coordsCoordinatesone ofStart point.
to_elementTargetElementone ofEnd element.
to_coordsCoordinatesone ofEnd point.
duration_msint500Time to move from start to end.
press_duration_msint0Hold before moving, to pick up an icon or a list row.
hold_duration_msint0Hold at the end before letting go, for drop targets that need a moment.

drag_path

Drags one finger along a path of points, for unlock patterns, drawing or curved moves.

JSON

{"action": "drag_path", "points": [{"x": 100, "y": 400}, {"x": 150, "y": 300, "duration_ms": 150}, {"x": 300, "y": 500, "duration_ms": 300}]}

.mob

drag_path 100,400 150,300:150 300,500:300
FieldTypeDefaultDescription
pointsarrayrequiredAt least 2 points of {"x", "y", "duration_ms"}. duration_ms is the time to move to the point from the one before, and every point after the first needs one. On the first point it is an optional press before moving. .mob: x,y:ms.

perform_actions

Sends raw pointer and key input in the W3C Actions format, for gestures no other action covers: several fingers, exact timing, or pointer and keys together. Two pointer sources with different ids move as two fingers at once. In pointerMove items, x_percent and y_percent (0 to 100) can replace x and y.

Also accepted as: w3c_actions, perform_gesture

JSON

{"action": "perform_actions", "actions": [{"type": "pointer", "id": "finger1", "parameters": {"pointerType": "touch"}, "actions": [{"type": "pointerMove", "duration": 0, "x_percent": 50, "y_percent": 50}, {"type": "pointerDown", "button": 0}, {"type": "pause", "duration": 300}, {"type": "pointerMove", "duration": 500, "x_percent": 50, "y_percent": 20}, {"type": "pointerUp", "button": 0}]}]}
FieldTypeDefaultDescription
actionsarrayrequiredInput sources: {"type": "pointer" | "key" | "none", "id", "parameters", "actions": [...]}.

Fails with a clear message on a device that cannot take raw input.

Text and keys

type

Types text. With a predicate, MobAI taps the field first unless it already has focus. Without one, the keyboard must be open or a field must have focus, otherwise the step fails.

In the web context, predicate needs a css_selector.

Also accepted as: input_text, type_text, enter_text, input, set_text

JSON

{"action": "type", "predicate": {"text_contains": "Email"}, "text": "[email protected]", "clear_first": true}
{"action": "type", "text": "hello"}

.mob

type ~"Email" → "[email protected]" clear
type "hello"
FieldTypeDefaultDescription
textstringrequiredThe text to type.
predicatePredicatefocused fieldThe field to type into.
clear_firstboolfalseClear the field before typing. .mob: clear.
dismiss_keyboardboolfalseClose the keyboard after typing. .mob keep_keyboard sets it to false, which is already the default.

In .mob, the field and the text are joined by → or ->.

type_secret

Types a credential saved in MobAI, by its id. The value is typed on the device and never appears in the script, the logs or the result. Targets the field the same way as type.

Also accepted as: type_password, input_secret, type_credential

JSON

{"action": "type_secret", "predicate": {"text_contains": "Password"}, "secret_id": "qa-password"}

.mob

type_secret ~"Password" → "qa-password"
FieldTypeDefaultDescription
secret_idstringrequiredId of the saved credential.
predicatePredicatefocused fieldThe field to type into.
clear_firstboolfalseClear the field first.
dismiss_keyboardboolfalseClose the keyboard after typing.

clear

Clears a text field without typing anything. With a predicate it focuses that field first, without one it clears the field that has focus.

Also accepted as: clear_text, clear_field, erase_text, erase

JSON

{"action": "clear", "predicate": {"text_contains": "Search"}}
{"action": "clear"}

.mob

clear ~"Search"
clear
FieldTypeDefaultDescription
predicatePredicatefocused fieldThe field to clear.

hide_keyboard

Closes the on-screen keyboard.

Also accepted as: dismiss_keyboard

JSON

{"action": "hide_keyboard"}

.mob

hide_keyboard

copy_text

Copies an element's text to the device clipboard.

Also accepted as: copy_text_from

JSON

{"action": "copy_text", "predicate": {"text_contains": "Code"}, "store_as": "code"}

.mob

copy_text ~"Code"
FieldTypeDefaultDescription
predicatePredicaterequiredThe element to copy from.
store_asstringnoneAlso keep the text as a variable with this name. JSON only.

paste_text

Types the clipboard's text into the field that has focus. It takes no target: focus the field first.

Also accepted as: paste

JSON

{"action": "paste_text"}

.mob

paste_text

press_key

Presses a keyboard key or a hardware button.

Also accepted as: key_press

JSON

{"action": "press_key", "key": "enter"}
{"action": "press_key", "key": "back"}

.mob

press_key enter
FieldTypeDefaultDescription
keystringrequiredKey name, see the list below.

Android: back, home, recent_apps, enter, tab, delete, escape, volume_up, volume_down, mute, power, play_pause, next, previous

iOS: enter, tab, delete, home, volume_up, volume_down. There is no back or recent apps key on iOS: tap the app's back button or swipe.

Web context: enter, tab, delete, escape. Enter also submits the form of the field that has focus.

navigate

Goes to the home screen, back, or to recent apps. A target that starts with http:// or https:// opens that URL, in the selected web page when there is one, else on the device.

Platforms: back and recent_apps are Android only. home works on both.

JSON

{"action": "navigate", "target": "home"}

.mob

navigate home
navigate back
FieldTypeDefaultDescription
targetstringrequiredhome, back, recent_apps or an http(s) URL.

toggle

Turns a switch, toggle button or checkbox to a state. If it is already there, nothing is tapped. Fails when the element found is not a switch: find the switch by its label with near.

JSON

{"action": "toggle", "predicate": {"type": "switch", "near": {"text_contains": "Wi-Fi"}}, "state": "on"}

.mob

toggle type:switch near ~"Wi-Fi" on
FieldTypeDefaultDescription
predicatePredicaterequiredThe switch.
statestringflipon or off. Leave it out to flip the switch whatever its state. Required in .mob.

The result has toggled: true when MobAI tapped, false when the switch was already in that state.

select_picker

Turns a picker wheel to the row with the given text: an iOS picker wheel (date, time and custom pickers) or an Android NumberPicker (time and date pickers in spinner mode). One step replaces a loop of swipes and checks.

Target the wheel itself, with index when a picker has several wheels, not the picker around it. On iOS, "7" matches a row that reads 7 or starts with the word 7, such as 7 o'clock. On Android, "7" also matches 07. Fails when there is no wheel at the target or no row matches. Not for drop-down menus: tap those and tap the option.

Also accepted as: pick, set_picker, picker_select, select_wheel

JSON

{"action": "select_picker", "predicate": {"type": "PickerWheel", "index": 1}, "value": "March"}
{"action": "select_picker", "predicate": {"type": "NumberPicker", "index": 0}, "value": "7"}

.mob

select_picker type:PickerWheel [1] "March"
select_picker 200,700 "7"
FieldTypeDefaultDescription
predicatePredicateone ofThe wheel.
coordsCoordinatesone ofA point inside the wheel.
valuestringrequiredThe text of the row to pick.

Screen, waits and recording

observe

Reads the screen. In the default explore mode only the last observe of a script runs, so put one at the end to see the result of the steps before it.

A ui_tree is a text list of elements, one per line, such as [4] Button "Settings" #settings (16,120 358x44): index, type, text, id, state and bounds. On iOS, a tree too large to return comes back cut down to its controls, with the text on screen added from OCR.

JSON

{"action": "observe", "include": ["ui_tree"]}
{"action": "observe", "include": ["ui_tree"], "filter": {"text_regex": "Settings|Wi-Fi"}}
{"action": "observe", "include": ["ui_tree"], "compact": true}
{"action": "observe", "include": ["screenshot"]}

.mob

observe
FieldTypeDefaultDescription
includestring[]["ui_tree", "screenshot"]What to return. Native: ui_tree, screenshot, ocr (iOS), activity (the app or activity in front), installed_apps. Web: ui_tree (the page's DOM) and page_info.
contextstring or array"native""web" reads the selected web page. ["native", "web"] reads both.
only_visiblebooltrueSet false to also list elements scrolled out of view, for reading whole lists. Elements out of view cannot be tapped.
include_keyboardboolfalseAlso list the keyboard's keys.
compactboolfalseA shorter tree without bounds or indentation, about half the size. Keep the full tree when you need coordinates.
filter.text_regexstringnoneKeep only elements whose text matches this regular expression. Also filters OCR text.
filter.boundsobjectnone{"x", "y", "width", "height"}: keep only elements inside this area.
store_asstringnoneAlso keep the observation as a variable.

In .mob, observe reads the UI tree, a screenshot, the installed apps and the activity.

On iOS, a screenshot without ui_tree also returns ocr_elements and icons: text and unlabeled icons found on the image, each with its center point to tap. Icons carry a short list of likely names, closest first, with a distance where lower means a closer match.

Result

{
  "observations": {
    "native": {
      "ui_tree": "[3] StaticText \"General\" (16,98 120x22)\n[4] Button \"Settings\" #settings (16,120 358x44)\n",
      "screenshot": "/tmp/mobai/screenshots/observe_1767000000000.png",
      "screenshot_saved": true
    }
  }
}

The screenshot is saved on this computer and the result holds its path. screenshot_scale appears when the image is smaller than the screen: multiply a point read off the image by it before tapping there. Other keys, when asked for: installed_apps (bundleId, name, version, siri), ocr_elements (text, confidence, centerX, centerY, width, height), icons and page_info (id, title, url).

audit

Checks the current screen for touch targets that are too small, clipped text, alignment, spacing, safe area and accessibility labels. Returns the findings and a screenshot with the flagged elements boxed. Findings are measurements, not verdicts: look at the screenshot to decide which ones are real problems.

JSON

{"action": "audit"}
FieldTypeDefaultDescription
store_asstringnoneAlso keep the result as a variable.

The findings are in observations.native.audit.findings, each with analyzer, type, elements, measured, expected, confidence and note.

screenshot

Saves a full-quality PNG of the screen on this computer.

Also accepted as: take_screenshot, capture_screenshot

JSON

{"action": "screenshot", "file_path": "~/mobai-shots", "name": "login_screen"}

.mob

screenshot "~/mobai-shots"
FieldTypeDefaultDescription
file_pathstringtemp folderFolder to save into. Defaults to mobai/screenshots in the system temp folder. Use an absolute path or ~/.
namestringscreenshot_<time>File name. .png is added. JSON only.

The result has screenshot_path, the full path of the file.

record_start

Starts recording the screen as a series of frames, for checking animations and transitions that a single screenshot misses. Only one recording runs at a time. A recording still running when the script ends is stopped and saved, and appears as a record_stop entry in the results.

JSON

{"action": "record_start"}

.mob

record_start
FieldTypeDefaultDescription
file_pathstringtemp folderFolder for the frames. JSON only.

record_stop

Stops the recording and saves the frames. Frames that did not change are left out.

JSON

{"action": "record_stop", "file_path": "~/mobai-recordings/login"}

.mob

record_stop
FieldTypeDefaultDescription
file_pathstringstart folderSave into this folder in place of the one from record_start. JSON only.

The result has recording_path, frame_count, contact_sheets (images that tile the frames in order, each tile labeled with its frame number) and transition_hints: frame pairs with a suspicious jump, each with from_frame, to_frame, type (jump, flash, stutter, incoherent_motion or structural), delta_percent, region and message. When transition_hints is empty, nothing stood out.

wait_for

Waits until an element matching the predicate is on screen. Fails with TIMEOUT when it does not show in time.

With "stable": true, or with no predicate, it waits until the screen stops changing, for example after an animation. A screen that never settles does not fail the step unless the step has its own on_fail.

Also accepted as: extended_wait_until, wait_for_element, wait_until_visible

JSON

{"action": "wait_for", "predicate": {"text": "Welcome"}, "timeout_ms": 5000}
{"action": "wait_for", "stable": true, "timeout_ms": 3000}

.mob

wait_for "Welcome" timeout:5000
wait_stable timeout:3000
FieldTypeDefaultDescription
predicatePredicatenoneThe element to wait for.
stableboolfalseWait for the screen to stop changing. .mob: wait_stable.
timeout_msint10000How long to wait. Falls back to the script's defaults.timeout_ms before 10000. .mob: timeout:.
poll_interval_msint500Time between checks. 300 when waiting for the screen to settle. JSON only.

In the web context it waits for css_selector on the page.

delay

Waits a fixed time. Prefer wait_for when you know what you are waiting for.

Also accepted as: sleep, pause

JSON

{"action": "delay", "duration_ms": 1000}

.mob

delay 1000
FieldTypeDefaultDescription
duration_msintrequiredMilliseconds, more than 0.

checkpoint

Marks a point in the run. Checkpoints come back in the response's checkpoints list.

JSON

{"action": "checkpoint", "name": "logged_in", "metadata": {"user": "qa"}}

.mob

checkpoint "logged_in"
FieldTypeDefaultDescription
namestringrequiredCheckpoint name.
metadataobjectnoneAnything you want returned with it. JSON only.

Device controls

set_location

Sets a simulated GPS location.

Platforms: iOS. Android emulators, and Android phones on Android 12 or later.

JSON

{"action": "set_location", "lat": 40.7128, "lon": -74.006}

.mob

set_location 40.7128,-74.006
FieldTypeDefaultDescription
latnumberrequiredLatitude, -90 to 90.
lonnumberrequiredLongitude, -180 to 180.

A location of exactly 0, 0 is rejected.

reset_location

Stops the simulated location so the device uses its real one again.

Platforms: Same as set_location.

JSON

{"action": "reset_location"}

.mob

reset_location

set_permissions

Grants, revokes or resets an app's permissions without a prompt, so a test starts from a known state. Run it before open_app: changing a permission the app uses ends the app.

Names: all, bluetooth, calendar, camera, contacts, health, homekit, location, medialibrary, microphone, motion, notifications, phone, photos, reminders, siri, sms, speech, storage, userTracking. all is applied first, so {"all": "deny", "camera": "allow"} leaves only the camera granted.

Platforms: iOS simulators and Android. On a simulator only all, calendar, contacts, location, medialibrary, microphone, motion, photos, reminders, siri can be set, and other names are skipped. On Android, a permission the app does not ask for is skipped, as are homekit, reminders, siri, speech, userTracking.

JSON

{"action": "set_permissions", "bundle_id": "com.example.app", "permissions": {"location": "allow", "photos": "deny"}}

.mob

set_permissions com.example.app location=allow photos=deny
FieldTypeDefaultDescription
bundle_idstringrequiredBundle ID or package name.
permissionsobjectrequiredPermission name to allow, deny or unset. unset makes the app ask again.

biometrics

Simulates the biometric sensor. Enroll a face or finger before open_app, then send match or nomatch while the app shows its prompt.

Platforms: iOS simulators: all four states for faceid and touchid. Android: match and nomatch with fingerprint, with a finger already enrolled in Settings. Not available on a physical iPhone.

Also accepted as: biometric, face_id, touch_id, fingerprint

JSON

{"action": "biometrics", "state": "enroll", "sensor": "faceid"}
{"action": "biometrics", "state": "match"}

.mob

biometrics enroll faceid
biometrics match
FieldTypeDefaultDescription
statestringrequiredmatch, nomatch, enroll or unenroll. fail and no_match mean nomatch.
sensorstringfaceid on iOS, fingerprint on Androidfaceid or touchid on iOS, fingerprint on Android.

fold

Folds or unfolds a foldable iOS simulator. The step returns once the device has settled, and later steps measure against the screen now in use. Observe again after folding: positions from before the fold no longer apply.

Platforms: Foldable iOS simulators only.

Also accepted as: set_fold, fold_device

JSON

{"action": "fold", "state": "closed"}
{"action": "fold", "angle": 90}

.mob

fold closed
fold 90
FieldTypeDefaultDescription
statestringone ofclosed, book (half open) or open.
anglenumberone ofHinge angle, 0 to 180 degrees. Wins over state when both are set.

siri

Sends a command to Siri as text. MobAI taps a consent button if Siri asks (Yes, Turn On, Allow, OK, Continue, Enable or Confirm), reads Siri's reply, then closes Siri. The reply comes back as js_value and as the variable siri_response. If Siri asks a follow-up question, send a more specific command.

Platforms: iOS only.

JSON

{"action": "siri", "prompt": "Set a timer for 5 minutes"}

.mob

siri "Set a timer for 5 minutes"
FieldTypeDefaultDescription
promptstringrequiredThe command.

Web actions

Web steps work inside a web page: Safari tabs and app web views on an iPhone, Chrome tabs on Android. Use native actions for the browser's own controls (address bar, tabs) and web actions for the page's content.

Requirements

iOS: physical devices only, with Settings → Safari → Advanced → Web Inspector turned on. Simulators fail with CONTEXT_NOT_AVAILABLE. Android: phones and emulators, Chrome tabs only. Web views inside other apps are not reached.

select_web_context

Picks the web page that later web steps work on. Run it first: web steps fail until a page is selected. With no filter it takes the page in front, else the first one. The result lists every page under pages (id, title, url, app_id, is_active).

JSON

{"action": "select_web_context", "url_contains": "example.com"}
FieldTypeDefaultDescription
url_containsstringnoneFirst page whose URL contains this, ignoring case.
title_containsstringnoneFirst page whose title contains this, ignoring case.
page_idintnonePosition in the page list, from 0.

A page that has just started loading may not be listed yet. Give the step an on_fail retry, as in the web example below.

execute_js

Runs a JavaScript expression in the selected page and returns its value as js_value. An exception comes back as was_thrown and js_error while the step still passes. Write an expression, not a function body: document.title, not return document.title. Return strings, numbers or booleans, or wrap objects in JSON.stringify.

JSON

{"action": "execute_js", "script": "document.title", "store_as": "title"}
{"action": "execute_js", "script": "location.href = 'https://example.com/pricing'"}
FieldTypeDefaultDescription
scriptstringrequiredThe expression.
store_asstringnoneAlso keep the value as a variable.

Web forms of native actions

Add "context": "web" and put a CSS selector in the predicate as css_selector.

JSON

{"action": "tap", "context": "web", "predicate": {"css_selector": "button[type='submit']"}}
{"action": "type", "context": "web", "predicate": {"css_selector": "input#email"}, "text": "[email protected]", "clear_first": true}
{"action": "clear", "context": "web", "predicate": {"css_selector": "input#email"}}
{"action": "press_key", "context": "web", "key": "enter"}
{"action": "assert_exists", "context": "web", "predicate": {"css_selector": "div.loaded"}, "timeout_ms": 5000}
{"action": "observe", "context": "web", "include": ["ui_tree", "page_info"]}
SelectorFinds
#login-btnThe element with id="login-btn"
.btn-primaryElements with the class btn-primary
button.submitA button with the class submit
input[type='email']An input of type email
a[href*='login']Links whose address contains login

These work on the page: tap, type, type_secret, clear, press_key, observe and execute_js, and with a css_selector, assert_exists, assert_not_exists, wait_for, if_exists and the while and until forms of repeat. Every other action reads the app screen, where a css_selector fails with VALIDATION_ERROR.

Assertions

An assertion that does not hold fails with ASSERTION_FAILED. Each takes an optional message that replaces the default failure message.

assert_exists

Fails unless an element matching the predicate is on screen. By default it checks once. With timeout_ms it keeps checking until the element shows or the time runs out. When the element exists but is scrolled out of view, the error says so and lists it under candidates. In the web context it checks the page for css_selector.

Also accepted as: assert, assert_visible, assert_present

JSON

{"action": "assert_exists", "predicate": {"text_contains": "Success"}, "timeout_ms": 3000}

.mob

assert_exists ~"Success" timeout:3000
FieldTypeDefaultDescription
predicatePredicaterequiredThe element.
timeout_msint0How long to keep checking. 0 checks once.
messagestringnoneFailure message. JSON only.

assert_not_exists

Fails if an element matching the predicate is on screen. With timeout_ms it waits up to that long for the element to go away.

Also accepted as: assert_not_visible, assert_absent, assert_hidden

JSON

{"action": "assert_not_exists", "predicate": {"text": "Error"}, "timeout_ms": 2000}

.mob

assert_not_exists "Error" timeout:2000
FieldTypeDefaultDescription
predicatePredicaterequiredThe element.
timeout_msint0How long to wait for it to go away. 0 checks once.
messagestringnoneFailure message. JSON only.

assert_count

Fails unless exactly expected elements match. It checks once, without waiting. The result has match_count and matched_elements.

JSON

{"action": "assert_count", "predicate": {"type": "cell"}, "expected": 5}

.mob

assert_count type:cell expected:5
FieldTypeDefaultDescription
predicatePredicaterequiredWhat to count.
expectedintrequiredThe number of matches you expect.
messagestringnoneFailure message. JSON only.

assert_property

Fails unless a property of the element equals expected_value. The result has property_value, the value found.

JSON

{"action": "assert_property", "predicate": {"text": "Submit", "type": "button"}, "property": "enabled", "expected_value": true}
{"action": "assert_property", "predicate": {"type": "input", "text_contains": "Email"}, "property": "value", "expected_value": "[email protected]"}
FieldTypeDefaultDescription
predicatePredicaterequiredThe element. It must match one element.
propertystringrequiredenabled, visible, selected, focused, editable, scrollable, value or text.
expected_valueanynoneThe value to compare with. Booleans for the states, a string for value and text.
messagestringnoneFailure message.

assert_screen_changed

Fails unless enough of the screen changed since the last observe that read the UI tree. The change is the share of elements on the screen now that were not there at that observe. The result has screen_change_percent. Put the observe before the action you check, and no other observe between them.

JSON

{"action": "assert_screen_changed", "threshold_percent": 20}
FieldTypeDefaultDescription
threshold_percentint50The least change, in percent, that passes.
messagestringnoneFailure message.

JSON

{
  "version": "0.2",
  "steps": [
    {"action": "observe", "include": ["ui_tree"]},
    {"action": "tap", "predicate": {"text": "Next"}},
    {"action": "wait_for", "stable": true, "timeout_ms": 3000},
    {"action": "assert_screen_changed", "threshold_percent": 20}
  ]
}

Performance metrics

Collect CPU, memory, frame rate, network and battery metrics while a script runs. The metrics steps are JSON only.

metrics_start

Starts collecting metrics in the background. Collection goes on after the script ends, until metrics_stop in this script or a later one. A device has one session at a time: starting another one stops the first. The result has metrics_started with session_id, device_id and started_at.

JSON

{"action": "metrics_start", "types": ["system_cpu", "system_memory", "fps"], "label": "app_launch"}
{"action": "metrics_start", "bundle_id": "com.example.app", "capture_logs": true, "label": "checkout"}
FieldTypeDefaultDescription
typesstring[]["system_cpu", "system_memory"]What to collect, see the table below.
interval_msint1000Time between samples.
labelstringnoneA name for the session.
bundle_idstringnoneAlso collect CPU and memory for this app (the process type).
capture_logsboolfalseAlso save the device log's warnings and errors to a file.
thresholdsobjectsee belowLimits that mark anomalies. When set, give every limit you care about: limits left out count as 0.
Metric typeWhat it measures
system_cpuCPU use of the whole device, in percent
system_memoryMemory use of the whole device, in bytes and percent
fpsFrame rate and dropped frames
networkBytes received and sent
batteryLevel, drain rate and temperature
processCPU and memory of one app. Needs bundle_id.
ThresholdDefaultMeaning
cpu_high80CPU percent above which a spike is reported
fps_low30Frame rate below which a drop is reported
fps_jank20Frame rate below which a sample counts toward jank_percent
memory_growth_mb_min50Memory growth per minute, in MB, that is reported
memory_high85Memory percent above which use is reported as high
battery_drain_rate10Battery percent per hour above which drain is reported

metrics_stop

Stops the session and returns metrics_summary. Fails when no session is running on the device.

JSON

{"action": "metrics_stop"}
FieldTypeDefaultDescription
formatstringsummarysummary or detailed. Both return the summary. The raw samples are in the file at session.data_file.

Result

{
  "metrics_summary": {
    "session": {"label": "app_launch", "duration_seconds": 12.4, "sample_count": 12, "session_id": "a1b2c3", "data_file": "/path/to/samples.jsonl", "logs_available": false},
    "overall_health": "healthy",
    "health_score": 96,
    "system_cpu": {"avg": 41.2, "max": 72.5, "min": 12.0, "p95": 68.1, "status": "ok"},
    "system_memory": {"avg_bytes": 2147483648, "max_bytes": 2252341248, "total_bytes": 6442450944, "avg_percent": 33.3, "max_percent": 35.0, "growth_mb": 12.5, "trend": "stable", "status": "ok"},
    "fps": {"avg": 57.8, "min": 44.0, "max": 60.0, "p5": 48.0, "jank_percent": 0, "status": "ok"}
  }
}

Other summary keys, present when collected: network (total_rx_mb, total_tx_mb, status), battery (start_level, end_level, drain_percent, drain_per_hour, status), app (bundle_id, cpu_avg, cpu_max, memory_avg_bytes, memory_max_bytes, memory_growth_mb, status), and session.logs_file with capture_logs. When limits were crossed, anomalies lists them under cpu_spikes, fps_drops, memory_issues and battery_drain, with recommendations in plain words. overall_health is healthy, warning or critical, and health_score runs from 0 to 100.

JSON

{
  "version": "0.2",
  "steps": [
    {"action": "metrics_start", "types": ["system_cpu", "system_memory", "fps"], "label": "app_launch"},
    {"action": "open_app", "bundle_id": "com.example.app", "fresh": true},
    {"action": "wait_for", "predicate": {"text": "Home"}, "timeout_ms": 10000},
    {"action": "delay", "duration_ms": 2000},
    {"action": "metrics_stop"}
  ]
}

Predicates

A predicate describes the element a step acts on. MobAI looks for it in the current UI tree, the same tree observe returns.

Native predicate keys

KeyTypeMatches
textstringExact text, case-sensitive. An element with no text is matched on its value.
text_containsstringText that contains this, ignoring case. The safest choice for most text.
text_starts_withstringText that starts with this, ignoring case. JSON only.
text_regexstringText that matches this regular expression (Go syntax). Start it with (?i) to ignore case.
valuestringThe element's current value, exactly: what was typed into a field, not its placeholder. Secure fields report a masked value.
value_containsstringA value that contains this, ignoring case.
accessibility_idstringThe id shown as #id in the UI tree. A leading # is fine. When exactly one element has the id, it matches even if other keys disagree.
typestringElement type, see below.
labelstringOlder key: the accessibility label or id, or the text. Prefer accessibility_id or text. JSON only.
label_containsstringOlder key: label that contains this, ignoring case. JSON only.
enabledboolEnabled state.
visibleboolTrue for elements that can be hit or have a size on screen.
selectedboolSelected state.
nearobjectElements near another one, see below.
bounds_hintstringElements whose center is in a region: top_half, bottom_half, left_half, right_half, center, top_left, top_right, bottom_left, bottom_right.
parent_ofPredicateThe closest container of the element this inner predicate finds, that also matches the other keys.
indexintWhen several elements match, the one at this position, from 0.
css_selectorstringWeb context only, see Web actions.

All keys in a predicate must match. Prefer text_contains or text_regex over text, since exact text breaks with state, locale and counts. Add type when a label and a control share the same text.

Types

TypeAlso matches
buttonButtons and image buttons
inputText fields, secure fields, search fields, and any element you can type into. Synonyms: text_field, textfield, text_box, textbox, field
textStatic text. Synonym: label
imageImages
cellTable and collection cells
scrollviewScroll views, tables, collection views and lists
switchSwitches, toggle buttons and checkboxes. Synonyms: checkbox, check_box, toggle
sliderSliders and seek bars
linkLinks
iconIcons
*Any type

A raw type from the UI tree works too, such as StaticText, SearchField, PickerWheel or android.widget.EditText. A word MobAI does not recognize as a type fails with VALIDATION_ERROR, which lists the valid types.

near

KeyTypeMeaning
textstringText of the anchor element. An exact match is preferred, then text that contains it.
text_containsstringText the anchor contains, ignoring case.
directionstringabove, below, left, right or any (default). Where the element must be, seen from the anchor.
max_distanceintLargest distance between the two centers, horizontal plus vertical, in screen units. JSON only.

JSON

{"action": "tap", "predicate": {"type": "input", "near": {"text": "Email", "direction": "below"}}}
{"action": "tap", "predicate": {"type": "button", "parent_of": {"text_contains": "Remove"}, "index": 0}}
{"action": "tap", "predicate": {"text": "Edit", "bounds_hint": "top_half"}}

When several elements match

Actions that need one element (tap, type, toggle and the like) narrow the matches down in this order:

StepRule
1Nested duplicates count once: a button and its own label are one match, and the more interactive one is kept.
2With near: the match closest to the anchor. index then counts from the closest.
3With index: the match at that position. Out of range fails with NO_MATCH.
4With bounds_hint: the match closest to the center of that region.
5Otherwise the step fails with AMBIGUOUS_MATCH and lists the matches under candidates. tap and long_press first take the one control among them if there is exactly one.

Waits and checks (wait_for, assert_exists, if_exists, repeat) only need one match and never fail on several. drag uses the first match. Only elements on screen count, unless an action says otherwise.

Predicates in .mob

.mobJSON
"Save"{"text": "Save"}
~"Save"{"text_contains": "Save"}
/"^\d+ items$"/{"text_regex": "^\\d+ items$"}. A trailing i adds (?i).
#login_button{"accessibility_id": "login_button"}
type:button{"type": "button"}
near "Email", near ~"Email"{"near": {"text": "Email"}}, {"near": {"text_contains": "Email"}}
direction:below{"near": {"direction": "below"}}
[2]{"index": 2}
bounds:top_half{"bounds_hint": "top_half"}
parent_of:"Delete"{"parent_of": {"text": "Delete"}}
enabled:true, visible:true, selected:false{"enabled": true} and so on
value:"hello", value_contains:"@"{"value": "hello"}, {"value_contains": "@"}

A line can use settings alone, with no text: tap type:button bounds:bottom_half. text_starts_with, label, label_contains, near.max_distance and css_selector have no .mob form.

Control flow

if_exists

Checks the screen once, without waiting, and runs then when an element matches, else else. Steps inside run in order, and the first one that fails ends the branch.

The if_exists step itself still passes when a step inside fails: read then_results or else_results, and condition_met. Steps inside ignore their own on_fail, and the whole block shares the time limit of the if_exists step.

JSON

{"action": "if_exists", "predicate": {"text": "Allow"}, "then": [{"action": "tap", "predicate": {"text": "Allow"}}], "else": [{"action": "checkpoint", "name": "no_prompt"}]}

.mob

if_exists "Allow" {
  tap "Allow"
} else {
  checkpoint "no_prompt"
}
FieldTypeDefaultDescription
predicatePredicaterequiredWhat to look for.
thenstep[]noneSteps to run when it is on screen.
elsestep[]noneSteps to run when it is not.

repeat

Runs body in a loop. Set exactly one of times, while, until or condition. Inside the body, ${repeat_index} is the loop count from 0. Loops can hold if_exists blocks and other loops.

A step in the body that fails ends the loop and fails the repeat step, so the repeat step's own on_fail applies. Body steps ignore their own on_fail. The whole loop shares the repeat step's time limit, 2 minutes unless you set timeout_ms. The result has iterations and body_results.

JSON

{"action": "repeat", "times": "3", "body": [{"action": "tap", "predicate": {"text": "Add"}}]}
{"action": "repeat", "while": {"text_contains": "Loading"}, "max_iterations": 20, "body": [{"action": "delay", "duration_ms": 500}]}
{"action": "repeat", "until": {"text": "Done", "type": "button"}, "body": [{"action": "scroll", "direction": "down"}]}
{"action": "repeat", "condition": "(vars.n || 0) < 3", "body": [{"action": "eval_script", "script": "vars.n = (vars.n || 0) + 1"}]}

.mob

repeat 3 times {
  tap "Add"
}
repeat while ~"Loading" max:20 {
  delay 500
}
repeat until "Done" type:button {
  scroll down
}
repeat while js:"(vars.n || 0) < 3" {
  eval "vars.n = (vars.n || 0) + 1"
}
FieldTypeDefaultDescription
timesstringone ofRun this many times. A string, so "${count}" works.
whilePredicateone ofRun while this matches, checked before each round.
untilPredicateone ofRun until this matches.
conditionstringone ofRun while this JavaScript expression is true. Reads variables as vars.name. .mob: while js:"...".
max_iterationsint100Limit for while, until and condition. Reaching it fails the step. .mob: max:.
bodystep[]requiredThe steps to repeat.

Variables

Write ${name} in a step to use a variable. It is filled in when the step runs, so a value saved by an earlier step is ready for later ones. A name with no value fails the step with unknown variable: ${name}. At the end of the run, every variable comes back in the response under storage.

SourceHow
paramsScript key with default values: {"params": {"email": "[email protected]"}}. A parameter with an empty default has no value until set. In .mob: # Param: email = [email protected]. Named APIs (POST /api/v1/apis/run/{name}) pass values for a .mob file's parameters.
extractSaves text, a screenshot path or a fixed value, see below.
store_asOn eval_script, run_script, execute_js, copy_text, observe and audit.
varsJavaScript in eval_script, run_script and repeat conditions reads and writes variables as vars.name.
siri_responseSet by siri.
repeat_indexThe loop count, inside a repeat body.

Variables are filled in these keys: bundle_id, text, name, key, target, file_path, log_path, message, prompt, direction, distance, amount, state, store_as, extract_regex, url_contains, title_contains, times, script for execute_js, and the predicate keys text, text_contains, text_starts_with, text_regex, accessibility_id, label, label_contains, css_selector, bounds_hint and near's text and text_contains. Other keys, such as url, value, secret_id, numbers and coordinates, take the text as written.

extract

Saves a value as a variable, under extract_key. Three kinds:

JSON

{"action": "extract", "extract_key": "order_id", "extract_kind": "text", "predicate": {"text_contains": "Order #"}, "extract_regex": "#([0-9]+)"}
{"action": "extract", "extract_key": "proof", "extract_kind": "screenshot", "file_path": "~/mobai-shots"}
{"action": "extract", "extract_key": "user", "extract_kind": "literal", "text": "qa-user"}

.mob

extract order_id from ~"Order #" regex:"#([0-9]+)"
extract proof screenshot
extract user = "qa-user"
FieldTypeDefaultDescription
extract_keystringrequiredVariable name: letters, digits and _, not starting with a digit.
extract_kindstringrequiredtext: the text of the element predicate finds (its text, else its value, else its description or id). screenshot: saves a screenshot and keeps its path. literal: keeps text.
predicatePredicatenoneFor text.
extract_regexstringnoneFor text: keep the first group of this regular expression, or the whole match when it has no group. No match fails the step.
file_pathstringtemp folderFor screenshot: folder to save into. JSON only.
textstringnoneFor literal: the value. Can use other variables.

eval_script

Runs JavaScript on this computer, not on the device, to compute values, seed test data or call a backend between steps. The value of the last expression comes back as js_value, and console.log output as logs.

JSON

{"action": "eval_script", "script": "vars.user_id = 'u-' + Date.now()"}
{"action": "eval_script", "script": "1 + 1", "store_as": "two"}

.mob

eval "vars.user_id = 'u-' + Date.now()"
eval "1 + 1" store_as:two
FieldTypeDefaultDescription
scriptstringrequiredJavaScript source.
store_asstringnoneKeep the value as a variable.

Available in the script: vars (also output), the variables. http.get, http.post, http.put, http.delete and http.request(method, url, opts), which wait for the answer and return {status, ok, headers, body}. opts takes headers, body (an object is sent as JSON) and timeout_ms. console.log, console.info, console.warn and console.error, shown under logs. There is no file access and no require. ${...} is not filled in inside script, so JavaScript template strings work as usual.

run_script

Runs a JavaScript file on this computer, with the same globals as eval_script. In .mob, the path is relative to the .mob file and the file is read when the test is loaded.

JSON

{"action": "run_script", "script_path": "/Users/me/tests/seed.js"}
{"action": "run_script", "script": "var r = http.get('https://api.example.com/seed')\nvars.seed_id = JSON.parse(r.body).id"}

.mob

script "./seed.js"
FieldTypeDefaultDescription
script_pathstringone ofAbsolute path of the file.
scriptstringone ofThe source, in place of a file.
store_asstringnoneKeep the value of the last expression as a variable. JSON only.

Failure strategies

A step's on_fail decides what happens when it fails.

StrategyWhat happens
abortStop the run. The default.
skipMark the step skipped and go on. A skipped step does not fail the run.
retryRun the step again, up to max_retries times, retry_delay_ms apart. When every try fails, fallback_strategy decides, or the run stops.
replanStop the run and return what happened so far, for callers that plan the next steps.
require_userStops the run, like abort.
FieldTypeDefaultDescription
strategystringabortOne of the strategies above.
max_retriesint3Tries after the first one, for retry.
retry_delay_msint1000Wait before each try.
fallback_strategyobjectabortAnother on_fail object, used when retries run out. JSON only.

JSON

{"action": "tap", "predicate": {"text": "Continue"}, "on_fail": {"strategy": "retry", "max_retries": 3, "retry_delay_ms": 1000, "fallback_strategy": {"strategy": "skip"}}}
{"action": "tap", "predicate": {"text": "Not now"}, "on_fail": {"strategy": "skip"}}

.mob

tap "Continue" on_fail:retry retries:3 retry_delay:1000
tap "Not now" on_fail:skip

Put on_fail at the top of the script to set it for every step. A step's own on_fail wins. Steps inside if_exists and repeat ignore their own on_fail.

Automatic recovery

If the bridge stops during a step, MobAI waits up to 60 seconds for it to come back and runs the step again once. With "alerts": "accept" or "dismiss", a failed step is checked for a system alert in the way, such as a permission prompt or a crash dialog. MobAI taps the allow-style or deny-style button, up to three alerts in a row, and runs the step again once. Alerts inside the app are never touched. Both happen before on_fail is applied.

Error codes

CodeMeaning
NO_MATCHNo element matched. If matching elements exist out of view, candidates lists them: scroll to them. If the message says they are on screen but not visible yet, they are still appearing or covered by the keyboard or a sheet: wait for them.
AMBIGUOUS_MATCHSeveral elements matched an action that needs one. candidates lists them. Add index, near, bounds_hint or a type.
TIMEOUTA wait ran out, or the step reached its time limit.
ASSERTION_FAILEDAn assertion did not hold.
VALIDATION_ERRORA predicate key, a type or a distance word is not known. valid_predicate_keys or valid_types lists the valid ones.
INVALID_ACTIONThe action name is not known.
CONTEXT_NOT_AVAILABLEA web step ran on a device without web support, such as an iOS simulator.
SAFETY_BLOCKEDMobAI's safety settings for the device blocked the action.
EXECUTION_ERRORAnything else. The message names the cause.

The error object holds code, message and, when they apply, predicate, candidates (index, type, text, bounds), a one-line hint, valid_predicate_keys and valid_types.

Response format

dsl/execute returns one JSON object for the whole run.

Response

{
  "success": false,
  "step_results": [
    {"success": true, "step_index": 0, "action": "open_app", "started_at_ms": 1767000000000, "duration_ms": 1830, "result": {}},
    {
      "success": false,
      "step_id": "save",
      "step_index": 1,
      "action": "tap",
      "started_at_ms": 1767000001830,
      "duration_ms": 412,
      "error": {
        "code": "AMBIGUOUS_MATCH",
        "message": "2 elements matched",
        "predicate": {"text": "Save"},
        "candidates": [
          {"index": 12, "type": "Button", "text": "Save", "bounds": {"x": 16, "y": 120, "width": 80, "height": 44}},
          {"index": 31, "type": "Button", "text": "Save", "bounds": {"x": 16, "y": 700, "width": 80, "height": 44}}
        ],
        "hint": "disambiguate with one of: index (0-based), near {text, direction}, bounds_hint (top_half/bottom_half/...), or a more specific type/text. See candidates for the choices."
      },
      "debug": {"ui_tree": "[12] Button \"Save\" (16,120 80x44)\n..."}
    }
  ],
  "total_time_ms": 2310
}

Top level

KeyMeaning
successTrue when the run reached its end. Skipped steps are allowed.
step_resultsOne entry per top-level step that ran, in order.
total_time_msLength of the whole run.
checkpointsThe checkpoint steps that ran, with name and metadata.
storageEvery variable at the end of the run.
metadataA summary of the last UI tree an observe or audit read, when one did: screen_title, interactive_elements (buttons, switches, text_fields), visible_text and warnings.

Each step result

KeyMeaning
successWhether the step passed.
step_idThe step's id, when it has one.
step_indexPosition in its list, from 0. Steps inside if_exists and repeat count within their block.
actionThe action that ran, with aliases resolved.
started_at_msStart time, in milliseconds since 1970.
duration_msHow long the step took. After a retry, the length of the last try.
resultWhat the action returned. Keys depend on the action: matched_element, observations, js_value, js_error, was_thrown, logs, pages, condition_met, then_results, else_results, iterations, body_results, toggled, match_count, matched_elements, property_value, screen_change_percent, screenshot_path, log_path, launch, recording_path, frame_count, contact_sheets, transition_hints, metrics_started, metrics_summary.
errorWhy the step failed, see Error codes.
debugIn explore mode, a failed step's UI tree, or a screenshot path when the tree could not be read.
skippedTrue for a step skipped by on_fail (its success stays false), and for an earlier observe skipped in explore mode.
warningsNotes on a step that passed, such as a tap after which the screen did not change.

matched_element has index, type, text and bounds (x, y, width, height).

The endpoint answers 200 with this body whether the run passed or failed. A body that is not valid JSON, or a step whose keys have the wrong type, gets 400 with {"code": "EXECUTION_ERROR", "message": "invalid DSL script: ..."} and no step runs. An unknown alerts value gets 500 with the same shape. An unknown device, or a bridge that cannot start, is reported before any step runs.

Complete examples

Sign in with a parameter and a saved password

Clears permission prompts on the way, waits for each screen, and ends with the UI tree so you can see where the run landed.

JSON

{
  "version": "0.2",
  "params": {"email": "[email protected]"},
  "alerts": "accept",
  "steps": [
    {"action": "open_app", "bundle_id": "com.example.app", "fresh": true},
    {"action": "wait_for", "predicate": {"text_contains": "Sign in"}, "timeout_ms": 10000},
    {"action": "type", "predicate": {"text_contains": "Email"}, "text": "${email}", "clear_first": true},
    {"action": "type_secret", "predicate": {"text_contains": "Password"}, "secret_id": "qa-password"},
    {"action": "tap", "predicate": {"text": "Sign in", "type": "button"}},
    {"action": "assert_exists", "predicate": {"text_contains": "Welcome"}, "timeout_ms": 5000},
    {"action": "wait_for", "stable": true, "timeout_ms": 3000},
    {"action": "observe", "include": ["ui_tree"]}
  ]
}

.mob

# Param: email = [email protected]
# Alerts: accept

app "com.example.app" fresh
wait_for ~"Sign in" timeout:10000
type ~"Email" → "${email}" clear
type_secret ~"Password" → "qa-password"
tap "Sign in" type:button
assert_exists ~"Welcome" timeout:5000
wait_stable timeout:3000
observe

Get past onboarding and read a total

Taps Next while it is on screen, closes an optional prompt, scrolls to the total, saves the amount as total and keeps a screenshot.

JSON

{
  "version": "0.2",
  "steps": [
    {"action": "open_app", "bundle_id": "com.example.shop"},
    {"action": "repeat", "while": {"text": "Next", "type": "button"}, "max_iterations": 5, "body": [
      {"action": "tap", "predicate": {"text": "Next", "type": "button"}},
      {"action": "wait_for", "stable": true, "timeout_ms": 2000}
    ]},
    {"action": "if_exists", "predicate": {"text": "Not now"}, "then": [
      {"action": "tap", "predicate": {"text": "Not now"}}
    ]},
    {"action": "scroll", "direction": "down", "to_element": {"predicate": {"text_contains": "Total"}}, "max_scrolls": 5,
     "on_fail": {"strategy": "retry", "max_retries": 2}},
    {"action": "extract", "extract_key": "total", "extract_kind": "text", "predicate": {"text_contains": "Total"}, "extract_regex": "([0-9]+[.][0-9]{2})"},
    {"action": "screenshot", "file_path": "~/mobai-shots"}
  ]
}

.mob

app "com.example.shop"
repeat while "Next" type:button max:5 {
  tap "Next" type:button
  wait_stable timeout:2000
}
if_exists "Not now" {
  tap "Not now"
}
scroll down to ~"Total" max_scrolls:5 on_fail:retry retries:2
extract total from ~"Total" regex:"([0-9]+[.][0-9]{2})"
screenshot "~/mobai-shots"

The response's storage holds total.

Fill a web form on a phone

Opens the page, retries until the browser lists it, waits for the field, submits, and reads the title of the next page. JSON only, since .mob has no web steps.

JSON

{
  "version": "0.2",
  "steps": [
    {"action": "open_link", "url": "https://example.com/signup"},
    {"action": "select_web_context", "url_contains": "example.com/signup",
     "on_fail": {"strategy": "retry", "max_retries": 5, "retry_delay_ms": 1000}},
    {"action": "assert_exists", "context": "web", "predicate": {"css_selector": "input[type='email']"}, "timeout_ms": 8000},
    {"action": "type", "context": "web", "predicate": {"css_selector": "input[type='email']"}, "text": "[email protected]", "clear_first": true},
    {"action": "press_key", "context": "web", "key": "enter"},
    {"action": "assert_exists", "context": "web", "predicate": {"css_selector": ".signup-done"}, "timeout_ms": 8000},
    {"action": "execute_js", "script": "document.title", "store_as": "page_title"}
  ]
}