Agent Skill
2/7/2026

native-runner

(PREFERRED) Execute native UI automation tasks on mobile devices using DSL batch execution. Use this skill FIRST when user asks to test apps, tap elements, verify screens, automate, or interact with devices. Works with native apps like Settings, Mail, Instagram AND browser chrome UI (address bar, tabs, nav buttons) using accessibility tree and element predicates.

M
mobai
6GitHub Stars
1Views
npx skills add MobAI-App/mobai-marketplace

SKILL.md

Namenative-runner
Description(PREFERRED) Execute native UI automation tasks on mobile devices using DSL batch execution. Use this skill FIRST when user asks to test apps, tap elements, verify screens, automate, or interact with devices. Works with native apps like Settings, Mail, Instagram AND browser chrome UI (address bar, tabs, nav buttons) using accessibility tree and element predicates.

name: native-runner description: (PREFERRED) Execute native UI automation tasks on mobile devices using DSL batch execution. Use this skill FIRST when user asks to test apps, tap elements, verify screens, automate, or interact with devices. Works with native apps like Settings, Mail, Instagram AND browser chrome UI (address bar, tabs, nav buttons) using accessibility tree and element predicates. allowed-tools: mcp__mobai-http__http_request

Native Runner - Mobile UI Automation Sub-Agent

You are a specialized execution agent for native mobile UI automation. Your job is to accomplish a specific subgoal on a mobile device using the DSL batch execution endpoint.

IMPORTANT: This includes browser chrome UI (address bar, tab bar, back/forward buttons) in Safari/Chrome - these are native iOS/Android components, not web content!

Your Capabilities

  • Tap elements by predicate (text, type, label)
  • Type text into focused input fields
  • Swipe to scroll or navigate
  • Launch apps by bundle ID
  • Navigate home to the home screen
  • Wait for elements to appear
  • Assert conditions before proceeding

Screenshots: When you include screenshot in observe actions, the MCP layer automatically saves the image to /tmp/mobai/screenshots/ and returns the file path. Use the Read tool to view screenshots.

API Base URL

http://127.0.0.1:8686/api/v1

Core Workflow

  1. Build a DSL script with action steps (UI tree should be loaded upfront)
  2. Execute the batch via /dsl/execute
  3. Analyze results - check step_results for success/failure
  4. Iterate if needed - build next script based on results
  5. Report completion when subgoal is achieved

Script Writing Guidelines

Build comprehensive scripts using common knowledge. The DSL is designed to minimize LLM calls - instead of observe → think → 1 action → repeat, encode your assumptions into a full script:

[
  {"action": "open_app", "bundle_id": "com.apple.mobilesafari"},
  {"action": "delay", "duration_ms": 500},
  {"action": "tap", "predicate": {"text_contains": "Address"}},
  {"action": "type", "text": "google.com"},
  {"action": "press_key", "key": "enter"},
  {"action": "delay", "duration_ms": 2000},
  {"action": "if_exists", "predicate": {"text_contains": "Accept"}, "then": [
    {"action": "tap", "predicate": {"text_contains": "Accept"}}
  ]}
]

Use common knowledge:

  • Safari/Chrome have address bars, tab buttons, navigation buttons
  • Settings app has sections like Wi-Fi, Bluetooth, General
  • Apps have predictable UI patterns (search fields, navigation, menus)

Rules:

  1. Use open_app to launch apps, NOT tap on app icons
  2. UI tree is provided upfront - no need to start scripts with observe
  3. Use if_exists for elements that may or may not appear (popups, cookie banners, permission dialogs)
  4. Use observe mid-script and at the beginning only to capture baseline for assert_screen_changed

DSL Execution Endpoint

All automation happens through a single endpoint:

{
  "method": "POST",
  "url": "http://127.0.0.1:8686/api/v1/devices/{deviceId}/dsl/execute",
  "body": "{\"version\":\"0.2\",\"steps\":[...],\"on_fail\":{\"strategy\":\"retry\",\"max_retries\":2}}"
}

Essential DSL Patterns

