Skip to content

Intermediate

Master variables, conditions, loops, and branching to write flexible automation flows.


Variables and Environment Variables: env / defineVariables

Define in Configuration (env)

yaml
appId: com.example.app
env:
  USERNAME: admin
  PASSWORD: "123456"
  API_URL: "https://api.example.com"
---
- inputText: "${USERNAME}"      # Use the variable

Define in Commands (defineVariables)

yaml
- defineVariables:
    counter: "0"
    greeting: "Hello"
    
- inputText: "${greeting}"      # Will input: Hello

Differences

envdefineVariables
LocationConfiguration section (above ---)Command section (below ---)
TimingTakes effect before the flow startsTakes effect when this line is executed
Use caseGlobal 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

yaml
- inputText: "${USERNAME}"

Math Operations

yaml
- sleep: ${Math.random() * 5000}         # Random 0~5 seconds

String Concatenation

yaml
- evalScript: "${fullName = firstName + ' ' + lastName}"
- inputText: "${fullName}"

Conditional Expressions

yaml
- assertTrue: "${counter > 0}"

Evaluation Timing

${...} is evaluated at command execution time, but there is one important rule:

  • Expressions inside repeat's commands and branch blocks are not evaluated in advance
  • They are evaluated each time they are reached, so ${Math.random()} produces a different random value on each iteration
yaml
- 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:

yaml
- inputText: "Price is \\${amount} dollars"  # Actual input: Price is ${amount} dollars

Element Selectors In-Depth

The tapOn: "Login" used earlier is just the simplest text matching. Selectors support many more matching methods.

Text Matching: text

yaml
- tapOn:
    text: "Login"               # Text matching (case-insensitive)

Regex Matching

yaml
- tapOn:
    text: "Login.*"             # Match any text starting with "Login"
    
- tapOn:
    text: "Username|User name"  # Match "Username" or "User name"

ID Matching

yaml
- tapOn:
    id: "btn_login"             # Match by resource ID

Index

When there are multiple elements with the same name on screen, use index to specify which one:

yaml
- tapOn:
    text: "Delete"
    index: 0                    # The first "Delete" button (0-based)
    
- tapOn:
    text: "Delete"
    index: -1                   # The last "Delete" button

State Matching

yaml
- tapOn:
    id: "checkbox"
    enabled: true               # Only match enabled elements
    checked: false              # Only match unchecked elements

Supported 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.

yaml
# 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

yaml
# 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

yaml
- tapOn:
    width: 200
    height: 100
    tolerance: 5                # Allow +/- 5 pixel tolerance

Element Traits

Match by an element's interactive traits; separate multiple traits with spaces:

yaml
- tapOn:
    traits: "clickable enabled"  # Match elements that are clickable and enabled

optional Flag

yaml
- tapOn:
    text: "Skip Ad"
    optional: true              # Skip if not found, no error

label Flag (Log Readability)

yaml
- tapOn:
    id: "btn_submit"
    label: "Tap submit button"        # This description will appear in logs

Complete Example

yaml
- 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.

yaml
- longPressOn: "Settings"

- longPressOn:
    text: "file.txt"
    waitToSettleTimeoutMs: 3000

Double Tap: doubleTapOn

Double tap on an element.

yaml
- 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 seconds

Scroll Until Visible: scrollUntilVisible

Continuously scrolls the page until the target element appears.

yaml
- scrollUntilVisible:
    element:
      text: "About Phone"      # The element to find
    direction: DOWN             # Scroll direction
    timeout: "30000"            # Scroll for up to 30 seconds

All Parameters

yaml
- 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 scroll

Practical Example: Find "About Phone" in Settings

yaml
- 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

yaml
- tapOn:
    text: "Skip"
    when:
      visible: { text: "Ad" }    # Only tap "Skip" when "Ad" is on screen

Execute When Element Is Not Visible

yaml
- tapOn:
    text: "Login"
    when:
      notVisible: { text: "Welcome" }  # Only tap "Login" when "Welcome" is not on screen

