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.
SKILL.md
| 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. |
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
- Build a DSL script with action steps (UI tree should be loaded upfront)
- Execute the batch via
/dsl/execute - Analyze results - check step_results for success/failure
- Iterate if needed - build next script based on results
- 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:
- Use
open_appto launch apps, NOT tap on app icons - UI tree is provided upfront - no need to start scripts with observe
- Use
if_existsfor elements that may or may not appear (popups, cookie banners, permission dialogs) - 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). Usepress_keywithenterto submit instead.
Dismissing Keyboard
- Use
press_key: enterto 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: backalso 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}
]
}
| Action | Purpose |
|---|---|
assert_exists | Verify element exists (with optional timeout) |
assert_not_exists | Verify element does NOT exist |
assert_count | Verify exact count of matching elements |
assert_property | Verify property value (enabled, visible, selected, focused, text, value) |
assert_screen_changed | Verify âĽ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_existswhich 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:
| Field | Example | Description |
|---|---|---|
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 |
index | 0 | Select 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.
| Field | Description |
|---|---|
text | Exact text match for anchor element |
text_contains | Partial text match for anchor (case-insensitive) |
direction | above, below, left, right, or any (uses element edges) |
max_distance | Maximum 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
- Always observe first - Get UI tree before any interaction
- Use predicates, not indices - More robust than hardcoded indices
- Add delays after navigation - Apps need time to render
- Use retry strategy - Transient failures are common
- 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 (useindexor more specific predicate)TIMEOUT- Operation timed out
Quick DSL Reference
| Action | Example |
|---|---|
| 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,batterybundle_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 scoresystem_cpu: avg, max, p95, statussystem_memory: avg_percent, growth_mb, trend, statusfps: avg, min, jank_percent, statuslogs_file: Path to detailed metrics log filelogs_available: Whether logs were capturedanomalies: Detected issues with severity and timestampsrecommendations: 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)