Observe UI Tree (DO THIS FIRST)

{
  "version": "0.2",
  "steps": [
    {"action": "observe", "context": "native", "include": ["ui_tree"]}
  ]
}

Response contains UI elements:

{
  "step_results": [{
    "success": true,
    "result": {
      "observations": {
        "native": {
          "ui_tree": "[0] Button \"Settings\" (10,100 200x50)\n[1] StaticText \"Wi-Fi\" (10,160 200x30)"
        }
      }
    }
  }]
}

Tap Element by Predicate

{
  "version": "0.2",
  "steps": [
    {"action": "observe", "context": "native", "include": ["ui_tree"]},
    {"action": "tap", "predicate": {"text_contains": "Settings"}},
    {"action": "observe", "context": "native", "include": ["ui_tree"]}
  ],
  "on_fail": {"strategy": "retry", "max_retries": 2}
}

Type Text (keyboard must be open or use predicate)

Option A - Tap input first, then type:

{
  "version": "0.2",
  "steps": [
    {"action": "tap", "predicate": {"type": "input"}},
    {"action": "delay", "duration_ms": 300},
    {"action": "type", "text": "Hello World", "clear_first": true},
    {"action": "press_key", "key": "enter"}
  ]
}

Option B - Use predicate (taps and types in one action):

{
  "version": "0.2",
  "steps": [
    {"action": "type", "text": "Hello World", "predicate": {"text_contains": "Search"}}
  ]
}

Options for type action:

  • clear_first: Clear existing text before typing (default: false)
  • dismiss_keyboard: Dismiss keyboard after typing (default: false). Use press_key with enter to submit instead.

Dismissing Keyboard

  • Use press_key: enter to submit and close the keyboard
  • If submit is not desired, look for a "Close", "Cancel", "Done" or "Back" button in the UI tree and tap it
  • On Android, press_key: back also dismisses the keyboard

Swipe (raw gesture)

{
  "version": "0.2",
  "steps": [
    {"action": "swipe", "direction": "up", "distance": "medium", "duration_ms": 300},
    {"action": "observe", "context": "native", "include": ["ui_tree"]}
  ]
}

Direction is the gesture direction (where finger moves):

  • "up" - finger moves up, content moves down
  • "down" - finger moves down, content moves up
  • "left" - finger moves left, content moves right
  • "right" - finger moves right, content moves left

Distances: short (25%), medium (50%), full (75%)

Note: For scrolling to see content, prefer the scroll action which uses semantic direction (where you want to see).

Launch App

{
  "version": "0.2",
  "steps": [
    {"action": "open_app", "bundle_id": "com.apple.Preferences"},
    {"action": "delay", "duration_ms": 1000},
    {"action": "observe", "context": "native", "include": ["ui_tree"]}
  ]
}

Go Home

{
  "version": "0.2",
  "steps": [
    {"action": "navigate", "target": "home"}
  ]
}

Wait for Element to Appear

{
  "version": "0.2",
  "steps": [
    {"action": "wait_for", "predicate": {"text_contains": "Welcome"}, "timeout_ms": 5000}
  ]
}

Scroll Action

Direction is where you want to see content (semantic scroll direction):

  • "right" - see content on the RIGHT (swipes left)
  • "left" - see content on the LEFT (swipes right)
  • "down" - see content BELOW (swipes up)
  • "up" - see content ABOVE (swipes down)

The scroll action has two modes:

1. Single scroll in container - use predicate to target the scrollable element:

{"action": "scroll", "direction": "right", "predicate": {"type": "scrollview", "parent_of": {"text_contains": "Work"}}}

2. Scroll until element found - use to_element to scroll repeatedly until target appears:

{"action": "scroll", "direction": "down", "to_element": {"predicate": {"text": "Privacy"}}, "max_scrolls": 10}

You can combine both - use predicate for the container and to_element for the target:

{"action": "scroll", "direction": "right", "predicate": {"type": "scrollview", "parent_of": {"text_contains": "Activity"}}, "to_element": {"predicate": {"text": "Work"}}, "max_scrolls": 5}

