Intermediate
Master variables, conditions, loops, and branching to write flexible automation flows.
Variables and Environment Variables: env / defineVariables
Define in Configuration (env)
appId: com.example.app
env:
USERNAME: admin
PASSWORD: "123456"
API_URL: "https://api.example.com"
---
- inputText: "${USERNAME}" # Use the variableDefine in Commands (defineVariables)
- defineVariables:
counter: "0"
greeting: "Hello"
- inputText: "${greeting}" # Will input: HelloDifferences
env | defineVariables | |
|---|---|---|
| Location | Configuration section (above ---) | Command section (below ---) |
| Timing | Takes effect before the flow starts | Takes effect when this line is executed |
| Use case | Global config (accounts, passwords, etc.) | Dynamically define variables during the flow |
Template Expressions: $
Use ${...} in any command parameter to reference variables or evaluate JavaScript expressions.
Reference Variables
- inputText: "${USERNAME}"Math Operations
- sleep: ${Math.random() * 5000} # Random 0~5 secondsString Concatenation
- evalScript: "${fullName = firstName + ' ' + lastName}"
- inputText: "${fullName}"Conditional Expressions
- assertTrue: "${counter > 0}"Evaluation Timing
${...} is evaluated at command execution time, but there is one important rule:
- Expressions inside
repeat'scommandsandbranchblocks are not evaluated in advance - They are evaluated each time they are reached, so
${Math.random()}produces a different random value on each iteration
- repeat:
times: "3"
commands:
# Math.random() is re-evaluated on each iteration, producing a different random number
- sleep: ${Math.random() * 5000}Escaping ${}
If you need to input a literal ${...}, add a backslash before it:
- inputText: "Price is \\${amount} dollars" # Actual input: Price is ${amount} dollarsElement Selectors In-Depth
The tapOn: "Login" used earlier is just the simplest text matching. Selectors support many more matching methods.
Text Matching: text
- tapOn:
text: "Login" # Text matching (case-insensitive)Regex Matching
- tapOn:
text: "Login.*" # Match any text starting with "Login"
- tapOn:
text: "Username|User name" # Match "Username" or "User name"ID Matching
- tapOn:
id: "btn_login" # Match by resource IDIndex
When there are multiple elements with the same name on screen, use index to specify which one:
- tapOn:
text: "Delete"
index: 0 # The first "Delete" button (0-based)
- tapOn:
text: "Delete"
index: -1 # The last "Delete" buttonState Matching
- tapOn:
id: "checkbox"
enabled: true # Only match enabled elements
checked: false # Only match unchecked elementsSupported states: enabled, selected, checked, focused.
Spatial Relationships (Relative Position)
When an element has no unique identifier, you can locate it by its spatial relationship to another element.
# Tap the input field below "Username"
- tapOn:
below: { text: "Username" }
# Tap the input field to the right of the "Password" label
- tapOn:
rightOf: { text: "Password" }Four directions: below, above, leftOf, rightOf.
Hierarchy Relationships
# Tap a button inside a container
- tapOn:
text: "OK"
childOf: { id: "dialog_container" }
# Tap a card that contains a "VIP" child element
- tapOn:
containsChild: { text: "VIP" }
# Tap a container that contains multiple child elements
- tapOn:
containsDescendants:
- text: "Title"
- text: "Subtitle"Size Matching
- tapOn:
width: 200
height: 100
tolerance: 5 # Allow +/- 5 pixel toleranceElement Traits
Match by an element's interactive traits; separate multiple traits with spaces:
- tapOn:
traits: "clickable enabled" # Match elements that are clickable and enabledoptional Flag
- tapOn:
text: "Skip Ad"
optional: true # Skip if not found, no errorlabel Flag (Log Readability)
- tapOn:
id: "btn_submit"
label: "Tap submit button" # This description will appear in logsComplete Example
- tapOn:
text: "OK"
below: { text: "Are you sure you want to delete?" }
childOf: { id: "confirm_dialog" }
enabled: true
optional: true
label: "Tap confirm delete button"Long Press: longPressOn
Long press on an element.
- longPressOn: "Settings"
- longPressOn:
text: "file.txt"
waitToSettleTimeoutMs: 3000Double Tap: doubleTapOn
Double tap on an element.
- doubleTapOn: "Image"
- doubleTapOn:
text: "Favorite"
delay: 250 # Interval between the two taps (default: 100 ms)
waitToSettleTimeoutMs: 3000 # Wait for screen to settle after double tap, up to 3 secondsScroll Until Visible: scrollUntilVisible
Continuously scrolls the page until the target element appears.
- scrollUntilVisible:
element:
text: "About Phone" # The element to find
direction: DOWN # Scroll direction
timeout: "30000" # Scroll for up to 30 secondsAll Parameters
- scrollUntilVisible:
element: # Required: the element selector to find
text: "Target Element"
direction: DOWN # Direction: UP / DOWN / LEFT / RIGHT, default DOWN
timeout: "20000" # Timeout in ms, default 20000
speed: "40" # Speed: 0 (slowest) ~ 100 (fastest), default 40
visibilityPercentage: 100 # Element visibility percentage 0~100, default 100
centerElement: false # Whether to try scrolling the element to screen center, default false
from: # Optional: scroll within a specific container (see [Advanced - Scrolling Within a Container](./advanced#scrolling-within-a-container))
id: "list_container"
waitToSettleTimeoutMs: 3000 # Optional: timeout for waiting for screen to settle after each scrollPractical Example: Find "About Phone" in Settings
- launchApp
- scrollUntilVisible:
element:
text: "About Phone"
direction: DOWN
timeout: "30000"
- tapOn: "About Phone"Conditional Execution: when
Add a when condition to any command -- the command only executes when the condition is met.
Execute When Element Is Visible
- tapOn:
text: "Skip"
when:
visible: { text: "Ad" } # Only tap "Skip" when "Ad" is on screenExecute When Element Is Not Visible
- tapOn:
text: "Login"
when:
notVisible: { text: "Welcome" } # Only tap "Login" when "Welcome" is not on screenScript Condition
- tapOn:
text: "Next Page"
when:
true: "${counter < 10}" # Only execute when counter is less than 10All Fields Supported by when
| Field | Type | Description |
|---|---|---|
visible | Selector | True when element is visible |
notVisible | Selector | True when element is not visible |
true | String | True when the JS expression evaluates to truthy |
label | String | Log label |
When multiple fields are specified simultaneously, the command executes only when all conditions are met (AND logic).
Probability Execution: chance
Make a command execute with a certain probability -- useful for randomized testing scenarios.
- tapOn:
text: "Skip"
optional: true
chance: 0.3 # 30% chance of tapping Skip- tapOn:
text: "View Details"
optional: true
chance: 0.1 # 10% chance of viewing detailschance value ranges from 0.0 to 1.0:
0.0= never execute0.5= 50% probability1.0= always execute
Tip:
chanceis typically used withoptional: true. This is because if the probability triggers but the element doesn't exist, the command will throw an error without optional.
Loops: repeat
Fixed Iteration Count
- repeat:
times: "5"
commands:
- scroll
- sleep: 1000Executes 5 times: scroll, then wait 1 second.
Time-based Loop
- repeat:
duration: "60000" # Loop for 60 seconds (1 minute)
commands:
- swipe: { direction: UP }
- sleep: [3000, 8000]Runs continuously for 1 minute.
Random Duration Loop
- repeat:
duration: [300000, 600000] # Random 5~10 minutes (a random value is chosen at startup)
commands:
- scroll
- sleep: [3000, 8000]Note: The array form of
durationrandomly selects a value when the flow starts, and the same duration is used throughout the entire loop.
Count + Time (Whichever Comes First)
- repeat:
times: "50" # Up to 50 iterations
duration: "600000" # Or up to 10 minutes
commands:
- scrollConditional Loop (while)
- repeat:
while:
visible: { text: "Loading..." }
commands:
- sleep: 1000 # Keep waiting as long as "Loading..." is visible- defineVariables:
counter: "0"
- repeat:
while:
true: "${counter < 10}" # Loop while counter < 10
commands:
- evalScript: "${counter = counter + 1}"
- tapOn: "Next"Limits: A single repeat can execute at most 1,000 iterations to prevent infinite loops. The default maximum duration is 30 minutes.
Conditional Branching: branch
Similar to if / else if / else in programming languages -- checks conditions from top to bottom and executes the first matching branch.
- branch:
# if: "Choose from Gallery" is on screen
- when: { visible: "Choose from Gallery" }
commands:
- tapOn: "Choose from Gallery"
# else if: "Change Photo" is on screen
- when: { visible: "Change Photo" }
commands:
- tapOn: "Change Photo"
- tapOn: "Choose from Gallery"
# else: none of the above match
- commands:
- backWith Script Conditions
- branch:
- when: { true: "${counter > 10}" }
commands:
- tapOn: "Done"
- when: { true: "${counter > 5}" }
commands:
- tapOn: "Continue"
- commands:
- tapOn: "Start"Sub-flows: runFlow
Package a group of commands as a sub-flow; can be used with conditions and probability.
Inline Commands
- runFlow:
commands:
- tapOn: "Settings"
- tapOn: "Account"
- backReference an External File
- runFlow: "subflow/common-auth.yaml"
# Or object form
- runFlow:
file: "subflow/common-auth.yaml"Note:
fileandcommandscannot be used together; choose one.
Conditional Sub-flow
- runFlow:
when:
visible: { text: "Login" }
commands:
- tapOn: "Login"
- inputText: "${PASSWORD}"
- tapOn: "OK"Probability Sub-flow
- runFlow:
chance: 0.1 # 10% chance of executing the feedback flow
commands:
- tapOn: "Feedback"
- inputText: "Test feedback"
- pressKey: Enter
- backSub-flow with Variables
- runFlow:
env:
TOKEN: "${authToken}"
commands:
- httpRequest:
url: "https://api.example.com"
headers:
Authorization: "Bearer ${TOKEN}"Exception Handlers: exceptionHandlers
Automatically handles unexpected popups (permission requests, ads, system prompts, etc.). Defined in the configuration section and takes effect globally.
How It Works
- A command fails (e.g., element not found)
- The engine checks if the current screen matches any exception handler
- If it does, it automatically taps the matching element to dismiss the popup
- Then retries the original failed command
Short Form
appId: com.example.app
exceptionHandlers:
- "Allow" # Automatically tap "Allow"
- "Got it" # Automatically tap "Got it"
- "Maybe later" # Automatically tap "Maybe later"
---
- launchAppObject Form (Advanced)
exceptionHandlers:
- text: "Skip"
maxTriggerCount: 3 # Trigger at most 3 times, then stop handling
- id: "close_button"
below: { text: "Ad" } # Supports spatial relationship selectorsCustom Wait: extendedWaitUntil
Wait for an element to appear or disappear with a custom timeout (assertVisible has a fixed timeout of 17 seconds; here you can customize it).
Wait for Element to Appear
- extendedWaitUntil:
visible:
text: "Loading complete"
timeout: "30000" # Wait up to 30 secondsWait for Element to Disappear
- extendedWaitUntil:
notVisible:
id: "loading_spinner"
timeout: "15000" # Wait for "loading" to disappear, up to 15 secondsIntermediate Practice: Browse Feed for 2 Minutes
appId: com.example.demo
name: Browse Feed for 2 Minutes
---
# Launch the Demo App and confirm we're on the feed page
- launchApp
- assertVisible: "For You"
# Browse for 2 minutes
- repeat:
duration: "120000"
commands:
# Swipe up to next content
- swipe:
direction: UP
duration: [200, 500]
waitToSettleTimeoutMs: 0
# Stay on each item for 3~8 seconds
- sleep: [3000, 8000]
# Done
- stopAppReady to unlock the full power? → Advanced
