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"Infinite Loops and break
When a loop has no fixed count or duration, use while: true to keep it running and exit with break when a condition is met:
- repeat:
while: true # Continue until break or task cancellation
commands:
- scroll
- break:
when:
visible: "No more content"break also supports a shorthand form:
- repeat:
while: true
commands:
- tapOn: "Next"
- break # Exit the current loop unconditionallybreak can only be used inside repeat and exits the nearest enclosing loop. The flow then continues with the command after that loop. Using break outside a loop causes the flow to fail.
times, duration, and while are independent optional stop conditions. Only explicitly supplied conditions apply. If several are supplied, the first one reached stops the loop. Omitting all three, or using while: true, creates an infinite loop that can still be stopped by break, task cancellation, or service shutdown.
Generate a 2FA Code
2fa generates a standard six-digit TOTP code from a Base32 secret and stores the result in a variable:
- 2fa:
code: "${TOTP_SECRET}" # Base32 TOTP secret
outputVariable: otp
- inputText: "${otp}"| Parameter | Type | Required | Description |
|---|---|---|---|
code | string | Yes | Base32 TOTP secret; pass it through an environment variable |
outputVariable | string | Yes | Variable that stores the generated code; reference it later with ${variableName} |
2fa uses standard TOTP settings: SHA-1, a 30-second period, and six digits. Do not write real secrets directly in YAML files.
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