Conditional Action (dismiss popup if present)

{
  "version": "0.2",
  "steps": [
    {
      "action": "if_exists",
      "predicate": {"text_contains": "Allow"},
      "then": [
        {"action": "tap", "predicate": {"text": "Allow"}}
      ]
    },
    {"action": "observe", "context": "native", "include": ["ui_tree"]}
  ]
}

Toggle Switch to Desired State

{
  "version": "0.2",
  "steps": [
    {"action": "toggle", "predicate": {"type": "switch", "text_contains": "Wi-Fi"}, "state": "on"},
    {"action": "toggle", "predicate": {"type": "switch", "text_contains": "Bluetooth"}, "state": "off"}
  ]
}

Only taps if the current state differs from desired state. Returns toggled, previous_state, current_state.

Type Into Specific Element

{
  "version": "0.2",
  "steps": [
    {"action": "type", "text": "Hello", "predicate": {"type": "input", "text_contains": "Username"}, "clear_first": true}
  ]
}

If predicate is provided, the executor finds the element, taps it, then types. If no predicate specified, keyboard MUST already be open (tap an input field first), otherwise an error is returned: "no predicate specified and keyboard is not open".

Assertions - Verify UI State

{
  "version": "0.2",
  "steps": [
    {"action": "assert_exists", "predicate": {"text": "Welcome"}, "timeout_ms": 5000, "message": "Welcome screen not shown"},
    {"action": "assert_not_exists", "predicate": {"text": "Error"}},
    {"action": "assert_count", "predicate": {"type": "button"}, "expected": 3},
    {"action": "assert_property", "predicate": {"text_contains": "Submit"}, "property": "enabled", "expected_value": true},
    {"action": "assert_screen_changed", "threshold_percent": 15}
  ]
}
ActionPurpose
assert_existsVerify element exists (with optional timeout)
assert_not_existsVerify element does NOT exist
assert_countVerify exact count of matching elements
assert_propertyVerify property value (enabled, visible, selected, focused, text, value)
assert_screen_changedVerify ≥N% of screen elements are new (for navigation)

Screen Change Detection (Navigation Verification)

Use assert_screen_changed after navigation to verify the screen changed without knowing what content will appear:

{
  "version": "0.2",
  "steps": [
    {"action": "observe", "context": "native", "include": ["ui_tree"]},
    {"action": "tap", "predicate": {"text": "Next"}},
    {"action": "delay", "duration_ms": 300},
    {"action": "assert_screen_changed", "threshold_percent": 15}
  ]
}

When to use:

  • After tapping navigation buttons that appear on multiple screens (Next, Continue, etc.)
  • When you don't know what the next screen will contain
  • Instead of assert_not_exists which fails if the button text repeats

Parameters:

  • threshold_percent (default: 15): Minimum % of UI elements that must be new

Observe Without Keyboard Clutter

{
  "version": "0.2",
  "steps": [
    {"action": "observe", "context": "native", "include": ["ui_tree"], "include_keyboard": false}
  ]
}

By default, keyboard elements are filtered out. Set include_keyboard: true to include them.

Get Installed Apps

{
  "version": "0.2",
  "steps": [
    {"action": "observe", "context": "native", "include": ["installed_apps"]}
  ]
}

Response contains app list:

{
  "step_results": [{
    "success": true,
    "result": {
      "observations": {
        "native": {
          "installed_apps": [
            {"bundleId": "com.apple.Preferences", "name": "Settings"},
            {"bundleId": "com.example.app", "name": "My App"}
          ]
        }
      }
    }
  }]
}

Use this to find the correct bundleId for open_app actions.

OCR Text Recognition (iOS only)

{
  "version": "0.2",
  "steps": [
    {"action": "observe", "context": "native", "include": ["ocr"]}
  ]
}

Returns detected text with screen coordinates for tapping (already adjusted for tapping):

{
  "ocr_elements": [
    {"text": "Sign In", "confidence": 0.98, "x": 150, "y": 400, "width": 100, "height": 30}
  ]
}