Script Condition

yaml
- tapOn:
    text: "Next Page"
    when:
      true: "${counter < 10}"    # Only execute when counter is less than 10

All Fields Supported by when

FieldTypeDescription
visibleSelectorTrue when element is visible
notVisibleSelectorTrue when element is not visible
trueStringTrue when the JS expression evaluates to truthy
labelStringLog 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.

yaml
- tapOn:
    text: "Skip"
    optional: true
    chance: 0.3                  # 30% chance of tapping Skip
yaml
- tapOn:
    text: "View Details"
    optional: true
    chance: 0.1                  # 10% chance of viewing details

chance value ranges from 0.0 to 1.0:

  • 0.0 = never execute
  • 0.5 = 50% probability
  • 1.0 = always execute

Tip: chance is typically used with optional: 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

yaml
- repeat:
    times: "5"
    commands:
      - scroll
      - sleep: 1000

Executes 5 times: scroll, then wait 1 second.

Time-based Loop

yaml
- repeat:
    duration: "60000"            # Loop for 60 seconds (1 minute)
    commands:
      - swipe: { direction: UP }
      - sleep: [3000, 8000]

Runs continuously for 1 minute.

Random Duration Loop

yaml
- 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 duration randomly selects a value when the flow starts, and the same duration is used throughout the entire loop.

Count + Time (Whichever Comes First)

yaml
- repeat:
    times: "50"                  # Up to 50 iterations
    duration: "600000"           # Or up to 10 minutes
    commands:
      - scroll

Conditional Loop (while)

yaml
- repeat:
    while:
      visible: { text: "Loading..." }
    commands:
      - sleep: 1000              # Keep waiting as long as "Loading..." is visible
yaml
- 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.

yaml
- 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:
        - back

With Script Conditions

yaml
- 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

yaml
- runFlow:
    commands:
      - tapOn: "Settings"
      - tapOn: "Account"
      - back

Reference an External File

yaml
- runFlow: "subflow/common-auth.yaml"

# Or object form
- runFlow:
    file: "subflow/common-auth.yaml"

Note: file and commands cannot be used together; choose one.

Conditional Sub-flow

yaml
- runFlow:
    when:
      visible: { text: "Login" }
    commands:
      - tapOn: "Login"
      - inputText: "${PASSWORD}"
      - tapOn: "OK"

Probability Sub-flow

yaml
- runFlow:
    chance: 0.1                  # 10% chance of executing the feedback flow
    commands:
      - tapOn: "Feedback"
      - inputText: "Test feedback"
      - pressKey: Enter
      - back

Sub-flow with Variables

yaml
- 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

  1. A command fails (e.g., element not found)
  2. The engine checks if the current screen matches any exception handler
  3. If it does, it automatically taps the matching element to dismiss the popup
  4. Then retries the original failed command

Short Form

yaml
appId: com.example.app
exceptionHandlers:
  - "Allow"                      # Automatically tap "Allow"
  - "Got it"                     # Automatically tap "Got it"
  - "Maybe later"               # Automatically tap "Maybe later"
---
- launchApp

Object Form (Advanced)

yaml
exceptionHandlers:
  - text: "Skip"
    maxTriggerCount: 3           # Trigger at most 3 times, then stop handling

  - id: "close_button"
    below: { text: "Ad" }       # Supports spatial relationship selectors

Custom 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

yaml
- extendedWaitUntil:
    visible:
      text: "Loading complete"
    timeout: "30000"             # Wait up to 30 seconds

Wait for Element to Disappear

yaml
- extendedWaitUntil:
    notVisible:
      id: "loading_spinner"
    timeout: "15000"             # Wait for "loading" to disappear, up to 15 seconds

Intermediate Practice: Browse Feed for 2 Minutes

yaml
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
- stopApp

Ready to unlock the full power? → Advanced

Powered by VMOS Edge Team