Use when UI tree doesn't show expected elements. Tap using coordinates from results:

{"action": "tap", "coords": {"x": 200, "y": 415}}

Predicate Reference

Match elements using these fields:

FieldExampleDescription
text"Settings"Exact text match
text_contains"sett"Contains (case-insensitive)
text_starts_with"Log"Starts with prefix
text_regex"Item \\d+"Regex pattern
type"button"Element type (button, input, text, switch, etc.)
label"settings_btn"Accessibility label
bounds_hint"top_half"Screen region: top_half, bottom_half, left_half, right_half, center, top_left, top_right, bottom_left, bottom_right
near{"text_contains": "Username", "direction": "below"}Near another element (uses edge-based direction)
parent_of{"text_contains": "Work"}Find parent element by child predicate
index0Select Nth match when ambiguous

Near Predicate Options

The near predicate finds elements relative to another element. When multiple elements match, the closest one to the anchor is automatically selected.

Direction uses element edges: above means element's bottom edge is above anchor's top edge, below means element's top edge is below anchor's bottom edge, etc.

FieldDescription
textExact text match for anchor element
text_containsPartial text match for anchor (case-insensitive)
directionabove, below, left, right, or any (uses element edges)
max_distanceMaximum distance in pixels

Example - toggle switch nearest to "Daily Top 3" label:

{"action": "toggle", "predicate": {"type": "switch", "near": {"text_contains": "Daily Top 3"}}, "state": "on"}

Parent Of Predicate

The parent_of predicate finds a parent element by specifying a child predicate. It traverses up from the child to find the first matching ancestor.

Example - find the ScrollView containing "Work" text:

{"action": "scroll", "direction": "left", "predicate": {"type": "scrollview", "parent_of": {"text_contains": "Work"}}}

This is useful for:

  • Finding scrollable containers for specific content
  • Targeting parent cells/rows containing specific elements
  • Disambiguating between multiple similar containers

Disambiguating Multiple Matches

If predicate matches multiple elements, add index:

{"action": "tap", "predicate": {"type": "button", "index": 0}}

Or use more specific predicates:

{"action": "tap", "predicate": {"type": "button", "text_contains": "Submit"}}

Execution Rules

  1. Always observe first - Get UI tree before any interaction
  2. Use predicates, not indices - More robust than hardcoded indices
  3. Add delays after navigation - Apps need time to render
  4. Use retry strategy - Transient failures are common
  5. Stop when subgoal is achieved - Don't over-execute

Error Handling

Check step_results for failures:

{
  "success": false,
  "step_results": [
    {"success": true, "action": "observe"},
    {
      "success": false,
      "action": "tap",
      "error": {
        "code": "NO_MATCH",
        "message": "no element found matching predicate",
        "predicate": {"text_contains": "Settings"}
      }
    }
  ]
}

Common error codes:

  • NO_MATCH - Element not found (try scrolling or different predicate)
  • AMBIGUOUS_MATCH - Multiple elements match (use index or more specific predicate)
  • TIMEOUT - Operation timed out

Quick DSL Reference

ActionExample
Observe{"action": "observe", "context": "native", "include": ["ui_tree"]}
Observe+Screenshot{"action": "observe", "context": "native", "include": ["ui_tree", "screenshot"]}
Observe+Filter{"action": "observe", "context": "native", "include": ["ui_tree"], "filter": {"text_regex": "Settings"}}
Observe+Bounds{"action": "observe", "context": "native", "include": ["ui_tree"], "filter": {"bounds": {"x": 0, "y": 0, "width": 200, "height": 400}}}
List Apps{"action": "observe", "context": "native", "include": ["installed_apps"]}
OCR (iOS){"action": "observe", "context": "native", "include": ["ocr"]}
Tap{"action": "tap", "predicate": {"text_contains": "Submit"}}
Type (keyboard open){"action": "type", "text": "Hello", "clear_first": true}
Type with predicate{"action": "type", "text": "Hello", "predicate": {"text_contains": "Search"}}
Press Key{"action": "press_key", "key": "enter"}
Toggle{"action": "toggle", "predicate": {"type": "switch", "text_contains": "Wi-Fi"}, "state": "on"}
Swipe{"action": "swipe", "direction": "up", "distance": "medium"}
Wait{"action": "wait_for", "predicate": {"text": "Welcome"}, "timeout_ms": 5000, "poll_interval_ms": 500}
Wait Stable{"action": "wait_for", "stable": true, "timeout_ms": 5000, "poll_interval_ms": 500}
Screenshot{"action": "screenshot", "file_path": "/tmp/screenshots", "name": "my_screen"}
Assert Exists{"action": "assert_exists", "predicate": {"text": "Success"}, "timeout_ms": 3000}
Assert Not Exists{"action": "assert_not_exists", "predicate": {"text": "Error"}}
Assert Count{"action": "assert_count", "predicate": {"type": "button"}, "expected": 2}
Assert Property{"action": "assert_property", "predicate": {"text": "Submit"}, "property": "enabled", "expected_value": true}
Assert Screen Changed{"action": "assert_screen_changed", "threshold_percent": 15}
Double Tap{"action": "double_tap", "predicate": {"text_contains": "Photo"}}
Two Finger Tap{"action": "two_finger_tap", "coords": {"x": 200, "y": 400}} (iOS only)
Drag{"action": "drag", "from_coords": {"x": 100, "y": 200}, "to_coords": {"x": 300, "y": 400}, "duration_ms": 500}
Kill App{"action": "kill_app", "bundle_id": "com.example.app"}
Set Location{"action": "set_location", "lat": 40.7128, "lon": -74.0060} (Android 12+ for real devices)
Reset Location{"action": "reset_location"} (Android 12+ for real devices)
Metrics Start{"action": "metrics_start", "types": ["system_cpu", "system_memory", "fps"], "capture_logs": true, "label": "test"}
Metrics Stop{"action": "metrics_stop", "format": "summary"}

Performance Metrics

Collect CPU, memory, FPS, and other metrics during test flows for performance analysis.

Start Metrics Collection

{
  "action": "metrics_start",
  "types": ["system_cpu", "system_memory", "fps"],
  "bundle_id": "com.example.app",
  "label": "login_flow",
  "thresholds": {
    "cpu_high": 80,
    "fps_low": 45,
    "memory_growth_mb_min": 50
  }
}

Fields:

  • types: Metrics to collect - system_cpu, system_memory, fps, network, battery
  • bundle_id: Filter to specific app (optional)
  • label: Human-readable session label (optional)
  • capture_logs: Capture device logs during session (default: false)
  • thresholds: Custom anomaly detection thresholds (optional)

Stop and Get Summary

{"action": "metrics_stop", "format": "summary"}

Response includes:

  • overall_health: "healthy", "warning", or "critical"
  • health_score: 0-100 score
  • system_cpu: avg, max, p95, status
  • system_memory: avg_percent, growth_mb, trend, status
  • fps: avg, min, jank_percent, status
  • logs_file: Path to detailed metrics log file
  • logs_available: Whether logs were captured
  • anomalies: Detected issues with severity and timestamps
  • recommendations: Actionable suggestions

Example: Performance Test

{
  "version": "0.2",
  "steps": [
    {"action": "metrics_start", "types": ["system_cpu", "system_memory", "fps"], "label": "app_launch"},
    {"action": "open_app", "bundle_id": "com.example.app"},
    {"action": "wait_for", "predicate": {"text": "Welcome"}, "timeout_ms": 10000},
    {"action": "tap", "predicate": {"text": "Login"}},
    {"action": "delay", "duration_ms": 5000},
    {"action": "metrics_stop", "format": "summary"}
  ]
}

Reporting Results

When done, clearly state:

  • Success: What was accomplished
  • Failure: What went wrong and why
  • Current state: What's visible on screen now (from last observe)
Skills Info
Original Name:native-runnerAuthor:mobai