# Agent
Source: https://docs.mobilerun.ai/agent
Configure and run the Mobilerun agent to automate tasks on your devices.
The Mobilerun agent is an AI-powered automation system that executes tasks on Android devices. Configure the agent with a prompt and settings, then run it on any connected device.
## Overview
The agent:
* Interprets natural language instructions
* Interacts with device UI elements
* Executes multi-step workflows autonomously
* Returns results and structured output
## Configuration
### Task Prompt
The prompt describes what the agent should do. Write clear, specific instructions for best results.
```
Open the Settings app, navigate to Display settings, and enable dark mode
```
### Model Selection
Choose the LLM model that powers the agent. Different models offer different tradeoffs:
| Model Type | Characteristics |
| ------------------- | -------------------------------------------------------------- |
| **Faster models** | Lower latency, lower cost per step, good for simple tasks |
| **Advanced models** | Better reasoning, higher accuracy, ideal for complex workflows |
Pass one of the following identifiers as the `llmModel` parameter in the REST API or the SDK, or
select it in the Playground. This table is the canonical catalog of models available for cloud
agent tasks.
| Model | `llmModel` identifier |
| --------------------------------- | --------------------------------- |
| Mobilerun Mobile Agent (Fast) | `mobilerun/mobile-agent-fast` |
| Mobilerun Mobile Agent (Thinking) | `mobilerun/mobile-agent-thinking` |
| Anthropic Claude Opus 4.6 | `anthropic/claude-opus-4.6` |
| Anthropic Claude Opus 4.8 | `anthropic/claude-opus-4.8` |
| Anthropic Claude Sonnet 4.6 | `anthropic/claude-sonnet-4.6` |
| Google Gemini 3.1 Flash Lite | `google/gemini-3.1-flash-lite` |
| Google Gemini 3.1 Pro (Preview) | `google/gemini-3.1-pro-preview` |
| Google Gemini 3.5 Flash | `google/gemini-3.5-flash` |
| Moonshot Kimi K2.6 | `moonshotai/kimi-k2.6` |
| OpenAI GPT-5.4 | `openai/gpt-5.4` |
| OpenAI GPT-5.4 Mini | `openai/gpt-5.4-mini` |
The model lineup changes over time. To see the live list your account can use, call
`GET /v1/models`, or use `client.models.list()` in the SDK. That endpoint is the source of truth
whenever an identifier here returns a "model unavailable" error.
These provider prefixed identifiers are specific to the **cloud** API. The open source
[Framework](/framework/quickstart) selects models in a different way, by provider class and model
ID such as `GoogleGenAI` with `gemini-3.1-flash-lite-preview`. See the
[SDK configuration](/framework/sdk/configuration#llm-configuration) for details.
### Execution Limits
| Setting | Description |
| --------------------- | ----------------------------------------------------------------------- |
| **Max Steps** | Maximum number of actions the agent can take (1-10,000) |
| **Execution Timeout** | Time limit in seconds before the task is stopped |
| **Temperature** | Controls response randomness (0-2). Lower values are more deterministic |
### Features
Enables logical thinking and step-by-step analysis. The agent will reason through complex tasks before acting.
Allows the agent to process and understand visual content on screen. Required for most UI automation tasks.
Enable human-like device interactions for automations where detection matters.
Cross-task memory personalization. Use `memoryNamespace` to isolate memory between different workflows.
### Platform Settings
| Setting | Description |
| --------------- | -------------------------------------------------------------- |
| **Device** | Choose a [device](/device-types) from your account |
| **Apps** | Select which [apps](/apps) should be available on the device |
| **Credentials** | Attach [credentials](/credentials) for authenticated workflows |
| **VPN Country** | Route traffic through a specific country |
### Structured Output
Define a JSON schema to receive structured responses from the agent. This is useful for:
* Data extraction tasks
* Validation workflows
* Integration with downstream systems
When a schema is defined, the agent formats its output to match the specified structure.
## Running the Agent
### Via Playground
Use the [Playground](/playground) for interactive testing:
1. Configure your task and settings
2. Select a device
3. Click **Run**
4. Monitor execution in the task stream
### Via API
Run tasks programmatically using the [API](/api-keys):
```json theme={null}
{
"task": "Open Settings and enable dark mode",
"llmModel": "mobilerun/mobile-agent-fast",
"maxSteps": 50,
"executionTimeout": 300,
"temperature": 0.5,
"reasoning": true,
"vision": true,
"stealth": true,
"memoryNamespace": "my-workflow",
"continueOnFailure": false,
"apps": ["com.android.settings"],
"credentials": [],
"outputSchema": {
"type": "object",
"properties": {
"success": { "type": "boolean" },
"darkModeEnabled": { "type": "boolean" }
}
}
}
```
| Parameter | Type | Default | Description |
| ------------------- | --------- | ----------- | -------------------------------------------------------------------------------------------------------------------------- |
| `task` | string | required | The prompt describing what the agent should do |
| `llmModel` | string | required | LLM model identifier such as `"mobilerun/mobile-agent-fast"`. See the [model catalog](#model-selection) for the full list. |
| `maxSteps` | integer | `100` | Maximum number of actions the agent can take |
| `executionTimeout` | integer | `1000` | Time limit in seconds before the task is stopped |
| `temperature` | number | `0.5` | Controls response randomness (0-2) |
| `reasoning` | boolean | `false` | Enable step-by-step reasoning mode |
| `vision` | boolean | `false` | Enable visual understanding of screen content |
| `stealth` | boolean | `false` | Enable stealth mode for human-like device interactions |
| `memoryNamespace` | string | `"default"` | Memory namespace for cross-task personalization |
| `continueOnFailure` | boolean | `false` | Whether the agent should keep executing if an individual action fails, instead of stopping the task |
| `apps` | string\[] | `[]` | Package names of apps to make available on the device |
| `credentials` | object\[] | `[]` | Credentials to attach for authenticated workflows |
| `outputSchema` | object | `null` | JSON Schema defining the structure of the agent's output |
| `vpnCountry` | string | `null` | Route device traffic through a specific country (`"US"`, `"DE"`, `"JP"`, etc.) |
### Via MCP
Use the [MCP integration](/mcp-server) to run tasks from AI assistants and IDEs.
## Execution Flow
1. **Initialization** - Agent connects to the selected device
2. **Observation** - Agent captures the current screen state
3. **Planning** - Agent determines the next action based on the prompt
4. **Action** - Agent executes the action (tap, type, scroll, etc.)
5. **Repeat** - Steps 2-4 repeat until the task is complete or limits are reached
6. **Output** - Agent returns results and any structured output
## Best Practices
| Practice | Description |
| ------------------------- | --------------------------------------------------------------------- |
| **Be specific** | Clear prompts lead to more reliable execution |
| **Set reasonable limits** | Use appropriate max steps and timeouts for your task complexity |
| **Use structured output** | Define schemas when you need to process results programmatically |
| **Enable reasoning** | Turn on for complex multi-step tasks that require planning |
| **Attach credentials** | Pre-configure authentication rather than including secrets in prompts |
# API Keys
Source: https://docs.mobilerun.ai/api-keys
Create and manage API keys for programmatic access to the Mobilerun API.
The API Keys tab allows you to generate and manage authentication keys for accessing the Mobilerun API. Use API keys to integrate Mobilerun into your applications, CI/CD pipelines, and automation workflows.
## Overview
API keys provide:
* Authentication for API requests
* Programmatic access to all Mobilerun features
* Integration with external tools and services
## Creating an API Key
To generate a new API key:
1. Click **Create API Key**
2. Enter a descriptive name for the key (e.g., "CI Pipeline", "Production Server")
3. Click **Create**
4. Copy the generated key immediately
API keys are only displayed once at creation. Store your key securely as you will not be able to view it again.
Mobilerun cloud keys always start with the prefix **`dr_sk_`**. Pass the full key, including the prefix, as a bearer token in the form `Authorization: Bearer dr_sk_...`. This is a different credential from your LLM provider keys such as `GOOGLE_API_KEY` or `OPENAI_API_KEY`, which are only used by the local [Framework](/framework/quickstart). See [the two kinds of API keys](/quickstart#two-kinds-of-api-keys) for guidance on when to use each one.
## Managing API Keys
Your API key list displays:
| Field | Description |
| ------------- | -------------------------------------- |
| **Name** | The descriptive name you assigned |
| **Created** | When the key was generated |
| **Last Used** | Most recent API request using this key |
### Revoke Keys
Delete API keys that are no longer needed or may have been compromised. Revoked keys immediately stop working for all API requests.
Revoking a key cannot be undone. You will need to create a new key and update any integrations that used the old key.
## Using API Keys
Include your API key in the request headers when calling the Mobilerun API:
```bash theme={null}
curl -X POST https://api.mobilerun.ai/v1/tasks \
-H "Authorization: Bearer dr_sk_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"task": "Open Settings app", "llmModel": "mobilerun/mobile-agent-fast"}'
```
If you are new to the API, the [Cloud Quickstart](/cloud/quickstart) walks through the full flow in TypeScript, Python, and cURL. It covers finding a device, running a task, and reading the result.
### Authentication Header
All API requests require the `Authorization` header:
```
Authorization: Bearer dr_sk_YOUR_API_KEY
```
## API Documentation
The complete API reference is available via OpenAPI specification. Access the full documentation to explore all available endpoints:
* **Tasks** - Create, monitor, and manage agent tasks
* **Devices** - List and manage your devices
* **Apps** - Upload and install applications
* **Credentials** - Manage stored credentials
* **Hooks** - Configure webhooks for task events
View the complete API documentation with request/response schemas and examples.
## Best Practices
| Practice | Description |
| ------------------------- | ------------------------------------------------------------------------- |
| **Use descriptive names** | Name keys by their purpose (e.g., "GitHub Actions", "Monitoring Service") |
| **Separate environments** | Create distinct keys for development, staging, and production |
| **Rotate periodically** | Generate new keys and revoke old ones on a regular schedule |
| **Limit exposure** | Store keys in environment variables or secret managers, never in code |
| **Monitor usage** | Check the "Last Used" timestamp to identify inactive keys |
## Environment Variables
Store your API key as an environment variable rather than hardcoding it:
```bash theme={null}
export MOBILERUN_API_KEY="your_api_key_here"
```
Then reference it in your code or scripts:
```bash theme={null}
curl -X POST https://api.mobilerun.ai/v1/tasks \
-H "Authorization: Bearer $MOBILERUN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"task": "Open Settings app", "llmModel": "mobilerun/mobile-agent-fast"}'
```
# Get a catalog entry
Source: https://docs.mobilerun.ai/api-reference/action-catalog/get-a-catalog-entry
/api-reference/workflows.yaml get /action-catalog/{catalogEntryId}
Fetch a single action catalog entry by its ID, including its service, method, and parameter schema. Returns 404 if no entry matches.
# List action catalog entries
Source: https://docs.mobilerun.ai/api-reference/action-catalog/list-action-catalog-entries
/api-reference/workflows.yaml get /action-catalog
Return a paginated list of catalog entries — the service/method templates that actions are created from, each carrying its parameter schema. Supports filtering by `service`.
# Create an action
Source: https://docs.mobilerun.ai/api-reference/actions/create-an-action
/api-reference/workflows.yaml post /actions
Create a reusable action from a catalog entry (`catalogEntryId`), with an optional `params` object supplying the values for that entry's service method. Returns 400 if the params are invalid for the chosen catalog entry.
# Delete an action
Source: https://docs.mobilerun.ai/api-reference/actions/delete-an-action
/api-reference/workflows.yaml delete /actions/{actionId}
Delete an action by its ID. Returns 404 if no action matches.
# Get an action
Source: https://docs.mobilerun.ai/api-reference/actions/get-an-action
/api-reference/workflows.yaml get /actions/{actionId}
Fetch a single action by its ID, including its configured service, method, and params. Returns 404 if no action matches.
# List actions
Source: https://docs.mobilerun.ai/api-reference/actions/list-actions
/api-reference/workflows.yaml get /actions
Return a paginated list of actions. Supports filtering by `service`, free-text `search`, and ordering by name, createdAt, or updatedAt.
# List allowed methods for a service
Source: https://docs.mobilerun.ai/api-reference/actions/list-allowed-methods-for-a-service
/api-reference/workflows.yaml get /actions/services/{service}/methods
Return the methods allowed for the given service, each with its parameter definitions (name, type, whether required, description, and optional default/example). Returns 404 if the service is unknown.
# List available services
Source: https://docs.mobilerun.ai/api-reference/actions/list-available-services
/api-reference/workflows.yaml get /actions/services
Return the names of the services that actions can be built against. Use these values to look up each service's allowed methods.
# Update an action
Source: https://docs.mobilerun.ai/api-reference/actions/update-an-action
/api-reference/workflows.yaml patch /actions/{actionId}
Partially update an action's name, description, or params; all fields are optional. Returns 404 if the action does not exist.
# Add a credential field
Source: https://docs.mobilerun.ai/api-reference/app-credentials/add-a-credential-field
/api-reference/phones.yaml post /credentials/packages/{packageName}/credentials/{credentialName}/fields
Adds a single field to an existing credential. The body specifies a `fieldType` (one of the supported field types) and its value. Returns a conflict if a field of that type already exists on the credential.
# Create a credential
Source: https://docs.mobilerun.ai/api-reference/app-credentials/create-a-credential
/api-reference/phones.yaml post /credentials/packages/{packageName}
Creates a credential under the given package with a `credentialName` and at least one field. Each field has a `fieldType` (email, username, password, api_token, phone_number, two_factor_secret, or backup_codes) and a value.
# Delete a credential and all its fields
Source: https://docs.mobilerun.ai/api-reference/app-credentials/delete-a-credential-and-all-its-fields
/api-reference/phones.yaml delete /credentials/packages/{packageName}/credentials/{credentialName}
Permanently deletes the credential identified by `packageName` and `credentialName`, removing all of its fields. Returns the deleted credential.
# Delete a field from a credential
Source: https://docs.mobilerun.ai/api-reference/app-credentials/delete-a-field-from-a-credential
/api-reference/phones.yaml delete /credentials/packages/{packageName}/credentials/{credentialName}/fields/{fieldType}
Removes a single field of the given `fieldType` from the specified credential while leaving the credential itself intact. Returns the updated credential.
# Get a credential
Source: https://docs.mobilerun.ai/api-reference/app-credentials/get-a-credential
/api-reference/phones.yaml get /credentials/packages/{packageName}/credentials/{credentialName}
Fetches a single credential by `packageName` and `credentialName`, including all of its stored fields. Returns not found if no matching credential exists.
# Initialize a new package/app
Source: https://docs.mobilerun.ai/api-reference/app-credentials/initialize-a-new-packageapp
/api-reference/phones.yaml post /credentials/packages
Creates a new package (identified by `packageName`) under which credentials can be grouped. Returns a conflict if a package with the same name already exists for the user.
# List credentials
Source: https://docs.mobilerun.ai/api-reference/app-credentials/list-credentials
/api-reference/phones.yaml get /credentials
Returns a paginated list of all credentials belonging to the authenticated user across every package. Accepts standard pagination query parameters and responds with the credential items plus pagination metadata.
# List credentials for a specific package
Source: https://docs.mobilerun.ai/api-reference/app-credentials/list-credentials-for-a-specific-package
/api-reference/phones.yaml get /credentials/packages/{packageName}
Returns all credentials stored under the given `packageName`. Each credential includes its name, secret path, and the list of fields it holds.
# List packages
Source: https://docs.mobilerun.ai/api-reference/app-credentials/list-packages
/api-reference/phones.yaml get /credentials/packages
Returns the names of all packages (apps) the authenticated owner has credentials grouped under. Use this to discover which `packageName` values are valid for the per-package credential routes.
# Update the value of a credential field
Source: https://docs.mobilerun.ai/api-reference/app-credentials/update-the-value-of-a-credential-field
/api-reference/phones.yaml patch /credentials/packages/{packageName}/credentials/{credentialName}/fields/{fieldType}
Updates the value of an existing field on a credential, identified by `packageName`, `credentialName`, and `fieldType` in the path. The body carries the new value and returns the updated credential.
# Get an app event
Source: https://docs.mobilerun.ai/api-reference/app-events/get-an-app-event
/api-reference/workflows.yaml get /app-events/{id}
Fetch a single structured app event by its ID, including its typed payload, source, and originating device. Returns 404 if no event matches.
# Get one app-event catalog entry by type/name
Source: https://docs.mobilerun.ai/api-reference/app-events/get-one-app-event-catalog-entry-by-typename
/api-reference/workflows.yaml get /app-events/catalog/{appEventType}
Fetch a single selectable app event by its appEventType (e.g. app.whatsapp.message_received).
# List structured app events
Source: https://docs.mobilerun.ai/api-reference/app-events/list-structured-app-events
/api-reference/workflows.yaml get /app-events
Structured, app-scoped events (e.g. app.whatsapp.message_received) derived from raw device notifications. Typed columns — not raw payloads (those stay in the event log).
# List the app-event catalog
Source: https://docs.mobilerun.ai/api-reference/app-events/list-the-app-event-catalog
/api-reference/workflows.yaml get /app-events/catalog
Selectable app-based trigger events (e.g. app.whatsapp.message_received) with their predefined payload — served from the JSON definition registry (always in sync, no DB).
# Confirm successful app upload
Source: https://docs.mobilerun.ai/api-reference/apps-cloud-storage/confirm-successful-app-upload
/api-reference/phones.yaml post /apps/{id}/confirm-upload
Verifies the APK file exists in R2 and sets the app status to available.
# Create a signed R2 upload URL for an app
Source: https://docs.mobilerun.ai/api-reference/apps-cloud-storage/create-a-signed-r2-upload-url-for-an-app
/api-reference/phones.yaml post /apps/create-signed-upload-url
Creates or updates an app and returns pre-signed Cloudflare R2 upload URLs for each file
# Delete uploaded app
Source: https://docs.mobilerun.ai/api-reference/apps-cloud-storage/delete-uploaded-app
/api-reference/phones.yaml delete /apps/{id}
Deletes an uploaded app by ID. Removes files from R2 storage and the database entry.
# Get app by ID
Source: https://docs.mobilerun.ai/api-reference/apps-cloud-storage/get-app-by-id
/api-reference/phones.yaml get /apps/{id}
Retrieves an app by its ID
# Get the user’s storage usage
Source: https://docs.mobilerun.ai/api-reference/apps-cloud-storage/get-the-user’s-storage-usage
/api-reference/phones.yaml get /apps/storage-usage
Returns the user’s total storage quota, bytes used, and remaining bytes — the reliable maximum size for the next upload.
# List apps
Source: https://docs.mobilerun.ai/api-reference/apps-cloud-storage/list-apps
/api-reference/phones.yaml get /apps
Retrieves a paginated list of apps with filtering and search capabilities
# List versions for an app
Source: https://docs.mobilerun.ai/api-reference/apps-cloud-storage/list-versions-for-an-app
/api-reference/phones.yaml get /apps/{id}/versions
Retrieves all versions of an app visible to the user (own uploads + system versions)
# Mark app upload as failed
Source: https://docs.mobilerun.ai/api-reference/apps-cloud-storage/mark-app-upload-as-failed
/api-reference/phones.yaml post /apps/{id}/mark-failed
Sets the app status to failed.
# Delete app
Source: https://docs.mobilerun.ai/api-reference/apps/delete-app
/api-reference/phones/tools.yaml delete /devices/{deviceId}/apps/{packageName}
Uninstalls the app identified by the path package name from the device. Protected packages cannot be deleted.
# Grant app permission
Source: https://docs.mobilerun.ai/api-reference/apps/grant-app-permission
/api-reference/phones/tools.yaml put /devices/{deviceId}/apps/{packageName}/permissions/{permission}
Grants an Android runtime permission to the package named in the path. The permission is given by its short name (e.g. POST_NOTIFICATIONS).
# Install app
Source: https://docs.mobilerun.ai/api-reference/apps/install-app
/api-reference/phones/tools.yaml post /devices/{deviceId}/apps
Requests an app install on the device. The request body must supply exactly one of an Android packageName or an iOS bundleId; protected packages are rejected. background (default false) selects the response contract: false installs inline and returns the outcome directly (200 on success, an error status on failure); true accepts the request and runs the download + install in the background, returning 202 immediately — poll list-app-installs for the backend's view of that attempt's status. Refuses with 409 once 2 other installs are already running on the device, in either mode; a repeat request for an app that already has an install running is also refused with 409 rather than superseding it — retry once that attempt reaches a terminal state.
# List app installs
Source: https://docs.mobilerun.ai/api-reference/apps/list-app-installs
/api-reference/phones/tools.yaml get /devices/{deviceId}/apps/installs
Reports the backend's view of background app-install attempts on this device — status reflects the install ATTEMPT, not device ground truth; list-apps remains authoritative for what is actually installed. Records are in-memory and lost on service restart; terminal records are kept ~15 minutes. Not gated on device readiness, so it also answers while the device is offline or crashed.
# List apps
Source: https://docs.mobilerun.ai/api-reference/apps/list-apps
/api-reference/phones/tools.yaml get /devices/{deviceId}/apps
Returns detailed information about apps installed on the device, including package name and label. System and protected apps are excluded unless the corresponding query parameters are set.
# List packages
Source: https://docs.mobilerun.ai/api-reference/apps/list-packages
/api-reference/phones/tools.yaml get /devices/{deviceId}/packages
Returns the package names of apps installed on the device. System and protected packages are excluded unless the corresponding query parameters are set.
# Open a deep link
Source: https://docs.mobilerun.ai/api-reference/apps/open-a-deep-link
/api-reference/phones/tools.yaml post /devices/{deviceId}/apps/open-deep-link
Opens a deep link on the device. On Android the link is dispatched as an intent — packageName optionally pins it to a specific app and action overrides the default android.intent.action.VIEW. On iOS the URL is opened directly and the optional fields must be omitted. Protected packages are rejected.
# Revoke app permission
Source: https://docs.mobilerun.ai/api-reference/apps/revoke-app-permission
/api-reference/phones/tools.yaml delete /devices/{deviceId}/apps/{packageName}/permissions/{permission}
Revokes an Android runtime permission from the package named in the path. The permission is given by its short name (e.g. POST_NOTIFICATIONS).
# Start app
Source: https://docs.mobilerun.ai/api-reference/apps/start-app
/api-reference/phones/tools.yaml put /devices/{deviceId}/apps/{packageName}
Launches the app identified by the path package name, optionally starting a specific activity given in the request body. Protected packages cannot be started.
# Stop app
Source: https://docs.mobilerun.ai/api-reference/apps/stop-app
/api-reference/phones/tools.yaml patch /devices/{deviceId}/apps/{packageName}
Force-stops the app identified by the path package name. When clearData is set in the request body, the app's data is also cleared. Protected packages cannot be stopped.
# Execute JavaScript in the device's Chrome browser (CDP)
Source: https://docs.mobilerun.ai/api-reference/browser/execute-javascript-in-the-devices-chrome-browser-cdp
/api-reference/phones/tools.yaml post /devices/{deviceId}/browser/execute-script
Evaluates a JavaScript expression in the device's foreground Chrome tab via the Chrome DevTools Protocol and returns its JSON-serialized result. Devices without browser support return an unsupported-feature error.
# List available countries
Source: https://docs.mobilerun.ai/api-reference/countries/list-available-countries
/api-reference/mobilerun-proxy.yaml get /connect/countries
Lookup of countries that can be selected when creating a proxy. Each country lists the proxy types available there; without a ?type filter, every covered country is returned.
# Count claimed devices
Source: https://docs.mobilerun.ai/api-reference/devices/count-claimed-devices
/api-reference/phones.yaml get /devices/count
Returns the number of claimed devices for the user, broken down by device type.
# Get capabilities for a specific device
Source: https://docs.mobilerun.ai/api-reference/devices/get-capabilities-for-a-specific-device
/api-reference/phones.yaml get /devices/{deviceId}/capabilities
Returns the set of capabilities supported by this device. For a legacy device this reflects the live instance's actual tools rather than its static type; for a core-managed device it is resolved from provider/pool configuration without guaranteeing a live instance. Used to determine which tools and features are available for the device.
# Get device info
Source: https://docs.mobilerun.ai/api-reference/devices/get-device-info
/api-reference/phones.yaml get /devices/{deviceId}
Returns the current state and metadata for a single device, including its lifecycle state, type, stream URL, billing strategy, and timestamps. A stream token is included while the device is active.
# List devices
Source: https://docs.mobilerun.ai/api-reference/devices/list-devices
/api-reference/phones.yaml get /devices
Returns a paginated list of the user's devices along with pagination metadata.
# List tasks for a device
Source: https://docs.mobilerun.ai/api-reference/devices/list-tasks-for-a-device
/api-reference/phones.yaml get /devices/{deviceId}/tasks
Returns a paginated list of tasks that have run on the device, along with pagination metadata.
# Provision a new device
Source: https://docs.mobilerun.ai/api-reference/devices/provision-a-new-device
/api-reference/phones.yaml post /devices
Requests a new device for the authenticated user from the device spec in the request body. Optional query parameters select the canonical device type, target country, billing mode, and a profile to use as the base spec; deprecated device-type aliases remain accepted only during the documented compatibility grace period. The response returns the device and its stream token.
# Reboot a device
Source: https://docs.mobilerun.ai/api-reference/devices/reboot-a-device
/api-reference/phones.yaml post /devices/{deviceId}/reboot
Triggers a reboot of the device. The device transitions through its reboot lifecycle and becomes ready again once the restart completes.
# Reset a device to a fresh state
Source: https://docs.mobilerun.ai/api-reference/devices/reset-a-device-to-a-fresh-state
/api-reference/phones.yaml post /devices/{deviceId}/reset
Resets the device back to a clean state, clearing installed apps and user data accumulated during the session. The device transitions through its reset lifecycle before becoming ready again.
# Terminate a device
Source: https://docs.mobilerun.ai/api-reference/devices/terminate-a-device
/api-reference/phones.yaml delete /devices/{deviceId}
Terminates the device and releases its resources. Termination can be scheduled for a future time or chained from a previous device via the request body, in which case a service key is required.
# Update device name
Source: https://docs.mobilerun.ai/api-reference/devices/update-device-name
/api-reference/phones.yaml put /devices/{deviceId}/name
Sets the display name for a device from the name in the request body and returns the updated device.
# Wait for device to be ready
Source: https://docs.mobilerun.ai/api-reference/devices/wait-for-device-to-be-ready
/api-reference/phones.yaml get /devices/{deviceId}/wait
Blocks until the device reaches the ready state, then returns the same payload as Get device info. The call returns early with an error if the wait is cancelled or times out.
# Ingest an event
Source: https://docs.mobilerun.ai/api-reference/events/ingest-an-event
/api-reference/workflows.yaml post /events/ingest
Ingest an event for trigger evaluation. Returns immediately with 202 Accepted.
# Simulate event matching (dry run)
Source: https://docs.mobilerun.ai/api-reference/events/simulate-event-matching-dry-run
/api-reference/workflows.yaml post /events/dry-run
Simulate an event against all configured flows. Returns which flows would match and what actions would run, without storing the event or enqueuing jobs.
# Abort a running or pending execution
Source: https://docs.mobilerun.ai/api-reference/executions/abort-a-running-or-pending-execution
/api-reference/workflows.yaml post /executions/{executionId}/abort
Signals the worker to stop the execution between steps and marks it cancelled. Idempotent-ish: already-terminal executions return 409.
# Get execution details
Source: https://docs.mobilerun.ai/api-reference/executions/get-execution-details
/api-reference/workflows.yaml get /executions/{executionId}
Fetch a single flow execution by its ID, including its status, kind, result or error, and start/finish timestamps. Returns 404 if no execution matches.
# Get execution metrics
Source: https://docs.mobilerun.ai/api-reference/executions/get-execution-metrics
/api-reference/workflows.yaml get /executions/metrics
Return aggregate execution metrics — total count, counts by status, average duration, and the last execution time. Can be scoped by `flowId`, `triggerId`, and a `from`/`to` time range.
# List flow executions
Source: https://docs.mobilerun.ai/api-reference/executions/list-flow-executions
/api-reference/workflows.yaml get /executions
Return a paginated history of flow executions. Supports filtering by `flowId`, `triggerId`, `status`, and a `from`/`to` time range, plus free-text `search` and ordering by startedAt, finishedAt, or status.
# Delete file
Source: https://docs.mobilerun.ai/api-reference/files/delete-file
/api-reference/phones/tools.yaml delete /devices/{deviceId}/files
Deletes the file at the path given in the path query parameter from the device.
# Download file
Source: https://docs.mobilerun.ai/api-reference/files/download-file
/api-reference/phones/tools.yaml get /devices/{deviceId}/files/download
Pulls the file at the given path query parameter from the device and returns its raw bytes as an octet-stream.
# List files
Source: https://docs.mobilerun.ai/api-reference/files/list-files
/api-reference/phones/tools.yaml get /devices/{deviceId}/files
Lists the files at the directory path given in the path query parameter, returning each entry's metadata along with the path and total count.
# Upload file
Source: https://docs.mobilerun.ai/api-reference/files/upload-file
/api-reference/phones/tools.yaml post /devices/{deviceId}/files
Uploads a file to the device via multipart form data, writing it into the directory given by the path query parameter using the uploaded file's name.
# Add an action to a flow
Source: https://docs.mobilerun.ai/api-reference/flows/add-an-action-to-a-flow
/api-reference/workflows.yaml post /flows/{flowId}/actions
Append a single action to a flow at the given `position`, optionally nesting it under a `parentFlowActionId` or supplying its own `children`. Supports a `nameOverride`, param `overrides`, and `continueOnError`. Returns 404 if the flow does not exist.
# Clone a flow
Source: https://docs.mobilerun.ai/api-reference/flows/clone-a-flow
/api-reference/workflows.yaml post /flows/{flowId}/clone
Create a copy of an existing flow, including its actions and settings. The optional body can override the new flow's `name` and target `deviceIds`. Returns 404 if the source flow does not exist.
# Create a flow
Source: https://docs.mobilerun.ai/api-reference/flows/create-a-flow
/api-reference/workflows.yaml post /flows
Create a flow that binds a trigger (`triggerId`) to an ordered list of actions, with at least one action required. Optional settings include target `deviceIds`, a cooldown (`cooldownSeconds`/`cooldownScope`), and webhook notifications on success or failure.
# Delete a flow
Source: https://docs.mobilerun.ai/api-reference/flows/delete-a-flow
/api-reference/workflows.yaml delete /flows/{flowId}
Delete a flow by its ID. Returns 404 if no flow matches.
# Dry-run a flow
Source: https://docs.mobilerun.ai/api-reference/flows/dry-run-a-flow
/api-reference/workflows.yaml post /flows/{flowId}/dry-run
Simulate this flow firing without storing events, enqueuing jobs, or consuming cooldown/rate-limit slots.
Works for every trigger activation type:
- `event`: validates the payload against the event catalog schema and evaluates the trigger conditions.
- `custom`: validates the payload against the custom payload schema (conditions do not apply).
- `schedule`: ignores the payload and reports the next fire time.
The response reports `wouldFire` — whether the flow would actually run right now — alongside the gates that decide it (enabled, device attached, blocked, cooldown). `rateLimited` is informational and is not folded into `wouldFire`.
# Get a flow
Source: https://docs.mobilerun.ai/api-reference/flows/get-a-flow
/api-reference/workflows.yaml get /flows/{flowId}
Fetch a single flow by its ID, including its trigger binding, configuration, and current status. Returns 404 if no flow matches.
# List actions for a flow
Source: https://docs.mobilerun.ai/api-reference/flows/list-actions-for-a-flow
/api-reference/workflows.yaml get /flows/{flowId}/actions
Return the ordered list of actions attached to a flow, including any nested child actions. Returns 404 if the flow does not exist.
# List flows
Source: https://docs.mobilerun.ai/api-reference/flows/list-flows
/api-reference/workflows.yaml get /flows
Return a paginated list of flows. Supports filtering by `triggerId`, `enabled`, one or more health `status` values (healthy, failing, blocked), `mine` (flows created by the calling actor), `createdBy` (flows created by a given actor id — mutually exclusive with `mine`), plus free-text `search` and ordering.
# List self-healing repair episodes
Source: https://docs.mobilerun.ai/api-reference/flows/list-self-healing-repair-episodes
/api-reference/workflows.yaml get /flows/{flowId}/repairs
# Remove an action from a flow
Source: https://docs.mobilerun.ai/api-reference/flows/remove-an-action-from-a-flow
/api-reference/workflows.yaml delete /flows/{flowId}/actions/{flowActionId}
Remove a single action from a flow by its `flowActionId`. Returns 404 if the flow or flow action does not exist.
# Replace all actions for a flow
Source: https://docs.mobilerun.ai/api-reference/flows/replace-all-actions-for-a-flow
/api-reference/workflows.yaml put /flows/{flowId}/actions
Replace a flow's entire action list with the supplied set (at least one required). Each action references an `actionId` and a unique `position`, and may include nested `children`, a `nameOverride`, param `overrides`, and a `continueOnError` flag. Returns 404 if the flow does not exist.
# Unblock a flow
Source: https://docs.mobilerun.ai/api-reference/flows/unblock-a-flow
/api-reference/workflows.yaml post /flows/{flowId}/unblock
Clear a flow's blocked status after fixing the underlying issue. Idempotent — safe to call on already-healthy flows.
# Update a flow
Source: https://docs.mobilerun.ai/api-reference/flows/update-a-flow
/api-reference/workflows.yaml patch /flows/{flowId}
Partially update a flow's settings — name, trigger binding, enabled state, target devices, cooldown, or notifications; all fields are optional. Actions are managed through the flow-actions endpoints, not here. Returns 404 if the flow does not exist.
# Check if overlay is visible
Source: https://docs.mobilerun.ai/api-reference/misc/check-if-overlay-is-visible
/api-reference/phones/tools.yaml get /devices/{deviceId}/overlay
Returns whether the accessibility overlay is currently visible on the device.
# Device fingerprint snapshot
Source: https://docs.mobilerun.ai/api-reference/misc/device-fingerprint-snapshot
/api-reference/phones/tools.yaml get /devices/{deviceId}/fingerprint
Returns a live snapshot of the device's spoofed identity, including model, display, identifiers, and carrier. Devices without fingerprint support return an unsupported-feature error.
# Device time
Source: https://docs.mobilerun.ai/api-reference/misc/device-time
/api-reference/phones/tools.yaml get /devices/{deviceId}/time
Returns the device's current wall-clock time as an RFC 3339 timestamp.
# Disable kiosk (lock-task) mode
Source: https://docs.mobilerun.ai/api-reference/misc/disable-kiosk-lock-task-mode
/api-reference/phones/tools.yaml delete /devices/{deviceId}/kiosk
Disables Android lock-task (kiosk) mode on the device, releasing it from the locked app.
# Enable kiosk (lock-task) mode
Source: https://docs.mobilerun.ai/api-reference/misc/enable-kiosk-lock-task-mode
/api-reference/phones/tools.yaml put /devices/{deviceId}/kiosk
Locks the device to the package named in the request body using Android lock-task (kiosk) mode, preventing the user from leaving the app.
# Get device language/locale
Source: https://docs.mobilerun.ai/api-reference/misc/get-device-languagelocale
/api-reference/phones/tools.yaml get /devices/{deviceId}/language
Returns the device's current language/locale as a BCP-47 locale string.
# Get device location
Source: https://docs.mobilerun.ai/api-reference/misc/get-device-location
/api-reference/phones/tools.yaml get /devices/{deviceId}/location
Returns the device's current simulated GPS location as latitude and longitude. Devices without geo support return an unsupported-feature error.
# Get device timezone
Source: https://docs.mobilerun.ai/api-reference/misc/get-device-timezone
/api-reference/phones/tools.yaml get /devices/{deviceId}/timezone
Returns the device's current timezone identifier. Devices that do not support timezone control return an unsupported-feature error.
# Reset the device location to default
Source: https://docs.mobilerun.ai/api-reference/misc/reset-the-device-location-to-default
/api-reference/phones/tools.yaml delete /devices/{deviceId}/location
Clears any simulated GPS location and restores the device's default location behavior. Devices without geo support return an unsupported-feature error.
# Set device language/locale
Source: https://docs.mobilerun.ai/api-reference/misc/set-device-languagelocale
/api-reference/phones/tools.yaml post /devices/{deviceId}/language
Sets the device language/locale to the BCP-47 locale in the request body. An optional restart flag applies the change immediately by restarting the zygote instead of waiting for the next reboot.
# Set device location
Source: https://docs.mobilerun.ai/api-reference/misc/set-device-location
/api-reference/phones/tools.yaml post /devices/{deviceId}/location
Sets the device's simulated GPS location to the latitude and longitude in the request body. Devices without geo support return an unsupported-feature error.
# Set device timezone
Source: https://docs.mobilerun.ai/api-reference/misc/set-device-timezone
/api-reference/phones/tools.yaml post /devices/{deviceId}/timezone
Sets the device timezone to the identifier in the request body. Devices that do not support timezone control return an unsupported-feature error.
# Set overlay visibility
Source: https://docs.mobilerun.ai/api-reference/misc/set-overlay-visibility
/api-reference/phones/tools.yaml post /devices/{deviceId}/overlay
Shows or hides the accessibility overlay on the device based on the visibility flag in the request body.
# Get all LLM models
Source: https://docs.mobilerun.ai/api-reference/models/get-all-llm-models
/api-reference/tasks.yaml get /models
Get all LLM models
# Clear input
Source: https://docs.mobilerun.ai/api-reference/navigation/clear-input
/api-reference/phones/tools.yaml delete /devices/{deviceId}/keyboard
Clears the contents of the currently focused text input field.
# Input key
Source: https://docs.mobilerun.ai/api-reference/navigation/input-key
/api-reference/phones/tools.yaml put /devices/{deviceId}/keyboard
Sends a single Android key event to the device, identified by its key code.
# Input text
Source: https://docs.mobilerun.ai/api-reference/navigation/input-text
/api-reference/phones/tools.yaml post /devices/{deviceId}/keyboard
Types the given text into the focused input field. Supports optionally clearing the field first and a stealth mode that emulates human typing speed and error rate on supported devices.
# Perform a global action
Source: https://docs.mobilerun.ai/api-reference/navigation/perform-a-global-action
/api-reference/phones/tools.yaml post /devices/{deviceId}/global
Performs a global system action on the device, such as navigating back or going to the home screen, identified by an action code.
# Swipe
Source: https://docs.mobilerun.ai/api-reference/navigation/swipe
/api-reference/phones/tools.yaml post /devices/{deviceId}/swipe
Swipes from a start coordinate to an end coordinate over the given duration in milliseconds. An optional stealth flag applies human-like jitter and curved paths on devices that support it.
# Take screenshot
Source: https://docs.mobilerun.ai/api-reference/navigation/take-screenshot
/api-reference/phones/tools.yaml get /devices/{deviceId}/screenshot
Captures the device screen and returns it as a PNG image. An optional hideOverlay query parameter excludes the accessibility overlay from the capture.
# Tap by coordinates
Source: https://docs.mobilerun.ai/api-reference/navigation/tap-by-coordinates
/api-reference/phones/tools.yaml post /devices/{deviceId}/tap
Taps the device screen at the given x/y coordinates. An optional stealth flag routes the tap through human-like input on devices that support it.
# UI state
Source: https://docs.mobilerun.ai/api-reference/navigation/ui-state
/api-reference/phones/tools.yaml get /devices/{deviceId}/ui-state
Returns the current accessibility UI state of the device as a structured tree of on-screen elements. An optional filter query reduces the result to interactive elements.
# Get notification preferences
Source: https://docs.mobilerun.ai/api-reference/notifications/get-notification-preferences
/api-reference/webhooks.yaml get /notifications/preferences
Returns your current notification preferences, expressed as the list of event types you have muted. An empty list means notifications are enabled for all notifiable event types.
# List notification event types
Source: https://docs.mobilerun.ai/api-reference/notifications/list-notification-event-types
/api-reference/webhooks.yaml get /notifications/catalog
Returns the catalog of notifiable event types grouped by source category. Each event lists its type identifier, label, and description, which can be referenced when muting event types in notification preferences.
# Update notification preferences
Source: https://docs.mobilerun.ai/api-reference/notifications/update-notification-preferences
/api-reference/webhooks.yaml patch /notifications/preferences
Replaces your set of muted event types with the supplied list. Any unknown or non-notifiable types are dropped, and the response returns the muted types that were actually stored.
# Cancel a phone-number purchase or an active rental
Source: https://docs.mobilerun.ai/api-reference/numbers/cancel-a-phone-number-purchase-or-an-active-rental
/api-reference/mobilerun-numbers.yaml delete /numbers/phones/{id}
Cancels a Mobilerun Phone. The outcome depends on the number's current state:
- If the number is still awaiting payment and no payment for it is currently being processed, the checkout is closed immediately and the number is retired.
- If the number is on the standard paid plan and already paid and in service, cancellation is scheduled for the end of the current billing period rather than taking effect immediately. The number stays usable through the period already paid for, with no partial refund. Calling this again while a cancellation is already scheduled is a no-op that returns the same result. The response's `state` reflects this as `cancel_scheduled` with `cancelAtPeriodEnd: true`; `currentPeriodEnd` is populated once billing confirms the cancellation.
Any other state (already refunding, a permanent billing failure, a payment currently being processed, an included-plan number, or a non-hosted/BYO number) returns 409 `not_cancellable`. Returns 404 if the number doesn't exist or isn't owned by the caller.
# Get phone number by id
Source: https://docs.mobilerun.ai/api-reference/numbers/get-phone-number-by-id
/api-reference/mobilerun-numbers.yaml get /numbers/phones/{id}
Retrieves a single phone number.
# List dedicated-number countries
Source: https://docs.mobilerun.ai/api-reference/numbers/list-dedicated-number-countries
/api-reference/mobilerun-numbers.yaml get /numbers/phones/countries
Lists the countries currently offered for a dedicated Mobilerun Phone, with live stock status. Pass `country` as the `country` field on POST /numbers/phones.
# List messages (inbox + outbox) on a number
Source: https://docs.mobilerun.ai/api-reference/numbers/list-messages-inbox-+-outbox-on-a-number
/api-reference/mobilerun-numbers.yaml get /numbers/phones/{id}/messages
Returns SMS messages on the number, inbound and outbound, scoped to the authenticated user. Newest first. Messages stay with the number across device switches.
# List phone numbers
Source: https://docs.mobilerun.ai/api-reference/numbers/list-phone-numbers
/api-reference/mobilerun-numbers.yaml get /numbers/phones
Lists phone numbers owned by the authenticated user — both BYO (`user`) and provisioned (`mobilerun`) numbers.
# List phone purposes
Source: https://docs.mobilerun.ai/api-reference/numbers/list-phone-purposes
/api-reference/mobilerun-numbers.yaml get /numbers/phones/purposes
Lists the optional purposes currently available for a Mobilerun Phone.
# Purchase a phone number
Source: https://docs.mobilerun.ai/api-reference/numbers/purchase-a-phone-number
/api-reference/mobilerun-numbers.yaml post /numbers/phones
Starts a Mobilerun Phone purchase for the authenticated owner. Accepted requests always return the same asynchronous envelope; poll GET /numbers/phones/{id} for its business state. `purpose` and `country` are mutually exclusive.
# Create a proxy
Source: https://docs.mobilerun.ai/api-reference/proxies/create-a-proxy
/api-reference/mobilerun-proxy.yaml post /connect/proxies
Provisions a proxy of the requested type for the caller in the selected country.
# Delete a proxy
Source: https://docs.mobilerun.ai/api-reference/proxies/delete-a-proxy
/api-reference/mobilerun-proxy.yaml delete /connect/proxies/{id}
Deletes the proxy identified by the path ID and releases its provisioning. Returns 404 if no such proxy exists for the caller.
# Get proxy by ID
Source: https://docs.mobilerun.ai/api-reference/proxies/get-proxy-by-id
/api-reference/mobilerun-proxy.yaml get /connect/proxies/{id}
Returns the proxy identified by the path ID. The response includes the proxy's password.
# Latency check
Source: https://docs.mobilerun.ai/api-reference/proxies/latency-check
/api-reference/mobilerun-proxy.yaml get /connect/proxies/{id}/ping
Returns the most recent cached network-latency measurement for the proxy, sampled periodically by connecting through the proxy to a fixed target. `latency` is null when no measurement is available yet (e.g. the proxy is not active, or it has not been sampled since coming online).
# List proxies
Source: https://docs.mobilerun.ai/api-reference/proxies/list-proxies
/api-reference/mobilerun-proxy.yaml get /connect/proxies
Returns proxies owned by the calling tenant (the X-Owner-Id header, falling back to X-User-ID). Credentials are omitted from the list.
# List Proxy Connections
Source: https://docs.mobilerun.ai/api-reference/proxies/list-proxy-connections
/api-reference/mobilerun-proxy.yaml get /connect/proxies/{id}/connections
Returns the connection history recorded for this proxy, one item per connection (aggregated across the connection's lifetime). Supports filtering on every property plus ordering and pagination. Returns 503 when the connection-insights backend is disabled or unreachable.
# Connect proxy
Source: https://docs.mobilerun.ai/api-reference/proxy/connect-proxy
/api-reference/phones/tools.yaml post /devices/{deviceId}/proxy
Routes the device's traffic through a SOCKS5 proxy supplied in the request body, replacing any existing connection. A smartIp option can be used to select an IP automatically; the legacy flat host/port/user/password fields remain supported.
# Disconnect proxy
Source: https://docs.mobilerun.ai/api-reference/proxy/disconnect-proxy
/api-reference/phones/tools.yaml delete /devices/{deviceId}/proxy
Disconnects the device's active proxy connection and clears its stored proxy state. Returns successfully if no proxy is connected.
# Get proxy connection state
Source: https://docs.mobilerun.ai/api-reference/proxy/get-proxy-connection-state
/api-reference/phones/tools.yaml get /devices/{deviceId}/proxy
Returns the device's current proxy connection state, including whether a proxy is connected and its protocol and name.
# Delete a device recording
Source: https://docs.mobilerun.ai/api-reference/recordings/delete-a-device-recording
/api-reference/phones/tools.yaml delete /devices/{deviceId}/recordings/{recordingId}
# Get a device recording
Source: https://docs.mobilerun.ai/api-reference/recordings/get-a-device-recording
/api-reference/phones/tools.yaml get /devices/{deviceId}/recordings/{recordingId}
# Get a device recording trajectory
Source: https://docs.mobilerun.ai/api-reference/recordings/get-a-device-recording-trajectory
/api-reference/phones/tools.yaml get /devices/{deviceId}/recordings/{recordingId}/trajectory
# Get a device recording video
Source: https://docs.mobilerun.ai/api-reference/recordings/get-a-device-recording-video
/api-reference/phones/tools.yaml get /devices/{deviceId}/recordings/{recordingId}/video
# List device recordings
Source: https://docs.mobilerun.ai/api-reference/recordings/list-device-recordings
/api-reference/phones/tools.yaml get /devices/{deviceId}/recordings
# Start a device recording
Source: https://docs.mobilerun.ai/api-reference/recordings/start-a-device-recording
/api-reference/phones/tools.yaml post /devices/{deviceId}/recordings
# Stop a device recording
Source: https://docs.mobilerun.ai/api-reference/recordings/stop-a-device-recording
/api-reference/phones/tools.yaml post /devices/{deviceId}/recordings/{recordingId}
# Attach Task
Source: https://docs.mobilerun.ai/api-reference/tasks/attach-task
/api-reference/tasks.yaml get /tasks/{task_id}/attach
Attach to a running task and receive its events as an SSE stream.
# Get Task
Source: https://docs.mobilerun.ai/api-reference/tasks/get-task
/api-reference/tasks.yaml get /tasks/{task_id}
Get full details of a task by ID.
# Get Task Screenshot
Source: https://docs.mobilerun.ai/api-reference/tasks/get-task-screenshot
/api-reference/tasks.yaml get /tasks/{task_id}/screenshots/{index}
Get a specific screenshot by index.
# Get Task Screenshots
Source: https://docs.mobilerun.ai/api-reference/tasks/get-task-screenshots
/api-reference/tasks.yaml get /tasks/{task_id}/screenshots
List all screenshot URLs for a task.
# Get Task Status
Source: https://docs.mobilerun.ai/api-reference/tasks/get-task-status
/api-reference/tasks.yaml get /tasks/{task_id}/status
Get the status of a task.
# Get Task Trajectory
Source: https://docs.mobilerun.ai/api-reference/tasks/get-task-trajectory
/api-reference/tasks.yaml get /tasks/{task_id}/trajectory
Get the trajectory of a task.
# Get Task Ui State
Source: https://docs.mobilerun.ai/api-reference/tasks/get-task-ui-state
/api-reference/tasks.yaml get /tasks/{task_id}/ui_states/{index}
Get a specific UI state by index.
# Get Task Ui States
Source: https://docs.mobilerun.ai/api-reference/tasks/get-task-ui-states
/api-reference/tasks.yaml get /tasks/{task_id}/ui_states
List all UI state URLs for a task.
# List Tasks
Source: https://docs.mobilerun.ai/api-reference/tasks/list-tasks
/api-reference/tasks.yaml get /tasks
List tasks with optional filtering, sorting, and pagination.
# Run Streamed Task
Source: https://docs.mobilerun.ai/api-reference/tasks/run-streamed-task
/api-reference/tasks.yaml post /tasks/stream
Create and dispatch a new agent task, returning an SSE stream of task events. Cancels the task if the client disconnects.
# Run Task
Source: https://docs.mobilerun.ai/api-reference/tasks/run-task
/api-reference/tasks.yaml post /tasks
Create and dispatch a new agent task. Returns the task ID and device stream details.
# Send Message
Source: https://docs.mobilerun.ai/api-reference/tasks/send-message
/api-reference/tasks.yaml post /tasks/{task_id}/message
Send a message to a running agent task. The message ID is delivered via SSE (UserMessageEvent with action=queued).
# Stop Task
Source: https://docs.mobilerun.ai/api-reference/tasks/stop-task
/api-reference/tasks.yaml post /tasks/{task_id}/cancel
Cancel a running task. Returns an error if the task is already in a terminal state.
# Create a trigger
Source: https://docs.mobilerun.ai/api-reference/triggers/create-a-trigger
/api-reference/workflows.yaml post /triggers
Create a trigger with an activation type of `event`, `schedule`, or `custom`. Each type requires its own fields (e.g. `eventType` and optional `conditions` for events, `scheduleRule` and `timezone` for schedules, `customPayloadSchema` for custom triggers); mismatched fields are rejected.
# Delete a trigger
Source: https://docs.mobilerun.ai/api-reference/triggers/delete-a-trigger
/api-reference/workflows.yaml delete /triggers/{triggerId}
Delete a trigger by its ID. Returns 404 if no trigger matches.
# Fire a custom trigger with payload
Source: https://docs.mobilerun.ai/api-reference/triggers/fire-a-custom-trigger-with-payload
/api-reference/workflows.yaml post /triggers/{triggerId}/fire
Invoke a custom trigger directly with an arbitrary JSON payload.
Fan-out: a trigger may be referenced by multiple flows (workflows). Firing it enqueues one execution per enabled, non-deleted flow attached to this trigger, each receiving the same payload. The `enqueuedCount` in the response reports how many were enqueued (0 if no flows are attached, or if all matching flows are gated by a cooldown).
Payload validation:
- If the trigger has a `customPayloadSchema`, the payload is validated against it (JSON Schema via AJV).
- If no schema is configured, the payload only needs to be a JSON object — any keys and values are accepted.
Only triggers with `activation = "custom"` can be fired through this endpoint; event and schedule triggers return 409.
# Get a trigger
Source: https://docs.mobilerun.ai/api-reference/triggers/get-a-trigger
/api-reference/workflows.yaml get /triggers/{triggerId}
Fetch a single trigger by its ID, including its activation type and type-specific configuration. Returns 404 if no trigger matches.
# List triggers
Source: https://docs.mobilerun.ai/api-reference/triggers/list-triggers
/api-reference/workflows.yaml get /triggers
Return a paginated list of triggers. Supports filtering by `activation` and `eventType`, free-text `search`, and ordering by name, createdAt, or updatedAt.
# Update a trigger
Source: https://docs.mobilerun.ai/api-reference/triggers/update-a-trigger
/api-reference/workflows.yaml patch /triggers/{triggerId}
Partially update a trigger; all fields are optional. When `activation` is changed, the type-specific field rules are re-validated. Returns 404 if the trigger does not exist.
# Create a SOCKS5 user
Source: https://docs.mobilerun.ai/api-reference/users/create-a-socks5-user
/api-reference/mobilerun-proxy.yaml post /connect/users
Creates a SOCKS5 credential, optionally bound to a proxy for dedicated routing. Username and password are generated when omitted.
# Delete a SOCKS5 user
Source: https://docs.mobilerun.ai/api-reference/users/delete-a-socks5-user
/api-reference/mobilerun-proxy.yaml delete /connect/users/{id}
Deletes the SOCKS5 user identified by the path ID, revoking its credentials and any proxy binding. Returns 404 if no such user exists for the caller.
# Get a SOCKS5 user by ID
Source: https://docs.mobilerun.ai/api-reference/users/get-a-socks5-user-by-id
/api-reference/mobilerun-proxy.yaml get /connect/users/{id}
Returns the SOCKS5 user identified by the path ID. The response includes the user's password.
# List connections by SOCKS5 user
Source: https://docs.mobilerun.ai/api-reference/users/list-connections-by-socks5-user
/api-reference/mobilerun-proxy.yaml get /connect/users/{id}/connections
Returns the connection history recorded for this user, one item per connection (aggregated across the connection's lifetime). Supports filtering on every property plus ordering and pagination. Returns 503 when the connection-insights backend is disabled or unreachable.
# List SOCKS5 users
Source: https://docs.mobilerun.ai/api-reference/users/list-socks5-users
/api-reference/mobilerun-proxy.yaml get /connect/users
Returns SOCKS5 users owned by the caller. Passwords are omitted from the list.
# Update a SOCKS5 user
Source: https://docs.mobilerun.ai/api-reference/users/update-a-socks5-user
/api-reference/mobilerun-proxy.yaml patch /connect/users/{id}
Rebind the user to a different proxy (or detach it by passing null).
# Get a delivery with its attempts
Source: https://docs.mobilerun.ai/api-reference/webhook-deliveries/get-a-delivery-with-its-attempts
/api-reference/webhooks.yaml get /webhooks/{id}/deliveries/{deliveryId}
Returns a single delivery for a webhook subscription along with the full list of captured attempt records. Each attempt includes the request URL, method, headers and body, whether it was signed, and the response status, headers, and snippet.
# Get delivery statistics
Source: https://docs.mobilerun.ai/api-reference/webhook-deliveries/get-delivery-statistics
/api-reference/webhooks.yaml get /webhooks/deliveries/stats
Returns aggregate delivery statistics across all of your webhooks, including the total count, a breakdown by status (pending, success, skipped, dead), and the overall success rate. An optional `since` timestamp narrows the reporting window.
# List deliveries across all your webhooks
Source: https://docs.mobilerun.ai/api-reference/webhook-deliveries/list-deliveries-across-all-your-webhooks
/api-reference/webhooks.yaml get /webhooks/deliveries
Returns a paginated feed of webhook deliveries across all of your subscriptions, with the originating endpoint URL included on each record. Results can be filtered by delivery status (pending, success, skipped, or dead), by a `since` timestamp, and by `eventId` (exact match against the originating event id).
# List deliveries for a webhook
Source: https://docs.mobilerun.ai/api-reference/webhook-deliveries/list-deliveries-for-a-webhook
/api-reference/webhooks.yaml get /webhooks/{id}/deliveries
Returns a paginated list of deliveries for a single webhook subscription, identified by its id. Each record reports the event, delivery status, attempt count, and the last response code or error. Results can be filtered by `eventId` (exact match against the originating event id).
# List subscribable event types per source
Source: https://docs.mobilerun.ai/api-reference/webhook-event-types/list-subscribable-event-types-per-source
/api-reference/webhooks.yaml get /event-types
Returns the catalog of event types that webhook subscriptions can subscribe to, grouped by source. Use the returned type identifiers as the `eventTypes` values when creating or updating a webhook.
# Delete a webhook subscription
Source: https://docs.mobilerun.ai/api-reference/webhooks/delete-a-webhook-subscription
/api-reference/webhooks.yaml delete /webhooks/{id}
Deletes a webhook subscription so it stops receiving deliveries. Returns 204 No Content on success.
# Get a webhook subscription
Source: https://docs.mobilerun.ai/api-reference/webhooks/get-a-webhook-subscription
/api-reference/webhooks.yaml get /webhooks/{id}
Returns a single webhook subscription by id, including its URL, subscribed event types, state, and system-observed delivery health. The signing secret is never included.
# List your webhook subscriptions
Source: https://docs.mobilerun.ai/api-reference/webhooks/list-your-webhook-subscriptions
/api-reference/webhooks.yaml get /webhooks
Returns a paginated list of your webhook subscriptions, optionally filtered by status (active, failing, blocked, or disabled) and/or by `search` (a case-insensitive substring match against the URL or description). The response also includes per-status counts across all of your subscriptions.
# Register a webhook subscription
Source: https://docs.mobilerun.ai/api-reference/webhooks/register-a-webhook-subscription
/api-reference/webhooks.yaml post /webhooks
Creates a webhook subscription with a delivery URL and an optional list of event types to subscribe to (defaults to all when omitted). The response includes the generated signing secret, which is returned only once at creation time and cannot be retrieved later.
# Rotate signing secret
Source: https://docs.mobilerun.ai/api-reference/webhooks/rotate-signing-secret
/api-reference/webhooks.yaml post /webhooks/{id}/rotate-secret
Generates a new signing secret for the webhook subscription and returns it once in the response. The previous secret is replaced immediately, so any signature verification on your endpoint must be updated to use the new value.
# Send a one-shot test delivery
Source: https://docs.mobilerun.ai/api-reference/webhooks/send-a-one-shot-test-delivery
/api-reference/webhooks.yaml post /webhooks/{id}/test
Sends a single test payload to the webhook subscription URL to verify connectivity. The response reports whether the attempt succeeded along with the returned HTTP status code or error, if any.
# Update a webhook subscription
Source: https://docs.mobilerun.ai/api-reference/webhooks/update-a-webhook-subscription
/api-reference/webhooks.yaml patch /webhooks/{id}
Updates a webhook subscription. Any combination of the subscribed event types, state (ACTIVE or DISABLED), and description may be changed, and at least one field must be supplied. Setting state to ACTIVE re-enables a subscription that was auto-blocked after sustained delivery failures.
# Apps
Source: https://docs.mobilerun.ai/apps
Manage your app library by uploading custom APKs to deploy to your devices.
The Apps tab is your central hub for managing Android applications. Upload your own APKs to build your app library, then deploy to your devices.
## Overview
The Apps tab allows you to:
* View all apps available in your user space
* Upload custom APK files
* Deploy apps to your devices
## App Library
Your app library displays all applications you have access to. Each app entry shows:
| Field | Description |
| ---------------- | ---------------------------------------------------- |
| **App Name** | The application display name |
| **Package Name** | Android package identifier (e.g., `com.example.app`) |
| **Version** | Installed app version |
## Uploading Custom APKs
To upload your own APK:
1. Click the **Upload** button
2. Select your APK file from your local machine
3. Wait for the upload and processing to complete
4. The app appears in your library once ready
Uploaded APKs are stored securely in your user space and are only accessible to your account.
## Deploying Apps to Devices
Once an app is in your library, you can install it on any of your devices:
| Device Type | Deployment |
| ------------------ | ----------------------------------------------------------------------------------------------- |
| **Cloud Phone** | Apps persist across sessions on your dedicated virtual devices |
| **Physical Phone** | Apps install to your dedicated real hardware in the Mobilerun data center |
| **Personal Phone** | Apps install to your connected hardware via the [Mobilerun Portal App](/guides/connect-android) |
### Installation Methods
**From the Apps tab**
Select an app and choose which device to install it on.
**From the Playground**
Reference apps by package name in your agent prompts. The agent can install and interact with any app in your library.
**Via API**
Specify apps in your task configuration when running tasks programmatically.
## Managing Apps
### Remove Apps
Delete apps from your library when no longer needed. This removes the app from your user space but does not uninstall it from devices where it was previously deployed.
### Update Apps
Upload a new version of an APK to replace the existing one in your library.
### View App Details
Click on any app to see:
* Full package information
* Installation history
* Devices where the app is currently installed
# Browser Automation
Source: https://docs.mobilerun.ai/browser-automation
Drive Chrome on a Mobilerun device with Puppeteer or Playwright over the Chrome DevTools Protocol.
Mobilerun exposes a per-device WebSocket that reverse-proxies Chrome-for-Android's DevTools (CDP) endpoint. Off-the-shelf browser automation libraries — [Puppeteer](https://pptr.dev) and [Playwright](https://playwright.dev) — connect to it with a single URL, just like they would to a local Chrome instance.
Use this when you want to script Chrome on a device directly (navigate, evaluate JavaScript, intercept network requests, scrape content) instead of going through the agent.
## Endpoint
```
wss://api.mobilerun.ai/v1/devices/{deviceId}/browser/cdp
```
The endpoint is a **browser-level** CDP socket. Pages are multiplexed over the CDP `Target` domain — you do not need a separate URL per tab.
### Authentication
Pass your Mobilerun API key as a request header during the WebSocket handshake:
```
Authorization: Bearer dr_sk_YOUR_API_KEY
```
Always send the key as a header. Never put credentials in the URL — query-string tokens leak into logs, traces, and proxy access records.
### Requirements
* The device must be in the `ready` state. See [Devices](/devices) for state details.
* Chrome must be installed and have remote debugging available on the device. If Chrome's CDP socket is not reachable, the connection fails at handshake with a clear error.
* You need a Mobilerun [API key](/api-keys).
## Connect with Puppeteer
```js theme={null}
import puppeteer from 'puppeteer-core';
const browser = await puppeteer.connect({
browserWSEndpoint: `wss://api.mobilerun.ai/v1/devices/${deviceId}/browser/cdp`,
headers: {
Authorization: `Bearer ${process.env.MOBILERUN_API_KEY}`,
},
});
const page = await browser.newPage();
await page.goto('https://example.com');
const title = await page.title();
console.log(title);
await browser.disconnect();
```
## Connect with Playwright
```js theme={null}
import { chromium } from 'playwright';
const browser = await chromium.connectOverCDP(
`wss://api.mobilerun.ai/v1/devices/${deviceId}/browser/cdp`,
{
headers: {
Authorization: `Bearer ${process.env.MOBILERUN_API_KEY}`,
},
}
);
const context = browser.contexts()[0] ?? await browser.newContext();
const page = await context.newPage();
await page.goto('https://example.com');
console.log(await page.title());
await browser.close();
```
## How it works
The endpoint transparently bridges the WebSocket to Chrome's `chrome_devtools_remote` socket on the device. There is no local port forwarding to set up and no separate ADB tunnel to manage — once the device is connected to Mobilerun, the CDP endpoint is available automatically on every adb-backed device type ([Cloud Phone](/device-types#cloud-phone) and [Physical Phone](/device-types#physical-phone)).
Standard CDP semantics apply: connect once at the browser level, then attach to individual tabs through `Target.getTargets` / `Target.attachToTarget` (Puppeteer and Playwright handle this for you).
## Limitations
* **Chrome only.** WebView debugging (`webview_devtools_remote_`) is not exposed.
* **Browser-level only.** There is no per-page `/json/list` proxy — use the CDP `Target` domain to enumerate and attach to pages.
* **Chrome must be debuggable.** A device without an active Chrome CDP socket fails at connect time.
# Cloud Quickstart
Source: https://docs.mobilerun.ai/cloud/quickstart
Run your first autonomous task on a hosted device using the TypeScript SDK, Python, or cURL.
This guide takes you from zero to a completed cloud task. You will find a ready device, submit an
autonomous goal, wait for it to finish, and read the result. You can do this in **TypeScript**, in
**Python**, or with plain **cURL**.
Everything here uses the Mobilerun **Cloud**, which gives you hosted devices and a managed agent.
There is no local install, no adb, and no Portal APK. If you would rather run the open source
package on your own machine, follow the [Framework Quickstart](/framework/quickstart) instead.
## Prerequisites
* A Mobilerun account. You can sign up at [cloud.mobilerun.ai](https://cloud.mobilerun.ai).
* A **cloud API key** that starts with `dr_sk_` from the [API Keys](/api-keys) page. This is the
only credential you need, because the agent's LLM usage is billed from your [credit](/credits)
balance and you do **not** need your own model key.
* A **ready device**. You can connect your own phone with the
[Portal app](/guides/connect-android), or provision a [Cloud Phone](/device-types#cloud-phone)
or a [Physical Phone](/device-types#physical-phone) in the dashboard.
Autonomous tasks consume [credits](/credits) at roughly 0.5 credits per agent step. New accounts
and device subscriptions include a monthly credit allowance.
## 1. Set your API key
The examples below read your key from the `MOBILERUN_API_KEY` environment variable.
```bash theme={null}
export MOBILERUN_API_KEY=dr_sk_your_key_here
```
If you call `new Mobilerun()` without an `apiKey`, the TypeScript SDK auto-detects the
`MOBILERUN_CLOUD_API_KEY` variable instead. The examples pass the key explicitly, so either name
works.
The TypeScript example uses top level `await`, so run it as an ES module. Set `"type": "module"`
in your `package.json` or use a `.mts` file, and then run it with `npx tsx quickstart.ts`.
## 2. Install
```bash npm theme={null}
npm install @mobilerun/sdk
```
```bash pip theme={null}
# The Python example below uses only the standard requests library
pip install requests
```
## 3. Run your first task
Each example does the same four things. It lists a ready device, submits a task, polls until the
task finishes, and prints the result.
```typescript TypeScript theme={null}
import Mobilerun from '@mobilerun/sdk';
// Pass your dr_sk_ key explicitly (or set MOBILERUN_CLOUD_API_KEY and call new Mobilerun())
const client = new Mobilerun({ apiKey: process.env.MOBILERUN_API_KEY });
// 1. Find a device that's ready to accept tasks
const devices = await client.devices.list({ state: ['ready'] });
const device = devices.items[0];
if (!device) throw new Error('No ready device. Connect one in the dashboard first.');
// 2. Submit an autonomous task
const task = await client.tasks.run({
deviceId: device.id,
task: 'Open Settings and tell me the Android version',
llmModel: 'mobilerun/mobile-agent-fast',
maxSteps: 50,
});
console.log('Task started:', task.id);
// 3. Wait for it to finish
let status = await client.tasks.getStatus(task.id);
while (!['completed', 'failed', 'cancelled'].includes(status.status)) {
await new Promise((r) => setTimeout(r, 3000));
status = await client.tasks.getStatus(task.id);
}
console.log('Final status:', status.status);
// 4. Read the result from the trajectory
const { trajectory } = await client.tasks.getTrajectory(task.id);
const result = trajectory.find((e) => e.event === 'ResultEvent');
console.log('Result:', result?.data);
```
```python Python theme={null}
import os, time, requests
API = "https://api.mobilerun.ai/v1"
HEADERS = {
"Authorization": f"Bearer {os.environ['MOBILERUN_API_KEY']}",
"Content-Type": "application/json",
}
# 1. Find a device that's ready to accept tasks
devices = requests.get(f"{API}/devices", headers=HEADERS, params={"state": "ready"}).json()
device_id = devices["items"][0]["id"]
# 2. Submit an autonomous task
task = requests.post(
f"{API}/tasks",
headers=HEADERS,
json={
"deviceId": device_id,
"task": "Open Settings and tell me the Android version",
"llmModel": "mobilerun/mobile-agent-fast",
"maxSteps": 50,
},
).json()
task_id = task["id"]
print("Task started:", task_id)
# 3. Poll until the task finishes
while True:
status = requests.get(f"{API}/tasks/{task_id}/status", headers=HEADERS).json()
if status["status"] in ("completed", "failed", "cancelled"):
break
time.sleep(3)
print("Final status:", status["status"])
# 4. Read the result from the trajectory
trajectory = requests.get(f"{API}/tasks/{task_id}/trajectory", headers=HEADERS).json()["trajectory"]
result = next((e for e in trajectory if e["event"] == "ResultEvent"), None)
print("Result:", result["data"] if result else None)
```
```bash cURL theme={null}
# 1. Find a ready device and grab its id
curl -s https://api.mobilerun.ai/v1/devices \
-H "Authorization: Bearer $MOBILERUN_API_KEY" | jq '.items[0].id'
# 2. Submit a task (use the device id from step 1)
curl -s -X POST https://api.mobilerun.ai/v1/tasks \
-H "Authorization: Bearer $MOBILERUN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"deviceId": "",
"task": "Open Settings and tell me the Android version",
"llmModel": "mobilerun/mobile-agent-fast",
"maxSteps": 50
}'
# 3. Check status (use the task id returned in step 2)
curl -s https://api.mobilerun.ai/v1/tasks//status \
-H "Authorization: Bearer $MOBILERUN_API_KEY"
# 4. Once status is "completed", read the result from the trajectory
curl -s https://api.mobilerun.ai/v1/tasks//trajectory \
-H "Authorization: Bearer $MOBILERUN_API_KEY" \
| jq '.trajectory[] | select(.event == "ResultEvent") | .data'
```
The trajectory is returned as `{ "trajectory": [ ... ] }`, which is an array of events. The final
result is the event whose `event` field equals `"ResultEvent"`, and its `data` holds the fields
`success`, `message`, `structured_output`, and `steps`. For data extraction tasks you can pass an
`outputSchema` in JSON Schema form when you create the task, and then `structured_output` is
populated with data matching it. See [Agent configuration](/agent#structured-output) for more.
## Read the result without the trajectory
The trajectory is the full step by step record, which is useful when you want to inspect every
screenshot and action. For most tasks you do not need it, because the status endpoint already
returns the answer. A `GET /tasks/{id}/status` call returns the task outcome directly.
```json theme={null}
{
"status": "completed",
"succeeded": true,
"message": "The Android version on your device is 15.",
"output": null,
"steps": 5
}
```
The `message` field holds the agent's natural language answer. The `output` field holds your
structured data when you submitted an `outputSchema`, and it is `null` otherwise. Because the
polling loop in step 3 already calls this endpoint, you can read `message` and `output` straight
from the final poll and skip the trajectory call entirely.
Use the status endpoint for the answer and for polling. Reach for the trajectory only when you
need the detailed run, such as the screenshots, the manager plan, or each executor action.
## Choosing a model
Pass `llmModel` to pick the model that drives the agent. Use one of the identifiers from the
[cloud model catalog](/agent#model-selection). For example, `mobilerun/mobile-agent-fast` is tuned
for speed and cost, while `mobilerun/mobile-agent-thinking` gives stronger reasoning for complex
flows. Omit the field to use your account default.
## Next steps
All task parameters, including models, vision, reasoning, stealth, memory, and structured
output.
Full REST schemas for every endpoint, with request and response examples.
Drive Mobilerun from Cursor, Claude Desktop, and other MCP clients.
Get notified when tasks change state instead of polling.
Server-side SDK calls, a live `@mobilerun/react` device stream, and status polling with
cancellation.
HMAC verification, replay protection, and secure file downloads.
# Credentials
Source: https://docs.mobilerun.ai/credentials
Securely store and manage authentication credentials for automated app interactions.
The Credentials tab allows you to create and manage secure credentials that your agent can use when automating apps. Store login information, API tokens, and other secrets without exposing them in your prompts.
## Overview
The Credentials tab provides:
* Secure storage for app credentials
* Per-app credential organization
* Encrypted secret management
* Credential attachment for agent tasks
## Security Model
Credentials are always encrypted at rest and in transit. The agent never has access to raw passwords or secrets.
When the agent needs to authenticate:
1. The agent identifies a login field in the app
2. It requests the credential from the secure vault
3. The credential is injected directly into the field
4. The raw value is never exposed to the agent's context
This architecture ensures your secrets remain protected even during automated interactions.
## Credential Structure
Credentials are organized by app package name. Each credential can contain multiple fields:
| Field Type | Description |
| --------------------- | --------------------------------- |
| **Username** | Account username or identifier |
| **Email** | Email address for login |
| **Password** | Account password (encrypted) |
| **API Token** | API keys or access tokens |
| **Phone Number** | Phone number for SMS verification |
| **Two-Factor Secret** | TOTP secret for 2FA codes |
| **Backup Codes** | Recovery codes for account access |
## Creating Credentials
To add credentials for an app:
1. Click **Add Credential**
2. Select the app package name from your [app library](/apps)
3. Enter a credential name (e.g., "Production Account", "Test User")
4. Add the required fields (username, password, etc.)
5. Save the credential
You can create multiple credentials per app. This is useful for testing different account types or environments.
## Managing Credentials
### View Credentials
Your credential list shows:
* App package name
* Credential name
* Field types configured (without revealing values)
* Last modified date
### Edit Credentials
Update credential fields when passwords change or tokens expire. Click on any credential to modify its fields.
### Delete Credentials
Remove credentials that are no longer needed. This action is permanent and cannot be undone.
## Using Credentials in Tasks
Attach credentials to your agent tasks to enable authenticated automation:
### In the Playground
Select the credentials to use before running your task. The agent will have access to inject these credentials when it encounters login screens.
### Via API
Specify credentials in your task configuration:
```json theme={null}
{
"task": "Log into the app and check notifications",
"credentials": [
{
"packageName": "com.example.app",
"credentialNames": ["Production Account"]
}
]
}
```
### Multiple Credentials
You can attach credentials for multiple apps in a single task. The agent will use the appropriate credential based on which app it is interacting with.
## Best Practices
| Practice | Description |
| ------------------------- | ------------------------------------------------------------ |
| **Use descriptive names** | Name credentials clearly (e.g., "QA Test Account") |
| **Separate environments** | Create distinct credentials for dev, staging, and production |
| **Rotate regularly** | Update credentials when passwords change |
| **Minimal access** | Only attach credentials the task actually needs |
# Credits
Source: https://docs.mobilerun.ai/credits
Understand how credits work and what they are used for on Mobilerun.
Credits are the currency used to pay for usage on Mobilerun. 1 credit = \$0.01 USD. You receive credits with your device subscriptions or can top up your balance as needed.
## How Credits Work
Credits are consumed when you use Mobilerun services. Your credit balance decreases as you run tasks.
## Credit Usage
### Agent Execution (LLM Tokens)
Each action the agent takes during task execution consumes credits based on the LLM tokens used. On average, agent steps cost approximately **\~0.5 credits per step**.
The exact cost per step varies depending on the model you select, the length of the conversation context, and whether features like vision or reasoning are enabled. You can monitor your LLM credit consumption in the Billing section of the dashboard.
## Getting Credits
### Device Subscriptions
Each device subscription includes a monthly credit allocation that refreshes every billing cycle:
| Device | Monthly Credits | Price |
| ---------------------------------------------- | --------------- | ----------- |
| [Personal Phone](/device-types#personal-phone) | 250 | \$5/month |
| [Cloud Phone](/device-types#cloud-phone) | 2,500 | \$50/month |
| [Physical Phone](/device-types#physical-phone) | 5,000 | \$150/month |
No base plan is required — sign up for free and add devices as needed.
### Top Up
Purchase additional credits when you need more capacity:
* **\$5 per 500 credits** (one-time purchase)
* Credits are added to your balance immediately
* Top-up credits do not expire
## Monitoring Usage
Track your credit consumption in the Billing section of the dashboard:
* Current credit balance
* Usage breakdown by category
* Historical consumption trends
## Cost Optimization
| Strategy | Description |
| ----------------------------- | ------------------------------------------------------------ |
| **Select appropriate models** | Use faster, cheaper models for simple tasks |
| **Optimize prompts** | Well-written prompts reduce the number of agent steps needed |
# Device Types
Source: https://docs.mobilerun.ai/device-types
Learn about the different device types available on Mobilerun and choose the one that best fits your automation needs.
Mobilerun offers three device types, each designed for different use cases and requirements. Understanding these options will help you select the right infrastructure for your mobile automation workflows.
## Personal Phone
Connect your own physical smartphone to the cloud. Follow the [Connect an iPhone](/guides/connect-iphone) or [Connect an Android](/guides/connect-android) guide depending on your device.
| Feature | Description |
| ---------------- | ------------------------------------------------- |
| **Provisioning** | BYO — bring your own device |
| **Persistence** | Persistent — your device keeps its state |
| **Cost** | \$5/month per device (includes 250 credits/month) |
| **Hardware** | Remote physical (your own device) |
| **Best For** | Quick testing on your own hardware |
Personal phones are ideal when you want to:
* Use your existing hardware for automation
* Maintain persistent device state across sessions
* Test on specific device models you own
Each Personal Phone subscription includes 250 credits per month.
***
## Cloud Phone
A high-performance virtual Android device with dedicated resources, persistent state, and advanced automation capabilities.
| Feature | Description |
| ---------------- | ---------------------------------------------------- |
| **Provisioning** | Dedicated — always-on, reserved for your account |
| **Persistence** | Persistent — full state persistence across sessions |
| **Cost** | \$50/month per device (includes 2,500 credits/month) |
| **Hardware** | Virtual device |
| **Location** | EU |
| **Best For** | Mobile RPA and scalable automation |
Cloud Phones are ideal when you need:
* Persistent app installations and configurations
* Scalable automation with multiple devices
* Profile support for managing multiple identities
* Predictable performance without cold-start delays
Cloud Phones support **profiles**, allowing you to manage multiple device identities from a single subscription.
See the [Cloud Phone Setup](/guides/cloud-phone-setup) guide for setup best practices, including proxy and Google account configuration.
***
## Physical Phone
A dedicated, premium real Android device hosted in the Mobilerun data center, optimized for reliable Mobile RPA operations.
| Feature | Description |
| ---------------- | ----------------------------------------------------- |
| **Provisioning** | Dedicated — always-on, reserved for your account |
| **Persistence** | Persistent — full state persistence across sessions |
| **Cost** | \$150/month per device (includes 5,000 credits/month) |
| **Hardware** | Premium real device |
| **Location** | EU / US |
| **Best For** | Social media automation and stealth operations |
Physical Phones are the right choice when you need:
* Real hardware for maximum realism and stealth
* eSIM support for cellular connectivity
* GPS location spoofing and proxy integration
* Production-grade automation on real devices
See the [Physical Phone Setup](/guides/physical-phone-setup) guide for setup best practices, including proxy, location, and eSIM configuration.
***
## Comparison
| Aspect | Personal Phone | Cloud Phone | Physical Phone |
| -------------------- | --------------- | ----------- | ------------------- |
| **Hardware** | Your own device | Virtual | Premium real device |
| **State** | Persistent | Persistent | Persistent |
| **Deployment** | BYO | Dedicated | Dedicated |
| **Location** | Yours | EU | EU / US |
| **Scalable** | No | Yes | No |
| **eSIM** | No | No | Yes |
| **Profiles** | No | Yes | No |
| **App Support** | Yes | Yes | Yes |
| **Cost** | \$5/mo | \$50/mo | \$150/mo |
| **Included Credits** | 250/mo | 2,500/mo | 5,000/mo |
## Choosing the Right Device Type
Choose this to connect your own physical smartphone for testing on your own hardware.
Choose this for scalable, persistent virtual devices with profile support and dedicated resources.
Choose this when you need real hardware, eSIM, and maximum stealth for production automation.
# Devices
Source: https://docs.mobilerun.ai/devices
View and manage all your devices and monitor active device streams.
The Devices tab provides a centralized overview of all devices associated with your account. Use it to monitor device status, manage your device inventory, and access active streams.
## Overview
The Devices tab displays:
* All devices allocated to your account
* Currently running devices and their streams
* Device status and availability
## Device Inventory
Your device inventory shows every device you have access to, organized by type:
| Device Type | Description |
| ------------------ | ---------------------------------------------------------------------------------------------- |
| **Personal Phone** | Your own physical smartphone connected via the [Mobilerun Portal App](/guides/connect-android) |
| **Cloud Phone** | Dedicated, persistent virtual devices with profile support |
| **Physical Phone** | Premium real Android hardware hosted in the Mobilerun data center |
For detailed information about each device type, including pricing and capabilities, see the [Device Types](/device-types) documentation.
## Device Status
Each device in your inventory displays its current status:
| Status | Description |
| ------------- | --------------------------------------------------------- |
| **Available** | Device is ready to accept new tasks |
| **Running** | Device is actively executing a task |
| **Offline** | Device is not currently connected (physical devices only) |
These are the human readable labels shown in the dashboard. The API and SDK use lowercase state
values instead. The most important one is **`ready`**, which you filter for with `?state=ready` in
REST or with `client.devices.list({ state: ['ready'] })` in the SDK. Other API states include
`creating`, `assigned`, `rebooting`, `resetting`, `terminated`, and `maintenance`.
## Live Streams
When a device is running, you can view its live stream directly from the Devices tab. The stream shows:
* Real-time device screen output
* Current task execution progress
* Agent actions as they occur
Click on any running device to open its stream view.
## Managing Devices
From the Devices tab, you can:
### View Device Details
Click on any device to see:
* Device specifications (model, OS version, screen resolution)
* Current task information (if running)
* Session history
### Connect Your Own Device
With a Personal Phone subscription, connect your own physical Android or iOS device using the [Mobilerun Portal App](/guides/connect-android). Connected devices appear in your inventory alongside cloud-hosted devices.
### Monitor Usage
Track your device utilization:
* Active session time
* Task execution history
# Architecture
Source: https://docs.mobilerun.ai/framework/concepts/architecture
Understanding the Mobilerun multi-agent system for device automation.
## What is Mobilerun?
Mobilerun uses a **multi-agent architecture** where specialized agents work together to complete tasks. Instead of one agent doing everything, different agents handle planning, execution, and computation.
```
MobileAgent (orchestrator)
├── Reasoning Mode: ManagerAgent → ExecutorAgent
└── Direct Mode: FastAgent
```
## Execution Modes
### Reasoning Mode (`reasoning=True`)
Manager creates plans, Executor takes actions. Best for complex multi-step tasks.
```
Goal → Manager (plan) → Executor (action) → Manager (check) → Executor (next) → ...
```
### Direct Mode (`reasoning=False`)
FastAgent executes immediately via XML tool-calling without planning overhead. Best for simple tasks.
```
Goal → FastAgent (XML tool-calling) → Done
```
## Core Agents
### MobileAgent (Orchestrator)
Main coordinator that routes between agents based on mode.
**Location**: `mobilerun/agent/droid/droid_agent.py`
### ManagerAgent (Planner)
Creates strategic plans and breaks tasks into subgoals. Reasoning mode only.
**Location**: `mobilerun/agent/manager/manager_agent.py`
**Workflow**: `prepare_context()` → `get_response()` → `process_response()` → `finalize()`
### ExecutorAgent (Actor)
Executes atomic actions for each subgoal. Reasoning mode only.
**Location**: `mobilerun/agent/executor/executor_agent.py`
**Workflow**: `prepare_context()` → `get_response()` → `process_response()` → `execute()` → `finalize()`
### FastAgent (Direct Executor)
Uses XML tool-calling for device interaction. Direct mode only.
**Location**: `mobilerun/agent/fast_agent/fast_agent.py`
**Available Actions**:
```python theme={null}
click(index), click_at(x, y), click_area(x1, y1, x2, y2),
long_press(index), long_press_at(x, y),
type(text, index), type_secret(secret_id, index),
swipe(coordinate, coordinate2), system_button(button),
wait(duration), open_app(text),
remember(information), complete(success, reason)
```
## Configuration
Configure different LLMs per agent:
```yaml theme={null}
llm_profiles:
manager:
provider: Anthropic
model: claude-sonnet-4
executor:
provider: OpenAI
model: gpt-4o
fast_agent:
provider: GoogleGenAI
model: gemini-3.1-flash-lite-preview
agent:
reasoning: true # Enable Manager/Executor workflow
max_steps: 15 # Maximum execution steps (global)
manager:
vision: true # Send screenshots to Manager
executor:
vision: true # Send screenshots to Executor
fast_agent:
vision: false
parallel_tools: true
```
## When to Use Each Mode
**Use Reasoning Mode for:**
* Multi-step tasks (booking flights, configuring settings)
* Tasks requiring planning and adaptation
* Complex workflows across multiple apps
**Use Direct Mode for:**
* Simple actions (screenshots, sending messages)
* Fast execution without planning overhead
* Well-defined single-step tasks
## Shared State
All agents share `MobileAgentState` for coordination:
* Action history and outcomes
* Error tracking and recovery
* Memory and context
* Current plan and progress
## Quick Reference
| Agent | Role | Best For | Mode | Config Key |
| ------------- | ------------ | ------------------ | --------- | -------------------- |
| MobileAgent | Orchestrator | Entry point | Both | `agent.*` |
| ManagerAgent | Planner | Strategy, recovery | Reasoning | `agent.manager.*` |
| ExecutorAgent | Actor | Action execution | Reasoning | `agent.executor.*` |
| FastAgent | Direct | Simple tasks | Direct | `agent.fast_agent.*` |
# Event Streaming
Source: https://docs.mobilerun.ai/framework/concepts/events-and-workflows
How to consume real-time events from MobileAgent execution.
## Overview
Mobilerun provides **real-time event streaming** that gives you visibility into agent execution as it happens. This allows you to build UIs, logging systems, or monitoring tools that react to agent actions in real-time.
Under the hood, Mobilerun uses [llama-index workflows](https://docs.llamaindex.ai/en/stable/understanding/workflows/) - an event-driven orchestration system that powers the agent architecture.
## Basic Usage
```python theme={null}
from mobilerun.agent.droid import MobileAgent
# Create and run agent
agent = MobileAgent(goal="Open Gmail and check inbox", config=config)
handler = agent.run()
# Stream events in real-time
async for event in handler.stream_events():
if isinstance(event, ManagerPlanDetailsEvent):
print(f"📋 Plan: {event.plan}")
print(f"🎯 Current subgoal: {event.subgoal}")
elif isinstance(event, ExecutorActionEvent):
print(f"⚡ Action: {event.description}")
print(f"💭 Thought: {event.thought}")
elif isinstance(event, ScreenshotEvent):
save_screenshot(event.screenshot, "screenshot.png")
elif isinstance(event, FastAgentResponseEvent):
if event.code:
print(f"🔧 Tool calls: {event.code}")
if event.thought:
print(f"💭 Thought: {event.thought}")
# Wait for final result
result = await handler
print(f"✅ Success: {result.success}")
print(f"📝 Reason: {result.reason}")
```
## Event Types
Used for workflow coordination between MobileAgent and its child agents.
```python theme={null}
# Main workflow
class FastAgentExecuteEvent(Event):
instruction: str
class FastAgentResultEvent(Event):
success: bool
reason: str
instruction: str
class FinalizeEvent(Event):
success: bool
reason: str
class ResultEvent(StopEvent):
success: bool
reason: str
steps: int
structured_output: BaseModel | None
# Manager/Executor coordination
class ManagerInputEvent(Event): pass
class ManagerPlanEvent(Event):
plan: str
current_subgoal: str
thought: str
answer: str = ""
success: bool | None = None
class ExecutorInputEvent(Event):
current_subgoal: str
class ExecutorResultEvent(Event):
action: Dict
outcome: bool
error: str
summary: str
# External user message events
class ExternalUserMessageAppliedEvent(Event):
message_ids: List[str]
consumer: str
step_number: int
class ExternalUserMessageDroppedEvent(Event):
message_ids: List[str]
reason: str
step_number: int
```
Internal to ManagerAgent, streamed to frontend/logging.
```python theme={null}
class ManagerContextEvent(Event): pass
class ManagerResponseEvent(Event):
response: str
usage: Optional[UsageResult] = None
class ManagerPlanDetailsEvent(Event):
plan: str
subgoal: str
thought: str
answer: str = ""
memory_update: str = ""
progress_summary: str = ""
success: bool | None = None
full_response: str = ""
```
Internal to ExecutorAgent, streamed to frontend/logging.
```python theme={null}
class ExecutorContextEvent(Event):
subgoal: str
class ExecutorResponseEvent(Event):
response: str
usage: Optional[UsageResult] = None
class ExecutorActionEvent(Event):
action_json: str
thought: str
description: str
full_response: str = ""
class ExecutorActionResultEvent(Event):
action: Dict
success: bool
error: str
summary: str
thought: str = ""
full_response: str = ""
```
Internal to FastAgent, used in direct execution mode.
```python theme={null}
class FastAgentInputEvent(Event):
pass
class FastAgentResponseEvent(Event):
thought: str
code: Optional[str] = None
usage: Optional[UsageResult] = None
class FastAgentToolCallEvent(Event):
tool_calls_repr: str
class FastAgentOutputEvent(Event):
output: str
class FastAgentEndEvent(Event):
success: bool
reason: str
tool_call_count: int = 0
```
```python theme={null}
# Common events (mobilerun/agent/common/events.py)
class ScreenshotEvent(Event):
screenshot: bytes
class RecordUIStateEvent(Event):
ui_state: list[Dict[str, Any]]
class ToolExecutionEvent(Event):
tool_name: str
tool_args: Dict[str, Any]
success: bool
summary: str
# Telemetry events (when enabled)
class MobileAgentInitEvent(TelemetryEvent):
goal: str
llms: Dict[str, str]
tools: str
max_steps: int
timeout: int
vision: Dict[str, bool]
reasoning: bool
enable_tracing: bool
debug: bool
save_trajectories: str
runtype: str
custom_prompts: Optional[Dict[str, str]]
class PackageVisitEvent(TelemetryEvent):
package_name: str
activity_name: str
step_number: int
class MobileAgentFinalizeEvent(TelemetryEvent):
success: bool
reason: str
steps: int
unique_packages_count: int
unique_activities_count: int
# Usage tracking
class UsageResult(BaseModel):
request_tokens: int
response_tokens: int
total_tokens: int
requests: int
```
## Common Patterns
### Building a Live UI
```python theme={null}
async def run_with_ui(goal: str):
agent = MobileAgent(goal=goal, config=config)
handler = agent.run()
async for event in handler.stream_events():
if isinstance(event, ManagerPlanDetailsEvent):
ui.update_plan(event.plan)
ui.update_current_step(event.subgoal)
elif isinstance(event, ExecutorActionEvent):
ui.add_action_log(event.description, event.thought)
elif isinstance(event, ScreenshotEvent):
ui.update_screenshot(event.screenshot)
result = await handler
ui.show_completion(result.success, result.reason)
```
### Tracking Token Usage
```python theme={null}
async def track_token_usage(goal: str):
agent = MobileAgent(goal=goal, config=config)
handler = agent.run()
total_tokens = 0
total_requests = 0
async for event in handler.stream_events():
# Check for events that contain usage information
if hasattr(event, 'usage') and event.usage:
total_tokens += event.usage.total_tokens
total_requests += event.usage.requests
print(f"LLM call - Input: {event.usage.request_tokens}, "
f"Output: {event.usage.response_tokens}, "
f"Total: {event.usage.total_tokens}")
result = await handler
print(f"\n📊 Total tokens used: {total_tokens}")
print(f"📊 Total LLM requests: {total_requests}")
```
### Logging and Monitoring
```python theme={null}
import logging
logger = logging.getLogger("mobilerun.monitor")
async def monitor_execution(goal: str):
agent = MobileAgent(goal=goal, config=config)
handler = agent.run()
start_time = time.time()
action_count = 0
async for event in handler.stream_events():
if isinstance(event, ExecutorActionEvent):
action_count += 1
logger.info(f"Action {action_count}: {event.description}")
elif isinstance(event, FastAgentOutputEvent):
logger.info(f"Tool execution result: {event.output}")
result = await handler
duration = time.time() - start_time
logger.info(f"Task completed in {duration:.2f}s with {action_count} actions")
logger.info(f"Result: {result.success} - {result.reason}")
```
## Notes
### Event Streaming Behavior
* Events are **streamed in real-time** as the agent executes
* Not all events are emitted in every execution (depends on mode and actions)
* All events are **Pydantic models** with full type safety
* The `handler` object is **async** - always use `await handler` to get the final result
### Event Emission by Mode
**Reasoning Mode** (`reasoning=True`) emits:
* Coordination: `ManagerInputEvent`, `ManagerPlanEvent`, `ExecutorInputEvent`, `ExecutorResultEvent`
* Internal Manager: `ManagerContextEvent`, `ManagerResponseEvent`, `ManagerPlanDetailsEvent`
* Internal Executor: `ExecutorContextEvent`, `ExecutorResponseEvent`, `ExecutorActionEvent`, `ExecutorActionResultEvent`
* Tool execution: `ToolExecutionEvent` (after every tool dispatch)
* Visual: `ScreenshotEvent`, `RecordUIStateEvent` (when enabled)
**Direct Mode** (`reasoning=False`) emits:
* Coordination: `FastAgentExecuteEvent`, `FastAgentResultEvent`
* Internal FastAgent: `FastAgentInputEvent`, `FastAgentResponseEvent`, `FastAgentToolCallEvent`, `FastAgentOutputEvent`, `FastAgentEndEvent`
* Tool execution: `ToolExecutionEvent` (after every tool dispatch)
* Visual: `ScreenshotEvent`, `RecordUIStateEvent` (when enabled)
**All Modes** emit:
* Finalization: `FinalizeEvent`, `ResultEvent`
* Telemetry: `MobileAgentInitEvent`, `PackageVisitEvent`, `MobileAgentFinalizeEvent` (when telemetry enabled)
### Event Categories
**Coordination Events** - Used for workflow routing between agents (minimal data)
* Located in `mobilerun/agent/droid/events.py`
* Examples: `ManagerPlanEvent`, `ExecutorResultEvent`, `ExternalUserMessageAppliedEvent`
**Internal Events** - Used for streaming to frontend/logging (full debug data)
* Located in agent-specific event files
* Examples: `ManagerPlanDetailsEvent`, `ExecutorActionEvent`, `FastAgentResponseEvent`
**Common Events** - Emitted during execution (for screenshots, UI state recording, and tool tracking)
* Located in `mobilerun/agent/common/events.py`
* Examples: `ScreenshotEvent`, `RecordUIStateEvent`, `ToolExecutionEvent`
**Telemetry Events** - Captured for analytics (when enabled)
* Located in `mobilerun/telemetry/events.py`
* Examples: `MobileAgentInitEvent`, `PackageVisitEvent`, `MobileAgentFinalizeEvent`
## Learn More
* [LlamaIndex Workflows](https://docs.llamaindex.ai/en/stable/understanding/workflows/) - The underlying orchestration system
# Prompt Templates
Source: https://docs.mobilerun.ai/framework/concepts/prompts
Customizing agent behavior with Jinja2 prompt templates.
## Overview
Mobilerun uses **Jinja2 templates** for agent prompts. You can customize agent behavior by passing custom template strings to `MobileAgent`:
```python theme={null}
custom_prompts = {
"manager_system": "Your Jinja2 template here...",
"executor_system": "Another template...",
"fast_agent_system": "...",
"fast_agent_user": "..."
}
agent = MobileAgent(
goal="Send an email",
config=config,
prompts=custom_prompts # Pass template strings, not file paths
)
```
**Important**: The `prompts` parameter accepts Jinja2 **template strings**, not file paths.
## How It Works
1. `MobileAgent` creates a `PromptResolver` with your custom prompts
2. Each agent checks if you provided a custom template for its key (e.g., "manager\_system")
3. If found: uses your custom template
4. If not found: loads the default template from Mobilerun's built-in files
5. Templates are rendered with context variables specific to each agent
## Available Prompt Keys
| Key | Agent | When Used |
| ------------------- | --------- | ----------------------------------------------- |
| `manager_system` | Manager | Planning and reasoning (only in reasoning mode) |
| `executor_system` | Executor | Action selection (only in reasoning mode) |
| `fast_agent_system` | FastAgent | Direct execution (always used) |
| `fast_agent_user` | FastAgent | Task input formatting (always used) |
## Context Variables
Each agent has access to different variables in its templates:
### Manager
* `instruction` - User's goal
* `device_date` - Current device date/time
* `app_card` - App-specific guidance (empty if none available)
* `error_history` - List of recent failed actions with details
* `custom_tools_descriptions` - Custom tool documentation
* `available_secrets` - Available credential IDs
* `variables` - Custom variables passed to MobileAgent
* `output_schema` - Pydantic model schema (if provided)
### Executor
* `instruction` - User's goal
* `device_state` - Current UI tree
* `plan` - Current plan from Manager
* `subgoal` - Current subgoal from Manager
* `progress_status` - Cumulative progress summary
* `atomic_actions` - Available actions (includes custom tools)
* `action_history` - Recent actions with outcomes
* `available_secrets` - Available credential IDs
* `variables` - Custom variables passed to MobileAgent
### FastAgent
**System prompt:**
* `tool_descriptions` - Available tool signatures
* `available_secrets` - Credential IDs
* `variables` - Custom variables
* `output_schema` - Output model schema (if provided)
**User prompt:**
* `goal` - Task description
* `variables` - Custom variables
## Example: Custom Manager Prompt
```python theme={null}
custom_prompts = {
"manager_system": """
You are a mobile automation planning agent.
Task: {{ instruction }}
Date: {{ device_date }}
{% if app_card %}
App guidance:
{{ app_card }}
{% endif %}
{% if error_history %}
Recent errors (you may be stuck):
{% for error in error_history %}
- Action: {{ error.action }}
Error: {{ error.error }}
{% endfor %}
{% endif %}
{% if custom_tools_descriptions %}
Custom tools:
{{ custom_tools_descriptions }}
{% endif %}
{% if variables.domain %}
Domain: {{ variables.domain }}
{% endif %}
Output format:
Your reasoning
1. First step
2. Second step
3. DONE
Or if complete:
Task is done. Answer: ...
"""
}
agent = MobileAgent(
goal="Send an email",
config=config,
prompts=custom_prompts,
variables={"domain": "finance"}
)
```
## Example: Using Custom Variables
Custom variables let you inject dynamic context into prompts:
```python theme={null}
custom_prompts = {
"manager_system": """
Task: {{ instruction }}
{% if variables.budget %}
Budget limit: ${{ variables.budget }}
{% endif %}
{% if variables.priority %}
Priority: {{ variables.priority }}
{% endif %}
Guidelines:
{% for rule in variables.rules %}
- {{ rule }}
{% endfor %}
"""
}
agent = MobileAgent(
goal="Buy a phone",
config=config,
prompts=custom_prompts,
variables={
"budget": 1000,
"priority": "high",
"rules": ["Check reviews", "Compare prices", "Use coupons"]
}
)
```
## Jinja2 Syntax Reference
### Variables
```jinja2 theme={null}
{{ instruction }}
{{ variables.my_var }}
```
### Conditionals
```jinja2 theme={null}
{% if app_card %}
{{ app_card }}
{% endif %}
{% if error_history %}
You have {{ error_history | length }} errors
{% endif %}
```
### Loops
```jinja2 theme={null}
{% for error in error_history %}
- {{ error.action }}: {{ error.error }}
{% endfor %}
```
### Filters
```jinja2 theme={null}
{{ instruction | upper }}
{{ available_secrets | join(', ') }}
{{ error_history | length }}
```
## Best Practices
### 1. Use Clear Structure
```jinja2 theme={null}
{{ instruction }}
1. Rule one
2. Rule two
Expected format
```
### 2. Handle Missing Data with Conditionals
```jinja2 theme={null}
{% if app_card %}
{{ app_card }}
{% else %}
No app-specific guidance available
{% endif %}
```
### 3. Document Expected Variables
```jinja2 theme={null}
{# Expected variables:
- instruction: str - User's goal
- device_date: str - Current date/time
- app_card: str - App guidance (may be empty)
#}
```
### 4. Use Variables for Dynamic Behavior
```jinja2 theme={null}
{% if variables.strict_mode %}
Follow instructions exactly. Do not make assumptions.
{% endif %}
```
## Complete Example
```python theme={null}
from mobilerun import MobileAgent
from mobilerun.config_manager import MobileConfig
# E-commerce automation with custom prompts
ecommerce_prompts = {
"manager_system": """
You are an e-commerce automation specialist.
Task: {{ instruction }}
Budget: ${{ variables.budget }}
{% if app_card %}
App info:
{{ app_card }}
{% endif %}
{% if error_history %}
Errors encountered:
{% for error in error_history %}
- {{ error.action }}: {{ error.summary }} - {{ error.error }}
{% endfor %}
Consider changing your approach.
{% endif %}
Rules:
1. Verify product names exactly
2. Check prices before purchasing
3. Store order confirmations in memory
4. Never exceed budget
Output:
Your reasoning
1. Step
2. DONE
"""
}
config = MobileConfig()
agent = MobileAgent(
goal="Buy iPhone 15 Pro from Amazon",
config=config,
prompts=ecommerce_prompts,
variables={"budget": 1200}
)
result = await agent.run()
```
## Key Points
* Pass Jinja2 template **strings** (not file paths) to `MobileAgent(prompts={...})`
* Each agent has different available variables in its template
* Use `variables` parameter to inject custom context
* Templates are rendered at runtime with current state
* If no custom prompt provided, default templates are used
* Supports full Jinja2 syntax (conditionals, loops, filters)
# Shared State
Source: https://docs.mobilerun.ai/framework/concepts/shared-state
MobileAgentState - the coordination mechanism for multi-agent workflow communication.
## What is Shared State?
**MobileAgentState** is a Pydantic model that serves as the **central coordination mechanism** for Mobilerun's multi-agent workflow. It's a shared data structure that all agents (Manager, Executor, FastAgent) can read from and write to.
Shared state enables:
* **Cross-agent communication**: Agents share information about actions, results, and errors
* **Progress tracking**: Step counts, action history, visited apps/screens
* **Memory management**: Agent memory, custom variables, user session data
* **Error coordination**: Error flags, escalation thresholds, error descriptions
**Key insight**: Shared state replaces complex message passing. Instead of sending data back and forth, agents update a single shared object.
## Core State Fields
```python theme={null}
class MobileAgentState(BaseModel):
# Task context
instruction: str = "" # Original task
step_number: int = 0 # Current step
# Device state
formatted_device_state: str = "" # Human-readable state
current_package_name: str = "" # Current app
current_activity_name: str = "" # Current screen
# Action tracking
action_history: List[Dict] = [] # All actions taken
action_outcomes: List[bool] = [] # Success/failure
summary_history: List[str] = [] # Action summaries
# Memory
manager_memory: str = "" # Manager's planning notes (append-only)
fast_memory: List[str] = [] # FastAgent remember() items (max 10)
# Planning (Manager)
plan: str = "" # Current plan
current_subgoal: str = "" # Current subgoal
answer: str = "" # Final answer (manager completion or complete() tool)
# Completion (set by complete() tool)
finished: bool = False # Whether task is done
success: Optional[bool] = None # Whether task succeeded
# Error handling
error_flag_plan: bool = False # Signal error to Manager
error_descriptions: List[str] = [] # Error messages
err_to_manager_thresh: int = 2 # Consecutive errors before escalation
# Message history (for stateful agents)
message_history: List[ChatMessage] = [] # Preserves ThinkingBlock, ImageBlock, etc.
# External user messages (mid-run injection queue)
pending_user_messages: List[QueuedUserMessage] = [] # Queued external messages
workflow_completed: bool = False # Set True at finalization
# Custom variables
custom_variables: Dict = {} # User-defined data
```
# App Instruction Cards
Source: https://docs.mobilerun.ai/framework/features/app-cards
App cards give your agents app-specific knowledge to operate apps more effectively. They automatically load when agents work with specific apps, improving success rates for navigation and complex tasks.
## What Are App Cards?
App cards are **app-specific instruction guides** that teach agents how to use apps effectively. Think of them as cheat sheets that help your agent understand:
* How to navigate the app's UI
* Where to find buttons and features
* App-specific shortcuts and gestures
* Search syntax and filters (for apps like Gmail)
* Common workflows and best practices
**Example:** When your agent opens Gmail, it automatically loads the Gmail app card and learns that:
* The compose button is at the bottom-right
* Search supports filters like `from:sender@email.com` or `has:attachment`
* Swiping right archives emails, swiping left deletes them
This knowledge helps agents complete tasks faster and more reliably.
***
## Why Use App Cards?
**Without app cards:**
* Agents guess how to navigate unfamiliar apps
* Trial-and-error wastes time and tokens
* Success rates drop for complex workflows
**With app cards:**
* ✅ Agents know exactly where to find features
* ✅ First-attempt success for common tasks
* ✅ Reduced token usage (less exploration needed)
* ✅ Better handling of app-specific quirks
***
## Quick Start
Mobilerun includes a sample Gmail app card to demonstrate how app cards work:
```bash theme={null}
# App cards are enabled by default
mobilerun run "Send an email to john@example.com" --reasoning
```
When the agent opens Gmail, the Gmail app card automatically loads and guides the workflow.
**Sample app card included:**
* **Gmail** (`com.google.android.gm`) - Email navigation, search, composition
You can use this as a template to create cards for other apps (see "Creating Custom App Cards" below).
***
## How App Cards Work
### Automatic Loading
1. **Detection:** Agent detects the current foreground app (e.g., Gmail)
2. **Loading:** Mobilerun loads the app card for that package name
3. **Injection:** App card content is added to the agent's prompt
4. **Guidance:** Agent uses the instructions to make better decisions
**Technical note:** App cards are loaded asynchronously and cached in memory. Loading happens in the background and doesn't block agent execution.
### When Are They Used?
App cards are used by the **Manager Agent** when running in reasoning mode (`--reasoning` flag or `reasoning: true` in config):
```bash theme={null}
# App cards enabled (Manager uses them for planning)
mobilerun run "Archive all unread emails" --reasoning
# App cards not used (direct execution mode)
mobilerun run "Tap the button"
```
***
## Creating Custom App Cards
Want to add an app card for your favorite app? Here's how:
### Step 1: Find the Package Name
```bash theme={null}
# Get all apps
adb shell pm list packages
# Or search for a specific app
adb shell pm list packages | grep keyword
```
**Common package names:**
* Chrome: `com.android.chrome`
* WhatsApp: `com.whatsapp`
* Instagram: `com.instagram.android`
* YouTube: `com.google.android.youtube`
### Step 2: Create the Files
Create the app cards directory and files:
```bash theme={null}
mkdir -p config/app_cards
touch config/app_cards/app_cards.json
touch config/app_cards/chrome.md
```
**Example structure:**
```markdown theme={null}
# Chrome App Guide
## Navigation
- Address bar at the top for entering URLs
- Three-dot menu (top-right) for settings and history
- Tabs button (top-right) to switch between tabs
## Search
- Type queries directly in the address bar
- Use voice search via the microphone icon
## Common Actions
- **New Tab**: Tap the tabs button → Plus icon
- **Close Tab**: Swipe tab away in tab switcher
- **Refresh**: Pull down from the top of the page
- **Bookmarks**: Three-dot menu → Bookmarks
## Tips
- Incognito mode available via three-dot menu
- Downloads accessible via three-dot menu → Downloads
```
### Step 3: Register the App Card
Add your app mapping to `config/app_cards/app_cards.json`:
```json theme={null}
{
"com.google.android.gm": "gmail.md",
"com.android.chrome": "chrome.md"
}
```
**Using subdirectories:**
```json theme={null}
{
"com.whatsapp": "social/whatsapp.md",
"com.instagram.android": "social/instagram.md"
}
```
### Step 4: Test
```bash theme={null}
mobilerun run "Open Chrome and search for mobilerun" --reasoning --debug
```
Look for this log message:
```
Loaded app card for com.android.chrome from config/app_cards/chrome.md
```
***
## Configuration
App cards are enabled by default and load from `config/app_cards/`:
```yaml theme={null}
agent:
app_cards:
enabled: true
app_cards_dir: config/app_cards
```
**To disable:**
```yaml theme={null}
agent:
app_cards:
enabled: false
```
***
## App Card Best Practices
### Content Guidelines
**Do:**
* Be concise and actionable
* Focus on UI patterns and workflows
* Include search syntax and special features
* Mention common pitfalls or quirks
* Use bullet points and clear headings
**Don't:**
* Write essays or lengthy explanations
* Describe every single feature
* Include information that changes frequently (version-specific details)
* Duplicate general Android knowledge (agents already know how to tap, swipe, etc.)
### Example: Good vs Bad
**❌ Bad (too verbose):**
```markdown theme={null}
Gmail is an email application developed by Google. It has many features
including the ability to send and receive emails. To compose an email,
you need to first understand that Gmail uses a material design interface
with a floating action button, which is a circular button...
```
**✅ Good (concise and actionable):**
```markdown theme={null}
## Composing Emails
- Tap the floating compose button (bottom-right)
- Fill recipient, subject, and body
- Send via paper plane icon (top-right)
```
***
## Troubleshooting
### App Card Not Loading
**Check these:**
1. **Is the package name correct?**
```bash theme={null}
adb shell dumpsys window windows | grep -E 'mCurrentFocus'
```
2. **Is the mapping correct in app\_cards.json?**
```json theme={null}
{
"com.your.app": "yourapp.md"
}
```
3. **Does the markdown file exist?**
```bash theme={null}
ls config/app_cards/yourapp.md
```
4. **Are app cards enabled?**
```yaml theme={null}
agent:
app_cards:
enabled: true
```
5. **Are you using reasoning mode?**
```bash theme={null}
mobilerun run "command" --reasoning
```
### Debug Mode
Run with `--debug` to see app card loading:
```bash theme={null}
mobilerun run "Open Gmail" --reasoning --debug
```
Look for these log messages:
```
Loaded app_cards.json with 2 entries
Loaded app card for com.google.android.gm from config/app_cards/gmail.md
```
***
## Related Documentation
* [CLI Usage](/framework/guides/cli) - Mobilerun CLI command reference
* [Configuration](/framework/sdk/configuration) - Configuration system details
* [Agent Architecture](/framework/concepts/architecture) - How agents work
* [Manager Agent](/framework/concepts/architecture#manageragent-planner) - Agent that uses app cards
***
**Help your agents become app experts with well-crafted app cards!**
# Credential Management
Source: https://docs.mobilerun.ai/framework/features/credentials
Extend Mobilerun with secure credential management
## Overview
Secure storage for passwords, API keys, and tokens.
* Stored in YAML files or in-memory dicts
* Never logged or exposed
* Auto-injected as `type_secret` action
* Simple string or dict format
## Quick Start
### Method 1: In-Memory (Recommended for SDK)
```python theme={null}
import asyncio
from mobilerun import MobileAgent, MobileConfig
async def main():
# Define credentials directly
credentials = {
"MY_PASSWORD": "secret123",
"API_KEY": "sk-1234567890"
}
config = MobileConfig()
agent = MobileAgent(
goal="Login to my app",
config=config,
credentials=credentials # Pass directly
)
result = await agent.run()
print(result.success)
asyncio.run(main())
```
### Method 2: YAML File
1. **Create credentials file:**
```yaml theme={null}
# credentials.yaml
secrets:
# Dict format (recommended)
MY_PASSWORD:
value: "your_password_here"
enabled: true
GMAIL_PASSWORD:
value: "gmail_pass_123"
enabled: true
# Simple string format (auto-enabled)
API_KEY: "sk-1234567890abcdef"
# Disabled secret
OLD_PASSWORD:
value: "old_pass"
enabled: false # Not loaded
```
2. **Enable in config.yaml:**
```yaml theme={null}
# config.yaml
credentials:
enabled: true
file_path: config/credentials.yaml
```
3. **Use in code:**
```python theme={null}
from mobilerun import MobileAgent, MobileConfig
# Config loads credentials from file
config = MobileConfig.from_yaml("config.yaml")
agent = MobileAgent(
goal="Login to Gmail",
config=config # Credentials loaded automatically
)
```
***
## How Agents Use Credentials
When credentials are provided, the `type_secret` action is **automatically available**:
### Executor/Manager Mode
```json theme={null}
{
"action": "type_secret",
"secret_id": "MY_PASSWORD",
"index": 5
}
```
### FastAgent Mode
```xml theme={null}
MY_PASSWORD
5
```
The agent never sees the actual value - only the secret ID.
***
## Example: Login Automation
```python theme={null}
import asyncio
from mobilerun import MobileAgent, MobileConfig
async def main():
credentials = {
"EMAIL_USER": "user@example.com",
"EMAIL_PASS": "secret_password"
}
config = MobileConfig()
agent = MobileAgent(
goal="Open Gmail and login with my credentials",
config=config,
credentials=credentials
)
result = await agent.run()
print(f"Success: {result.success}")
asyncio.run(main())
```
**What the agent does:**
1. Opens Gmail: `open_app("Gmail")`
2. Clicks email field: `click(index=3)`
3. Types email: `type("user@example.com", index=3)`
4. Clicks password field: `click(index=5)`
5. Types password securely: `type_secret("EMAIL_PASS", index=5)`
6. Clicks login: `click(index=7)`
## Credentials vs Variables
| Feature | Credentials | Variables |
| ------------ | ---------------------- | ------------------ |
| **Purpose** | Passwords, API keys | Non-sensitive data |
| **Storage** | YAML or in-memory | In-memory only |
| **Logging** | Never logged | May appear in logs |
| **Access** | Via `type_secret` tool | In shared state |
| **Security** | Protected | No protection |
**Example: Using Variables**
```python theme={null}
variables = {
"target_email": "john@example.com",
"subject_line": "Monthly Report"
}
agent = MobileAgent(
goal="Compose email to {{target_email}}",
config=config,
variables=variables # Non-sensitive
)
```
***
## Troubleshooting
### Error: Credential manager not initialized
**Solution:**
```yaml theme={null}
# config.yaml
credentials:
enabled: true # Must be true
file_path: config/credentials.yaml
```
Or:
```python theme={null}
agent = MobileAgent(..., credentials={"PASSWORD": "secret"})
```
### Error: Secret 'X' not found
**Check available secrets:**
```python theme={null}
from mobilerun.credential_manager import FileCredentialManager
cm = FileCredentialManager("config/credentials.yaml")
print(await cm.get_keys())
```
**Verify in YAML:**
```yaml theme={null}
secrets:
X:
value: "your_value"
enabled: true # Must be true
```
***
## Custom Credential Managers
Extend `CredentialManager` for custom secret storage:
```python theme={null}
from mobilerun.credential_manager import CredentialManager
class MyCredentialManager(CredentialManager):
def __init__(self, api_key):
self.api_key = api_key
async def resolve_key(self, key: str) -> str:
# Implement your own credential retrieval logic
return await fetch_from_service(key, self.api_key)
async def get_keys(self) -> list[str]:
# Return list of available credential keys
return await fetch_available_keys(self.api_key)
# Use it
credentials = MyCredentialManager(api_key="...")
agent = MobileAgent(goal="Login", config=config, credentials=credentials)
```
Implement any custom secret storage backend.
***
## Related
See [Configuration Guide](/framework/sdk/configuration) for credential setup.
See [Custom Variables](/framework/features/custom-variables) for non-sensitive data.
# Custom Tools
Source: https://docs.mobilerun.ai/framework/features/custom-tools
Extend Mobilerun with custom Python functions
## Overview
Custom tools are Python functions that extend agent capabilities beyond built-in atomic actions (click, type, swipe).
**Use cases:**
* External API calls (webhooks, REST services)
* Data processing and calculations
* Database operations
* Domain-specific logic
***
## Quick Start
### Basic Example
Simple custom tool without device access:
```python theme={null}
import asyncio
from mobilerun import MobileAgent, MobileConfig
def calculate_tax(amount: float, rate: float, **kwargs) -> str:
"""Calculate tax for a given amount."""
tax = amount * rate
total = amount + tax
return f"Tax: ${tax:.2f}, Total: ${total:.2f}"
custom_tools = {
"calculate_tax": {
"parameters": {
"amount": {"type": "number", "required": True},
"rate": {"type": "number", "required": True},
},
"description": "Calculate tax for a given amount and rate",
"function": calculate_tax
}
}
async def main():
config = MobileConfig()
agent = MobileAgent(
goal="Calculate tax for $100 at 8% rate",
config=config,
custom_tools=custom_tools
)
result = await agent.run()
print(result.success, result.reason)
asyncio.run(main())
```
***
## Tool Structure
All custom tools follow this format:
```python theme={null}
custom_tools = {
"tool_name": {
"parameters": { # Parameter definitions
"arg1": {"type": "string", "required": True},
"arg2": {"type": "string", "required": True},
},
"description": "Tool description...", # For LLM prompt
"function": callable_function # Python function
}
}
```
**Function signature:**
```python theme={null}
async def tool_name(arg1: type, arg2: type, *, ctx: ActionContext, **kwargs) -> str:
"""
Args:
arg1: Your parameter
arg2: Another parameter
ctx: ActionContext (injected automatically by the registry)
"""
# Implementation
return "result"
```
**Key points:**
* List only user parameters in `"parameters"` (not `ctx`)
* `ctx` (an `ActionContext` instance) is injected automatically as a keyword argument
* Access device via `ctx.driver`, shared state via `ctx.shared_state`, credentials via `ctx.credential_manager`
* Use `**kwargs` for forward compatibility
* Return type should be `str`
***
## Using ActionContext
Access the device and state via the `ctx` parameter (an `ActionContext` instance injected automatically):
```python theme={null}
async def screenshot_and_count(*, ctx, **kwargs) -> str:
"""Take screenshot and count UI elements."""
# Take screenshot via the driver
screenshot = await ctx.driver.screenshot()
# Get UI state via the state provider
ui_state = await ctx.state_provider.get_state()
element_count = len(ui_state.elements) if ui_state else 0
return f"Screenshot taken. Found {element_count} UI elements"
custom_tools = {
"screenshot_and_count": {
"parameters": {},
"description": "Take screenshot and count UI elements on screen",
"function": screenshot_and_count
}
}
```
**Available via `ctx`:**
* `ctx.driver` - `DeviceDriver` for raw device I/O (screenshot, tap, swipe, etc.)
* `ctx.state_provider` - `StateProvider` to fetch/parse UI state
* `ctx.ui` - Current `UIState` (refreshed each step)
* `ctx.shared_state` - `MobileAgentState` for agent coordination
* `ctx.credential_manager` - `CredentialManager` for secrets
***
## Accessing Shared State
Access agent state via `ctx.shared_state`:
```python theme={null}
async def check_action_history(action_name: str, *, ctx, **kwargs) -> str:
"""Check if action was recently performed."""
shared_state = ctx.shared_state
# Check recent actions
recent_actions = shared_state.action_history[-5:]
already_done = any(a.get("action") == action_name for a in recent_actions)
if already_done:
return f"Action '{action_name}' was already performed recently"
# Check step count
if shared_state.step_number > 10:
return "Warning: Task taking too many steps"
# Access fast memory
if any("skip_validation" in m for m in shared_state.fast_memory):
return "Validation skipped per memory"
return f"Action '{action_name}' not yet performed"
custom_tools = {
"check_action_history": {
"parameters": {
"action_name": {"type": "string", "required": True},
},
"description": "Check if a specific action was recently performed in agent history",
"function": check_action_history
}
}
```
**MobileAgentState fields:**
* `step_number` - Current execution step
* `action_history` - List of executed actions
* `action_outcomes` - Success/failure per action
* `manager_memory` - Manager planning notes (append-only string)
* `fast_memory` - FastAgent remember() items (list of strings, max 10)
* `custom_variables` - User-provided variables
* `visited_packages` - Apps visited
* `current_package_name` - Current app package
* `plan` - Current Manager plan
* More in `mobilerun/agent/droid/state.py`
***
## Common Patterns
### API Integration
```python theme={null}
import requests
def fetch_weather(city: str, **kwargs) -> str:
"""Fetch weather data from API."""
try:
# Using OpenWeatherMap API example
api_key = "your_api_key"
url = f"https://api.openweathermap.org/data/2.5/weather?q={city}&appid={api_key}"
response = requests.get(url, timeout=10)
response.raise_for_status()
data = response.json()
temp = data["main"]["temp"] - 273.15 # Convert to Celsius
weather = data["weather"][0]["description"]
return f"Weather in {city}: {weather}, {temp:.1f}°C"
except Exception as e:
return f"Error: {str(e)}"
custom_tools = {
"fetch_weather": {
"parameters": {
"city": {"type": "string", "required": True},
},
"description": "Fetch current weather data for a given city",
"function": fetch_weather
}
}
```
### Database Query
```python theme={null}
import sqlite3
def query_database(query: str, **kwargs) -> str:
"""Query local database."""
try:
conn = sqlite3.connect("app.db")
cursor = conn.execute(query)
results = cursor.fetchall()
conn.close()
return f"Found {len(results)} results"
except Exception as e:
return f"Database error: {str(e)}"
custom_tools = {
"query_database": {
"parameters": {
"query": {"type": "string", "required": True},
},
"description": "Execute SQL query on local database and return results",
"function": query_database
}
}
```
### Async Operations
```python theme={null}
import aiohttp
async def fetch_async(url: str, **kwargs) -> str:
"""Fetch data asynchronously."""
try:
async with aiohttp.ClientSession() as session:
async with session.get(url, timeout=10) as response:
data = await response.text()
return f"Fetched {len(data)} bytes from {url}"
except Exception as e:
return f"Error: {str(e)}"
custom_tools = {
"fetch_async": {
"parameters": {
"url": {"type": "string", "required": True},
},
"description": "Asynchronously fetch data from a URL",
"function": fetch_async
}
}
```
***
## Best Practices
### 1. Clear Descriptions
Write descriptive, specific descriptions:
```python theme={null}
# Good
"description": "Send POST request to webhook URL with JSON data payload"
# Bad
"description": "Send webhook"
```
### 2. Error Handling
Always catch exceptions:
```python theme={null}
def robust_tool(url: str, **kwargs) -> str:
try:
response = requests.get(url, timeout=10)
response.raise_for_status()
return f"Success: {response.status_code}"
except requests.Timeout:
return "Error: Request timed out"
except requests.RequestException as e:
return f"Error: {str(e)}"
except Exception as e:
return f"Unexpected error: {str(e)}"
```
### 3. Argument Validation
Validate inputs before processing:
```python theme={null}
def validated_tool(count: int, **kwargs) -> str:
if not isinstance(count, int):
return "Error: count must be integer"
if count < 0 or count > 100:
return "Error: count must be 0-100"
return f"Processed {count} items"
```
### 4. Logging
Use Python logging for debugging:
```python theme={null}
import logging
logger = logging.getLogger("mobilerun")
def logged_tool(data: str, **kwargs) -> str:
logger.info(f"Processing: {data[:50]}...")
# Process data
logger.info("Complete")
return "Success"
```
***
## Advanced Example
Combining ActionContext, shared state, and credentials:
```python theme={null}
import requests
async def send_authenticated_request(
url: str,
data: str,
*,
ctx,
**kwargs
) -> str:
"""Send authenticated API request with credentials."""
try:
# Access credentials via ActionContext
if not ctx.credential_manager:
return "Error: Credential manager not available"
api_key = await ctx.credential_manager.resolve_key("API_KEY")
# Check if we've made too many requests
if ctx.shared_state.step_number > 15:
return "Error: Too many API calls"
# Send authenticated request
headers = {"Authorization": f"Bearer {api_key}"}
response = requests.post(url, json={"data": data}, headers=headers, timeout=10)
response.raise_for_status()
return f"Request successful: {response.status_code}"
except Exception as e:
return f"Error: {str(e)}"
custom_tools = {
"send_authenticated_request": {
"parameters": {
"url": {"type": "string", "required": True},
"data": {"type": "string", "required": True},
},
"description": "Send authenticated API request using stored credentials",
"function": send_authenticated_request
}
}
# Usage with credentials
credentials = {"API_KEY": "sk-1234567890"}
agent = MobileAgent(
goal="Send data to API",
config=config,
custom_tools=custom_tools,
credentials=credentials
)
```
***
## Related
See [Agent Architecture](/framework/concepts/architecture) for understanding shared state and custom tools integration.
# Custom Variables
Source: https://docs.mobilerun.ai/framework/features/custom-variables
Pass dynamic data to your Mobilerun agents using the `variables` parameter. Variables enable parameterized workflows and reusable automation.
Custom variables are accessible in:
* **Agent prompts** via custom Jinja2 templates
* **Custom tools** via `ctx.shared_state.custom_variables`
***
## Quick Start
```python theme={null}
from mobilerun import MobileAgent, MobileConfig
# Define custom prompts that render variables
custom_prompts = {
"manager_system": """
{{ instruction }}
{% if variables %}
Available variables:
{% for key, value in variables.items() %}
- {{ key }}: {{ value }}
{% endfor %}
{% endif %}
"""
}
# Create agent with variables
config = MobileConfig()
agent = MobileAgent(
goal="Send email to recipient with subject",
config=config,
variables={"recipient": "john@example.com", "subject": "Update"},
prompts=custom_prompts # Required to see variables
)
result = await agent.run()
```
**Important:** Default prompts don't render variables. You must provide custom prompts via the `prompts` parameter.
***
## How Variables Work
When you pass `variables` to `MobileAgent`:
1. **Stored** in `MobileAgentState.custom_variables`
2. **Passed** to prompt templates as `variables` dict
3. **Rendered** in Jinja2 templates via `{% if variables %}` blocks
4. **Available** to all child agents throughout the workflow
**Access Summary:**
* ✅ Agent prompts (via custom Jinja2 templates)
* ✅ Agents can read and pass to tools as arguments
* ✅ Custom tools (via `ctx.shared_state.custom_variables`)
***
## Basic Usage
```python theme={null}
from mobilerun import MobileAgent, MobileConfig
# Define variables
variables = {
"recipient": "alice@example.com",
"message": "Hello from Mobilerun!"
}
# Custom prompt to render variables
custom_prompts = {
"fast_agent_system": """
You are an agent that controls Android devices.
{% if variables %}
Available variables:
{% for key, value in variables.items() %}
- {{ key }}: {{ value }}
{% endfor %}
{% endif %}
Use these variables when executing tasks.
"""
}
# Create agent
config = MobileConfig()
agent = MobileAgent(
goal="Send message to recipient",
config=config,
variables=variables,
prompts=custom_prompts
)
result = await agent.run()
```
### Available Prompt Keys
Customize these prompts to render variables:
* `manager_system` - Manager agent system prompt
* `executor_system` - Executor agent system prompt
* `fast_agent_system` - FastAgent system prompt
* `fast_agent_user` - FastAgent user prompt
***
## Accessing Variables in Custom Tools
Custom tools can access variables via the `ctx` keyword argument (an `ActionContext` instance injected automatically):
```python theme={null}
from mobilerun import MobileAgent, MobileConfig
async def send_notification(title: str, *, ctx, **kwargs):
"""Send a notification using channel from custom variables.
Args:
title: Notification title
ctx: ActionContext (injected automatically by the registry)
"""
# Access custom variables via ctx.shared_state
channel = ctx.shared_state.custom_variables.get("notification_channel", "default")
return f"Sent '{title}' to {channel}"
custom_tools = {
"send_notification": {
"parameters": {
"title": {"type": "string", "required": True},
},
"description": "Send a notification with title. Usage: {\"action\": \"send_notification\", \"title\": \"Alert\"}",
"function": send_notification
}
}
config = MobileConfig()
agent = MobileAgent(
goal="Send notification with title 'Alert'",
config=config,
custom_tools=custom_tools,
variables={"notification_channel": "alerts"}
)
```
***
## Use Cases
### Parameterized Workflows
```python theme={null}
# Define reusable workflow with different variables
for user in ["alice@example.com", "bob@example.com"]:
agent = MobileAgent(
goal="Send welcome email",
config=config,
variables={"recipient": user},
prompts=custom_prompts
)
await agent.run()
```
### Configuration Data
```python theme={null}
variables = {
"api_endpoint": "https://api.example.com/v2",
"timeout": 30
}
agent = MobileAgent(
goal="Call API endpoint",
config=config,
variables=variables,
prompts=custom_prompts
)
```
***
## Key Points
1. **Custom prompts required** - Default prompts don't render variables in agent context
2. **Direct access in tools** - Custom tools access `ctx.shared_state.custom_variables` via the `ActionContext`
3. **Available to all agents** - Manager, Executor, FastAgent all receive variables
4. **Jinja2 templates** - Use `{% if variables %}` blocks in custom prompts
5. **Auto-injection** - `ctx` (ActionContext) is injected automatically by the tool registry
## Related Documentation
* [Custom Prompts](/framework/concepts/prompts) - How to customize agent prompts
* [Custom Tools](/framework/features/custom-tools) - Creating custom tool functions
* [MobileAgent SDK](/framework/sdk/droid-agent) - Complete API reference
# Structured Output
Source: https://docs.mobilerun.ai/framework/features/structured-output
Extract structured data from device interactions using Pydantic models
## Quick Start
```python theme={null}
import asyncio
from pydantic import BaseModel, Field
from mobilerun import MobileAgent, MobileConfig
# 1. Define output structure
class ContactInfo(BaseModel):
"""Contact information from device."""
name: str = Field(description="Full name of the contact")
phone: str = Field(description="Phone number")
email: str = Field(description="Email address", default="Not provided")
# 2. Create agent with output_model
async def main():
config = MobileConfig()
agent = MobileAgent(
goal="Find John Smith's contact information",
config=config,
output_model=ContactInfo,
)
# 3. Run and access structured output
result = await agent.run()
if result.success and result.structured_output:
contact: ContactInfo = result.structured_output
print(f"Name: {contact.name}")
print(f"Phone: {contact.phone}")
print(f"Email: {contact.email}")
asyncio.run(main())
```
***
## How It Works
### Two-Stage Process
**Stage 1: Task Execution**
* MobileAgent performs device actions while collecting required information
* System prompt is automatically injected with your Pydantic schema
* Agent completes with natural language answer containing the data
**Stage 2: Extraction (Post-Completion)**
* `StructuredOutputAgent` receives the final answer text
* Uses LLM's `astructured_predict()` to extract data into your model
* Validates against schema and returns typed object or `None`
***
## Example: Invoice Extraction
```python theme={null}
from pydantic import BaseModel, Field
from typing import List
class Invoice(BaseModel):
"""Invoice information."""
invoice_number: str = Field(description="Invoice ID")
vendor_name: str = Field(description="Vendor name")
total_due: float = Field(description="Total amount in dollars")
agent = MobileAgent(
goal="Open Gmail and extract invoice from Acme Corp email",
config=MobileConfig(),
output_model=Invoice,
)
result = await agent.run()
invoice = result.structured_output
print(f"Invoice {invoice.invoice_number}: ${invoice.total_due}")
```
***
## Working with Results
### Accessing Data
```python theme={null}
result = await agent.run()
if result.success:
if result.structured_output:
data = result.structured_output # Typed Pydantic object
print(f"Extracted: {data}")
else:
print(f"Extraction failed, text answer: {result.reason}")
else:
print(f"Task failed: {result.reason}")
```
### Exporting to JSON
```python theme={null}
result = await agent.run()
if result.structured_output:
# Convert to JSON and save
json_str = result.structured_output.model_dump_json(indent=2)
with open("output.json", "w") as f:
f.write(json_str)
```
***
## Configuration
### Custom Extraction LLM
By default, extraction uses the `structured_output` LLM profile. If not configured, it falls back to the `fast_agent` LLM. You can specify a dedicated `structured_output` profile:
**config.yaml:**
```yaml theme={null}
llm_profiles:
fast_agent:
provider: GoogleGenAI
model: gemini-3.1-flash-lite-preview
temperature: 0.3
structured_output:
provider: OpenAI
model: gpt-4o-mini
temperature: 0.0 # Low temp for consistent extraction
```
**Programmatically:**
```python theme={null}
from mobilerun import load_llm
config = MobileConfig()
llms = {
"fast_agent": load_llm("GoogleGenAI", "gemini-3.1-flash-lite-preview"),
"structured_output": load_llm("OpenAI", "gpt-4o-mini"),
}
agent = MobileAgent(
goal="Extract contact info for Alice",
llms=llms,
config=config,
output_model=ContactInfo,
)
```
### Reasoning Mode
Works in both direct and reasoning modes:
```python theme={null}
# Direct mode
config = MobileConfig()
config.agent.reasoning = False
agent = MobileAgent(
goal="Find weather for SF",
config=config,
output_model=WeatherInfo,
)
# Reasoning mode
config.agent.reasoning = True
agent = MobileAgent(
goal="Find weather for SF",
config=config,
output_model=WeatherInfo,
)
```
***
## Best Practices
**1. Add clear field descriptions** - The LLM uses these to understand what to extract:
```python theme={null}
name: str = Field(description="Full name of customer who placed order")
```
**2. Provide defaults for optional fields** - Prevents extraction failures:
```python theme={null}
rating: Optional[float] = Field(description="Customer rating (1-5)", default=None)
```
**3. Guide data collection in your goal**:
```python theme={null}
agent = MobileAgent(
goal="Find contact and get their phone number, email, and full name",
config=config,
output_model=ContactInfo,
)
```
***
## Troubleshooting
**Extraction returns None:**
* Verify `output_model` is passed to `MobileAgent`
* Check if task succeeded: `result.success`
* Enable debug logging: `config.logging.debug = True`
**Partial or incorrect data:**
* Add more specific field descriptions
* Mention required fields explicitly in the goal
**Validation errors:**
* Add `Optional` and defaults for uncertain fields
***
## Advanced
### Multiple Items
Extract lists of data using a model with `List` fields:
```python theme={null}
class ContactList(BaseModel):
"""Multiple contacts."""
contacts: List[ContactInfo] = Field(description="List of contacts")
agent = MobileAgent(
goal="Find contacts for John Smith and Jane Doe",
config=config,
output_model=ContactList,
)
```
### Workflow Integration
Extraction happens automatically in `MobileAgent.finalize()`:
```python theme={null}
@step
async def finalize(self, ctx: Context, ev: FinalizeEvent) -> ResultEvent:
result = ResultEvent(
success=ev.success,
reason=ev.reason,
steps=self.shared_state.step_number,
structured_output=None,
)
# Extract if model was provided
if self.output_model is not None and ev.reason:
structured_agent = StructuredOutputAgent(
llm=self.structured_output_llm,
pydantic_model=self.output_model,
answer_text=ev.reason,
)
extraction_result = await structured_agent.run()
if extraction_result["success"]:
result.structured_output = extraction_result["structured_output"]
return result
```
***
## Related Documentation
* [MobileAgent API](/framework/sdk/droid-agent)
* [Pydantic Documentation](https://docs.pydantic.dev/)
* [Configuration Guide](/framework/sdk/configuration)
* [Custom Variables](/framework/features/custom-variables)
# Telemetry
Source: https://docs.mobilerun.ai/framework/features/telemetry
Configure anonymous telemetry
# Why We Need Telemetry
Telemetry helps us:
* Identify which features are most used and need improvement
* Prioritize bug fixes and new features based on real usage
* Ensure Mobilerun works well across different environments
We do not collect any personal or sensitive data. All telemetry is strictly anonymized and used only to improve the framework for everyone.
If you have questions or concerns, please reach out on [GitHub](https://github.com/droidrun/mobilerun) or review our privacy policy.
***
# Toggling Telemetry
Mobilerun collects anonymized usage data to help us understand which features are most valuable and where improvements are needed. This data is never used for advertising or tracking individuals, and is only used to make Mobilerun better for the community.
## How to Disable Telemetry
You can disable telemetry at any time by setting the following environment variable:
```bash theme={null}
export MOBILERUN_TELEMETRY_ENABLED=false
```
Add this line to your shell profile (e.g., `.bashrc`, `.zshrc`, or `.profile`) to make it persistent across sessions.
## How to Enable Telemetry Again
To re-enable telemetry, set the environment variable to `true`:
```bash theme={null}
export MOBILERUN_TELEMETRY_ENABLED=true
```
Telemetry is currently controlled only by the `MOBILERUN_TELEMETRY_ENABLED` environment variable. While a `telemetry.enabled` config option exists in the configuration schema, it is not currently used by the telemetry system.
***
# Tracing
Source: https://docs.mobilerun.ai/framework/features/tracing
Configure Phoenix/Langfuse tracing and trajectory recording
Mobilerun provides multiple monitoring capabilities:
1. **LLM Tracing** - Real-time execution tracing via Arize Phoenix or Langfuse
2. **Trajectory Recording** - Local screenshots and UI state for debugging
## Quick Reference
```sh theme={null}
# Enable Phoenix tracing
mobilerun run "task" --tracing
# Enable trajectory recording
mobilerun run "task" --save-trajectory step
```
***
## LLM Tracing
Mobilerun supports two tracing providers for real-time monitoring of LLM calls, agent execution, and tool invocations:
* **Arize Phoenix** (default) - Open-source observability platform
* **Langfuse** - LLM engineering platform with cloud and self-hosted options
Use tracing to debug agent behavior, monitor token usage, and analyze execution flow.
***
### Arize Phoenix Tracing
#### Setup
**1. Install Phoenix:**
```sh theme={null}
uv pip install arize-phoenix
```
**2. Start Phoenix server:**
```sh theme={null}
phoenix serve
```
The server starts at `http://localhost:6006` and provides a web UI for viewing traces.
**3. Enable tracing in Mobilerun:**
**Via CLI:**
```sh theme={null}
mobilerun run "Open settings" --tracing
```
**Via config.yaml:**
```yaml theme={null}
tracing:
enabled: true
```
**Via code:**
```python theme={null}
from mobilerun import MobileAgent, MobileConfig
config = MobileConfig()
config.tracing.enabled = True
agent = MobileAgent(goal="Open settings", config=config)
await agent.run()
```
**4. View traces:**
Navigate to `http://localhost:6006` to see:
* LLM calls with prompts, responses, and token counts
* Agent workflow execution (Manager, Executor, FastAgent)
* Tool invocations and their results
* Execution timings and errors
For more on using Phoenix, see the [Arize Phoenix documentation](https://docs.arize.com/phoenix).
#### Phoenix Configuration
Set environment variables to customize Phoenix:
```sh theme={null}
# Custom Phoenix server URL (default: http://0.0.0.0:6006)
export phoenix_url=http://localhost:6006
# Project name for organizing traces
export phoenix_project_name=my_mobilerun_project
```
Environment variable names are lowercase: `phoenix_url` and `phoenix_project_name`.
***
### Langfuse Tracing
Langfuse provides LLM observability with features like session tracking, user analytics, and cost monitoring.
#### Setup
**1. Get Langfuse credentials:**
* **Cloud**: Sign up at [cloud.langfuse.com](https://cloud.langfuse.com)
* **Self-hosted**: Deploy using [Langfuse docs](https://langfuse.com/docs/deployment/self-host)
**2. Configure Mobilerun:**
**Via environment variables:**
```sh theme={null}
export LANGFUSE_SECRET_KEY=sk-lf-...
export LANGFUSE_PUBLIC_KEY=pk-lf-...
export LANGFUSE_HOST=https://cloud.langfuse.com
```
**Via config.yaml:**
```yaml theme={null}
tracing:
enabled: true
provider: langfuse
langfuse_secret_key: sk-lf-...
langfuse_public_key: pk-lf-...
langfuse_host: https://cloud.langfuse.com
langfuse_user_id: user@example.com # Optional: track by user
langfuse_session_id: "" # Optional: custom session ID
```
**Via code:**
```python theme={null}
from mobilerun import MobileAgent, MobileConfig, TracingConfig
config = MobileConfig(
tracing=TracingConfig(
enabled=True,
provider="langfuse",
langfuse_secret_key="sk-lf-...",
langfuse_public_key="pk-lf-...",
langfuse_host="https://cloud.langfuse.com",
langfuse_user_id="user@example.com",
)
)
agent = MobileAgent(goal="Open settings", config=config)
await agent.run()
```
**3. View traces:**
Navigate to your Langfuse dashboard to see:
* LLM calls with prompts, completions, and token usage
* Agent execution traces and nested workflows
* Session-based analytics and cost tracking
* User-level metrics (if `langfuse_user_id` is set)
For more on using Langfuse, see the [Langfuse documentation](https://langfuse.com/docs).
***
## Trajectory Recording
Trajectory recording saves screenshots and UI state locally for offline debugging and analysis. Unlike telemetry (sent to PostHog) and tracing (sent to Phoenix or Langfuse), trajectories stay on your machine.
### Recording Levels
| Level | What's Saved | When to Use |
| ---------------- | ------------------------------------ | ------------------------------------------------- |
| `none` (default) | Nothing | Production use, saves disk space |
| `step` | Screenshot + state per agent step | General debugging, recommended for most use cases |
| `action` | Screenshot + state per atomic action | Detailed debugging, captures every tap/swipe/type |
**Note:** `action` level generates significantly more files than `step` level.
### Enable Recording
**Via CLI:**
```sh theme={null}
mobilerun run "Open settings" --save-trajectory step
```
**Via config.yaml:**
```yaml theme={null}
logging:
save_trajectory: step # none | step | action
```
**Via code:**
```python theme={null}
from mobilerun import MobileAgent, MobileConfig
config = MobileConfig()
config.logging.save_trajectory = "action"
agent = MobileAgent(goal="Open settings", config=config)
await agent.run()
```
### Output Location
Trajectories are saved to `trajectories/` in your working directory:
```
trajectories/
└── 2025-10-17_14-30-45_open_settings/
├── step_000_screenshot.png
├── step_000_state.json
├── step_001_screenshot.png
└── step_001_state.json
```
Each trajectory folder contains:
* **Screenshots** - PNG images of the device screen at each step/action
* **State files** - JSON files with:
* UI accessibility tree (element hierarchy with IDs, text, bounds)
* Action executed (e.g., `click(5)`, `type("hello", 3)`)
* Agent reasoning and step number
* Device state (current app package, activity)
Use these files to:
* Debug why the agent made specific decisions
* Replay failed executions
* Analyze UI element detection issues
* Build training datasets for agent improvement
***
## Related Documentation
* [Configuration System](/framework/sdk/configuration) - Configure tracing and telemetry settings
* [Events and Workflows](/framework/concepts/events-and-workflows) - Build custom monitoring integrations
* [CLI Usage](/framework/guides/cli) - Command-line flags for monitoring
# Vision Mode
Source: https://docs.mobilerun.ai/framework/features/vision
Screenshots for agents, and how screen coordinates stay accurate
With vision enabled, agents receive a screenshot of the device alongside the
accessibility tree on every step.
## Enabling vision
```yaml theme={null}
agent:
manager:
vision: true # planning agent sees screenshots
executor:
vision: true # action agent sees screenshots
fast_agent:
vision: true # direct-execution agent sees screenshots
```
```bash theme={null}
# CLI: enable for all agents at once
mobilerun run "Open settings and enable dark theme" --vision
```
```python theme={null}
# SDK
from mobilerun import MobileConfig
from mobilerun.config_manager import AgentConfig, FastAgentConfig
config = MobileConfig(agent=AgentConfig(fast_agent=FastAgentConfig(vision=True)))
```
`vision_only: true` (CLI: `--vision-only`) is a separate mode that drops the
accessibility tree and drives the device from screenshots alone.
## The coordinate contract
With vision active, coordinates are handled automatically:
1. The screenshot is resized to the exact dimensions the model will ground
on, with a labeled coordinate grid drawn on it.
2. The device state declares that coordinate space, and element bounds in the
accessibility tree text are emitted in the same space.
3. Coordinates the model passes to `click_at`, `click_area`, `long_press_at`,
and `swipe` are converted back automatically — to device pixels on
Android, to points on iOS.
4. Coordinates outside the declared space are rejected with a corrective
error and the model retries.
No configuration is needed. Element-index actions (`click`, `long_press`) are
unaffected; they always tap accessibility-tree bounds.
### Per-model effective screenshot size
Some providers downsize images server-side before the model sees them. To
keep coordinates exact, mobilerun resolves the screenshot dimensions each
active vision model actually grounds on and resizes to the smallest result
across all recipients (manager + executor, or the fast agent):
| Model family | Longest edge |
| ------------------------------------ | ------------ |
| Anthropic standard (e.g. Sonnet) | 1568 px |
| Anthropic high-res (e.g. Opus, 4.x+) | 2576 px |
| OpenAI, Gemini, Ollama | 2048 px |
Unknown Anthropic model IDs fall back to the standard 1568 budget. The
declared coordinate space always matches the image the model receives, so
`convert_point` is exact regardless of which model is configured.
### Capping the screenshot size
For local or undocumented vision models that downsize to a size mobilerun
can't infer (for example, certain Ollama models), set an explicit cap with
`agent.model_screenshot_max_side`. The cap is applied on top of per-model
resolution and uses the smaller of the two.
```yaml theme={null}
agent:
# Cap the model-facing screenshot at 1280 px on the longest edge.
model_screenshot_max_side: 1280
```
```python theme={null}
from mobilerun import MobileConfig
from mobilerun.config_manager import AgentConfig
config = MobileConfig(agent=AgentConfig(model_screenshot_max_side=1280))
```
Leave this unset for the supported providers above — the per-model defaults
already match what they ground on.
## Coordinate tools and vision
`swipe` is always available. `click_at`, `click_area`, and `long_press_at`
are disabled by default; enabling vision re-enables `click_at` automatically
(non-normalized). To enable all coordinate tools:
```yaml theme={null}
tools:
disabled_tools: []
```
## Normalized coordinates
`agent.use_normalized_coordinates: true` makes the model emit `[0-1000]`
coordinates instead of pixels; the coordinate contract is inactive in this
mode.
An occasional "Coordinates ... are outside the ... coordinate space" error is
the guardrail correcting the model; it retries automatically. Persistent
errors suggest a model that is weak at visual grounding.
# CLI Usage
Source: https://docs.mobilerun.ai/framework/guides/cli
Command-line interface for controlling devices with natural language
## Overview
The Mobilerun CLI lets you control Android and iOS devices using natural language commands powered by LLM agents.
### Quick Start
```bash theme={null}
# Setup device
mobilerun setup
# Run a command
mobilerun run "Open Spotify and play my Discover Weekly"
```
**Auto-configuration**: If no config exists, Mobilerun creates a default `config.yaml` at `~/.config/mobilerun/config.yaml` automatically.
***
## Commands
Execute natural language commands on your device.
### Usage
```bash theme={null}
mobilerun run "" [OPTIONS]
```
### Common Flags
| Flag | Description | Default |
| ------------------- | ------------------------------------------------------------------- | --------------------------------- |
| `--agent`, `-a` | External agent to use (not yet supported — reserved for future use) | None |
| `--provider`, `-p` | LLM provider (GoogleGenAI, OpenAI, Anthropic, etc.) | From config |
| `--model`, `-m` | Model name | From config |
| `--device`, `-d` | Device serial or IP | Auto-detect |
| `--steps` | Max execution steps | `15` |
| `--reasoning` | Enable planning mode | `false` |
| `--vision` | Enable vision for all agents | From config |
| `--tcp` | Use TCP instead of content provider | `false` |
| `--debug` | Verbose logging | `false` |
| `--save-trajectory` | Save execution (`none`, `step`, `action`) | `none` |
| `--config`, `-c` | Custom config path | `~/.config/mobilerun/config.yaml` |
### Examples
```bash theme={null}
# Simple command
mobilerun run "Open Settings"
# Multi-step task
mobilerun run "Send WhatsApp to John: I'll be late"
# Specific device
mobilerun run "Check battery" --device emulator-5554
```
```bash theme={null}
# Google Gemini
export GOOGLE_API_KEY=your-key
mobilerun run "Archive old emails" \
--provider GoogleGenAI \
--model gemini-3.1-flash-lite-preview
# OpenAI
export OPENAI_API_KEY=your-key
mobilerun run "Create shopping list" \
--provider OpenAI \
--model gpt-4o
# Anthropic Claude
export ANTHROPIC_API_KEY=your-key
mobilerun run "Reply to latest email" \
--provider Anthropic \
--model claude-sonnet-4-5-latest
# Local Ollama (free)
mobilerun run "Turn on dark mode" \
--provider Ollama \
--model llama3.3:70b \
--base_url http://localhost:11434
```
```bash theme={null}
# Complex task with planning
mobilerun run "Organize inbox by sender" \
--reasoning \
--vision \
--steps 30
# Debug failing command
mobilerun run "Book Uber to airport" \
--debug \
--save-trajectory action
# Wireless execution
mobilerun run "Clear cache" \
--device 192.168.1.100:5555 \
--tcp
# Custom config
mobilerun run "Enable 2FA" \
--config /path/to/config.yaml
```
### Provider Options
| Provider | Install | Environment Variable |
| ----------- | --------------------------------------- | -------------------- |
| GoogleGenAI | Included by default | `GOOGLE_API_KEY` |
| OpenAI | Included by default | `OPENAI_API_KEY` |
| OpenAILike | Included by default | Varies by provider |
| OpenRouter | Included by default | `OPENROUTER_API_KEY` |
| Ollama | Included by default | None (local) |
| Anthropic | `uv pip install 'mobilerun[anthropic]'` | `ANTHROPIC_API_KEY` |
| DeepSeek | `uv pip install 'mobilerun[deepseek]'` | `DEEPSEEK_API_KEY` |
### `mobilerun devices`
List connected devices.
```bash theme={null}
mobilerun devices
# Output:
# Found 2 connected device(s):
# • emulator-5554
# • 192.168.1.100:5555
```
***
### `mobilerun setup`
Install Portal APK on device.
```bash theme={null}
# Auto-detect device
mobilerun setup
# Specific device
mobilerun setup --device emulator-5554
# Custom APK
mobilerun setup --path /path/to/portal.apk
```
**What it does:**
1. Downloads compatible Portal APK for your SDK version
2. Installs with all permissions
3. Auto-enables accessibility service
4. Opens settings if manual enable needed
***
### `mobilerun ping`
Test Portal connection.
```bash theme={null}
# Test default communication
mobilerun ping
# Test TCP mode
mobilerun ping --tcp
# Specific device
mobilerun ping --device 192.168.1.100:5555
```
**Success output:** `Portal is installed and accessible. You're good to go!`
***
### `mobilerun connect`
Connect to device via TCP/IP.
```bash theme={null}
mobilerun connect 192.168.1.100:5555
```
**Prerequisites:**
```bash theme={null}
# Enable wireless debugging (Android 11+)
# Settings > Developer options > Wireless debugging
# Or via USB:
adb tcpip 5555
adb shell ip route | awk '{print $9}' # Get IP
mobilerun connect :5555
```
***
### `mobilerun disconnect`
Disconnect from device.
```bash theme={null}
mobilerun disconnect 192.168.1.100:5555
```
***
### `mobilerun device`
Direct device actions that bypass the LLM agent.
```bash theme={null}
# Take a screenshot
mobilerun device screenshot
# Print the UI accessibility tree
mobilerun device ui
# Tap at coordinates
mobilerun device tap 500 500
# Swipe from point to point
mobilerun device swipe 100 500 100 200 --duration 0.5
# Long press at coordinates
mobilerun device long-press 500 500
# Type text into focused field
mobilerun device type "Hello world" --clear
# Press a system button (back, home, enter)
mobilerun device press back
# List installed apps
mobilerun device apps --system
# Launch an app by package name
mobilerun device start com.example.app
```
All `device` subcommands support `--device`, `--config`, `--tcp`, and `--ios` flags.
Record and replay automation sequences.
### `mobilerun macro list`
List saved trajectories.
```bash theme={null}
# Default directory
mobilerun macro list
# Custom directory
mobilerun macro list /path/to/trajectories
```
**Output:**
```
Found 3 trajectory(s):
┏━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┓
┃ Folder ┃ Description ┃ Actions ┃
┡━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━┩
│ open-settings │ Opens settings app │ 3 │
│ enable-dark-mode │ Navigate to display... │ 8 │
└──────────────────┴───────────────────────────┴─────────┘
```
***
### `mobilerun macro replay`
Replay recorded macro.
```bash theme={null}
# Basic replay
mobilerun macro replay trajectories/open-settings
# Custom device and timing
mobilerun macro replay trajectories/login-flow \
--device emulator-5554 \
--delay 0.5
# Start from specific step
mobilerun macro replay trajectories/checkout \
--start-from 5 \
--max-steps 10
# Preview without executing
mobilerun macro replay trajectories/test --dry-run
```
**Flags:**
| Flag | Description | Default |
| -------------------- | ----------------------- | ----------- |
| `--device`, `-d` | Device serial | Auto-detect |
| `--delay`, `-t` | Seconds between actions | `1.0` |
| `--start-from`, `-s` | Start step (1-based) | `1` |
| `--max-steps`, `-m` | Max steps to run | All |
| `--dry-run` | Preview only | `false` |
***
### Recording Trajectories
```bash theme={null}
# Record at action level (most detailed)
mobilerun run "Create alarm for 7am" --save-trajectory action
# Record at step level
mobilerun run "Export contacts" --save-trajectory step
```
**Trajectory structure:**
```
trajectories/2025-10-16_14-30-45/
├── macro.json # Action sequence
├── step_0.png # Screenshots
├── step_1.png
└── ...
```
***
## Configuration
### Override Priority
1. **CLI flags** (highest)
2. **Config file** (`~/.config/mobilerun/config.yaml`)
3. **Defaults** (lowest)
### Common Patterns
```bash Quick Test theme={null}
mobilerun run "Turn on dark mode" \
--provider GoogleGenAI \
--model gemini-3.1-flash-lite-preview
```
```bash Debug Task theme={null}
mobilerun run "Book ride to airport" \
--debug \
--reasoning \
--vision \
--save-trajectory action
```
```bash Cost Optimization theme={null}
mobilerun run "Set alarm" \
--provider GoogleGenAI \
--model gemini-3.1-flash-lite-preview \
--no-vision
```
```bash Multiple Devices theme={null}
for device in $(adb devices | awk 'NR>1 {print $1}'); do
mobilerun run "Clear notifications" --device $device
done
```
***
## Troubleshooting
```bash theme={null}
# Check ADB
adb devices
# If unauthorized: Accept prompt on device
# If not listed: Try different USB port/cable
# Restart ADB
adb kill-server && adb start-server
```
```bash theme={null}
# Verify installation
adb shell pm list packages | grep mobilerun
# Reinstall
mobilerun setup
# Enable accessibility manually
adb shell settings put secure enabled_accessibility_services \
com.mobilerun.portal/com.mobilerun.portal.service.MobilerunAccessibilityService
```
```bash theme={null}
# Install provider
uv pip install 'mobilerun[anthropic]'
# Check API key
echo $GOOGLE_API_KEY
# Set if missing
export GOOGLE_API_KEY=your-key
```
```bash theme={null}
# Increase steps
mobilerun run "Complex task" --steps 50
# Enable debug mode
mobilerun run "Task" --debug
# Try reasoning mode
mobilerun run "Multi-step task" --reasoning
```
```bash theme={null}
# Enable TCP mode (USB connected first)
adb tcpip 5555
# Get device IP
adb shell ip route | awk '{print $9}'
# Connect
mobilerun connect :5555
# Verify
mobilerun ping --tcp
```
***
## Environment Variables
For first-time setup, use `mobilerun configure` to interactively choose your provider and credentials. Environment variables below are useful for overrides and CI/CD.
| Variable | Description | Default |
| ------------------- | --------------------- | --------------------------------- |
| `GOOGLE_API_KEY` | Google Gemini API key | None |
| `OPENAI_API_KEY` | OpenAI API key | None |
| `ANTHROPIC_API_KEY` | Anthropic API key | None |
| `DEEPSEEK_API_KEY` | DeepSeek API key | None |
| `MOBILERUN_CONFIG` | Config file path | `~/.config/mobilerun/config.yaml` |
**Setting variables:**
```bash Linux/macOS theme={null}
export GOOGLE_API_KEY=your-key
```
```bash Windows PowerShell theme={null}
$env:GOOGLE_API_KEY="your-key"
```
```bash Permanent (Linux/macOS) theme={null}
echo 'export GOOGLE_API_KEY=your-key' >> ~/.bashrc
source ~/.bashrc
```
***
## Next Steps
* [Configuration Guide](/framework/sdk/configuration) - Customize behavior
* [Device Setup](/framework/guides/device-setup) - Detailed setup instructions
* [Agent Architecture](/framework/concepts/architecture) - How it works
* [Custom Tools](/framework/features/custom-tools) - Extend functionality
# Device Setup
Source: https://docs.mobilerun.ai/framework/guides/device-setup
Setting up Android and iOS devices for Mobilerun automation
## Overview
Mobilerun controls devices through a specialized Portal app that bridges your computer and the device.
## Prerequisites
**macOS**: `brew install android-platform-tools`
**Linux**: `sudo apt install adb`
**Windows**: Download from [Android Developer Site](https://developer.android.com/studio/releases/platform-tools)
Verify: `adb version`
1. Go to **Settings** > **About phone**
2. Tap **Build number** 7 times (enables Developer options)
3. Go to **Settings** > **Developer options**
4. Enable **USB debugging**
5. Connect device and tap **Always allow**
Verify: `adb devices`
```bash theme={null}
# Automatic setup (downloads compatible Portal APK)
mobilerun setup
# Or specify device
mobilerun setup --device SERIAL_NUMBER
```
This will:
* Download the compatible Portal APK for your SDK version
* Install with all permissions granted
* Enable accessibility service automatically
```bash theme={null}
mobilerun ping
# Output: Portal is installed and accessible. You're good to go!
```
***
## Portal App
The Mobilerun Portal (`com.mobilerun.portal`) provides:
* **Accessibility Tree** - Extracts UI elements and their properties
* **Device State** - Tracks current activity, keyboard visibility
* **Action Execution** - Tap, swipe, text input, and other actions
* **Dual Communication** - TCP (faster) or Content Provider (fallback)
The Portal only communicates locally via ADB. No data is sent to external servers.
***
## Communication Modes
**How it works:**
* Portal runs HTTP server on device port 8080
* ADB forwards local port → device port 8080
* Mobilerun sends HTTP requests to `localhost:PORT`
**Enable:**
```bash theme={null}
# CLI
mobilerun run "your command" --tcp
# Python
config = MobileConfig(device=DeviceConfig(serial="DEVICE_SERIAL", use_tcp=True))
```
**Troubleshooting:**
```bash theme={null}
# Check port forwarding
adb forward --list
# Test Portal server
adb shell netstat -an | grep 8080
# Remove all forwards and retry
adb forward --remove-all
mobilerun ping --tcp
```
**How it works:**
* Portal exposes content provider at `content://com.mobilerun.portal/`
* Commands sent via ADB shell: `content query --uri ...`
* JSON responses parsed from shell output
**Usage:**
```bash theme={null}
# Default mode (no flag needed)
mobilerun ping
# Python
config = MobileConfig(device=DeviceConfig(serial="DEVICE_SERIAL", use_tcp=False))
```
**Troubleshooting:**
```bash theme={null}
# Test content provider directly
adb shell content query --uri content://com.mobilerun.portal/state
# Should show: Row: 0 result={"data": "{...}"}
```
***
## Advanced Setup
### Setup
1. **Settings** > **Developer options** > **Wireless debugging**
2. Note IP address and port (e.g., `192.168.1.100:37757`)
**QR Code Method:**
```bash theme={null}
adb pair
```
**Pairing Code Method:**
1. Tap **Pair device with pairing code**
2. Note pairing code and IP:port
3. Run: `adb pair IP:PORT`
4. Enter pairing code
```bash theme={null}
adb connect IP:PORT
mobilerun ping --device IP:PORT
```
### Common Issues
* Connection refused → Check same WiFi network and firewall
* Frequent drops → Use 5GHz WiFi or stay near router
* Can't find IP → Run `adb shell ip addr show wlan0 | grep "inet "` via USB
```bash theme={null}
# Connect via USB first
adb tcpip 5555
```
```bash theme={null}
adb shell ip addr show wlan0 | grep inet
```
```bash theme={null}
# Disconnect USB cable
adb connect DEVICE_IP:5555
mobilerun ping --device DEVICE_IP:5555
```
### List Devices
```bash theme={null}
mobilerun devices
# Found 2 connected device(s):
# • emulator-5554
# • 192.168.1.100:5555
```
### Target Specific Device
```bash theme={null}
# CLI
mobilerun run "your command" --device emulator-5554
# Python
config = MobileConfig(device=DeviceConfig(serial="emulator-5554"))
agent = MobileAgent(goal="your task", config=config)
```
### Parallel Control
```python theme={null}
import asyncio
from mobilerun import DeviceConfig, MobileConfig, MobileAgent
from async_adbutils import adb
async def control_device(serial: str, command: str):
device_config = DeviceConfig(serial=serial)
config = MobileConfig(device=device_config)
agent = MobileAgent(goal=command, config=config)
return await agent.run()
async def main():
devices = await adb.list()
tasks = [
control_device(devices[0].serial, "Open settings"),
control_device(devices[1].serial, "Check battery"),
]
results = await asyncio.gather(*tasks)
print(results)
asyncio.run(main())
```
***
## Troubleshooting
**Symptoms:** `adb devices` shows no devices or `unauthorized`
**Solutions:**
1. Unplug/replug USB cable, try different port
2. Revoke USB debugging authorizations (Developer options)
3. Reconnect and tap "Always allow"
4. Restart ADB: `adb kill-server && adb start-server`
5. **Windows**: Install [Google USB Driver](https://developer.android.com/studio/run/win-usb)
**Symptoms:** `mobilerun ping` fails with "Portal is not installed"
**Solutions:**
1. Reinstall: `mobilerun setup`
2. Check: `adb shell pm list packages | grep mobilerun`
3. Verify APK architecture matches device (arm64-v8a for most devices)
**Symptoms:** `mobilerun ping` fails with "accessibility service not enabled"
**Solutions:**
1. Auto-enable:
```bash theme={null}
adb shell settings put secure enabled_accessibility_services \
com.mobilerun.portal/com.mobilerun.portal.service.MobilerunAccessibilityService
adb shell settings put secure accessibility_enabled 1
```
2. Manual: Settings > Accessibility > Mobilerun Portal > Toggle ON
3. Verify:
```bash theme={null}
adb shell settings get secure enabled_accessibility_services
# Should contain: com.mobilerun.portal/...
```
**Symptoms:** `input_text()` fails or types gibberish
**Solutions:**
1. Keyboard auto-enabled by `AndroidDriver` initialization:
```bash theme={null}
# Verify
adb shell settings get secure default_input_method
# Should show: com.mobilerun.portal/.input.MobilerunKeyboardIME
```
2. Manual switch: Long press space bar → Select "Mobilerun Keyboard"
3. Focus the element first (tap it), then input text
**Symptoms:** `get_state()` returns empty or incomplete UI tree
**Solutions:**
1. Verify accessibility: `mobilerun ping`
2. Some apps block accessibility services (WebViews, games, custom UI)
3. Wait for UI: `time.sleep(1)` after tap/swipe
4. Enable Portal overlay to see detected elements
iOS support is stable and provides the same core device automation capabilities as Android through `mobilerun-ios`. Platform-specific differences are listed under [Supported Features](#supported-features).
***
## Prerequisites
* **macOS** with **Xcode** installed
* **Apple ID** with signing capability (a personal team is sufficient)
* **iPhone** with **Developer Mode** enabled
* **USB data cable** to connect the iPhone to your Mac
* **Mobilerun WebDriverAgent** installed and signed on the iPhone (see the [Connect an iPhone guide](/guides/connect-iphone))
* **`mobilerun-ios`** installed on your Mac
### Enable Developer Mode on your device
1. Go to **Settings** > **Privacy & Security** > **Developer Mode**
2. Toggle **Developer Mode** on
3. Restart the device when prompted
4. After restart, confirm the prompt to enable Developer Mode
For the complete device preparation and WebDriverAgent signing instructions, follow the [Connect an iPhone guide](/guides/connect-iphone). You only need to complete the device, WebDriverAgent, and `mobilerun-ios` installation steps; logging in is not required for local mode.
***
## Install and Run `mobilerun-ios`
`mobilerun-ios --local` connects to WebDriverAgent over USB and exposes the device API on your Mac. The framework discovers that API automatically.
Choose either Homebrew or the installation script:
```bash Homebrew theme={null}
brew install droidrun/tap/mobilerun-ios
```
```bash Installation script theme={null}
curl -fsSL https://github.com/droidrun/mobilerun-ios-releases/releases/latest/download/install.sh | sh
```
Verify the installation:
```bash theme={null}
mobilerun-ios --version
```
Connect and unlock the iPhone, accept the **Trust This Computer** prompt, then run:
```bash theme={null}
mobilerun-ios list
```
Copy the UDID shown for the iPhone you want to control.
```bash theme={null}
mobilerun-ios --local
```
Keep this command running. By default, it serves the device API at `http://127.0.0.1:8080`.
```bash theme={null}
curl http://127.0.0.1:8080/ping
```
The response should contain `"result":"pong"`. Then test the framework:
```bash theme={null}
mobilerun run "Take a screenshot" --ios
```
To select the server explicitly, pass its URL:
```bash theme={null}
mobilerun run "Take a screenshot" --ios --device http://127.0.0.1:8080
```
When several iPhones are attached, ports advance per device (`8080`, `8081`, and so on). Each device URL is printed when `mobilerun-ios` starts. Framework auto-discovery scans ports `8080` through `8089`.
Loopback local mode does not require authentication. If you bind to a non-loopback address, `mobilerun-ios` requires `--local-token`. Set the same token in `MOBILERUN_DEVICE_TOKEN` or `device.auth_token` in the framework configuration.
***
## Architecture
The iOS local driver uses a different architecture than Android:
| Feature | Android | iOS |
| ------------- | -------------------------- | ---------------------------------------- |
| Communication | ADB + TCP/Content Provider | Local HTTP API (port 8080 by default) |
| Device bridge | Portal APK | `mobilerun-ios` backed by WebDriverAgent |
| Setup tool | `mobilerun setup` | `mobilerun-ios --local ` |
| Accessibility | Android Accessibility API | WebDriverAgent / XCUITest |
| Text Input | Custom keyboard IME | Direct XCUITest text input |
| Connection | ADB over USB/TCP | USB through `mobilerun-ios` |
`mobilerun-ios` uses WebDriverAgent to extract accessibility trees, perform gestures, manage apps, and capture screenshots. Local mode exposes these operations through the portal HTTP contract used by the framework.
***
## Usage
### CLI
```bash theme={null}
# Auto-discovers mobilerun-ios on ports 8080-8089
mobilerun run "your command" --ios
# Explicit local server URL
mobilerun run "your command" --ios --device http://127.0.0.1:8080
```
### Python API
```python theme={null}
from mobilerun import MobileAgent, MobileConfig, DeviceConfig
config = MobileConfig(
device=DeviceConfig(
platform="ios",
serial="http://127.0.0.1:8080", # optional, auto-discovered if omitted
)
)
agent = MobileAgent(
goal="Open Settings and check WiFi",
config=config
)
result = await agent.run()
```
***
## Supported Features
| Feature | Status | Notes |
| ----------------- | ------ | --------------------------------------------------- |
| `tap()` | ✅ | |
| `swipe()` | ✅ | |
| `input_text()` | ✅ | |
| `screenshot()` | ✅ | |
| `get_state()` | ✅ | Accessibility tree extraction |
| `start_app()` | ✅ | By bundle identifier |
| `stop_app()` | ✅ | By bundle identifier |
| `get_apps()` | ✅ | Includes system-app filtering |
| `get_date()` | ✅ | |
| `press_button()` | ✅ | `home`, `back`, `enter`, `delete`, and `app_switch` |
| `drag()` | ❌ | |
| `install_app()` | ✅ | From a path or URL accessible to `mobilerun-ios` |
| `uninstall_app()` | ❌ | Not exposed by the local API |
***
## Troubleshooting
**Symptoms:** The connected iPhone does not appear in the device list
**Solutions:**
1. Unlock the iPhone and accept the **Trust This Computer** prompt
2. Confirm the USB cable supports data, not only charging
3. Open Xcode once with the device connected and verify it appears as a run destination
**Symptoms:** `mobilerun-ios --local` times out while waiting for WebDriverAgent
**Solutions:**
1. Re-sign and build `WebDriverAgentRunner` in Xcode
2. Trust the developer certificate under **Settings** > **General** > **VPN & Device Management**
3. Keep the device unlocked and run `mobilerun-ios --local ` again
4. Review the [Connect an iPhone guide](/guides/connect-iphone) for the complete signing procedure
**Symptoms:** `curl http://127.0.0.1:8080/ping` times out or is refused
**Solutions:**
1. Confirm `mobilerun-ios --local ` is still running
2. Check its startup output for the actual server URL
3. If another process uses port 8080, choose an available high port, for example: `mobilerun-ios --local --local-addr 127.0.0.1:18080 `
4. Test the printed URL with `/ping`. Because custom ports are not auto-discovered, pass it to the framework explicitly with `--device http://127.0.0.1:18080`
**Symptoms:** `mobilerun run ... --ios` cannot find the iPhone
**Solutions:**
1. Confirm the local server is on one of the automatically scanned ports, `8080` through `8089`
2. Pass the URL explicitly: `--device http://127.0.0.1:8080`
3. If the server uses a token, export `MOBILERUN_DEVICE_TOKEN` before starting the framework
***
## Next Steps
* Learn about the [Agent System](/framework/concepts/architecture)
* Explore [Configuration Options](/framework/sdk/configuration)
* Try [Custom Tools](/framework/features/custom-tools)
* Implement [Structured Output](/framework/features/structured-output)
# Quickstart with Docker
Source: https://docs.mobilerun.ai/framework/guides/docker
Get up and running with Mobilerun in Docker quickly and effectively
This guide will help you get Mobilerun running in a Docker container, controlling your Android device through natural language without installing it natively.
### Host configuration
Follow the steps below to ensure your host system is properly configured to mount the smartphone inside the container.
1. Make sure you’ve completed the prerequisite steps in the [Quickstart](/framework/quickstart). Then verify the ADB connection with `adb devices`. This should list your phone as “device” along with its Serial Number. If the phone appears as `unauthorized` make sure to accept the prompt on your phone.
2. Confirm that the ADB keys have been correctly created. Check for an `.android` folder under your `$HOME` directory that contains: `adb.5037`, `adbkey`, and `adbkey.pub`.
3. Set up a custom rule to uniquely identify your phone and map it to a static path:
* Open a terminal and find your device’s `idVendor` and `idProduct` using the `lsusb` command.
In the example below, the `idVendor` and `idProduct` are `18d1` and `4ee2`, respectively.
```bash theme={null}
> lsusb
Bus 003 Device 002: ID 18d1:4ee2 Google Inc. Nexus/Pixel Device (MTP + debug)
```
* Create a new file (sudo may be required) under `/etc/udev/rules.d` and name it `51-android.rules`.
* Add the following content and save the file:
```bash theme={null}
SUBSYSTEM=="usb", ATTR{idVendor}=="XXXX", ATTR{idProduct}=="YYYY", ATTR{serial}=="SSSS", MODE="0666", GROUP="plugdev", SYMLINK+="phone1/phone"
```
where `XXXX`, `YYYY`, and `SSSS` are the vendor ID, product ID, and serial number of your phone.
This static mapping is required because otherwise the phone’s mount path will change across reconnections!
* Reload the rules
```bash theme={null}
sudo udevadm control --reload-rules
sudo udevadm trigger
```
4. Kill the ADB server on the host so that the container can actually detect the phone:
```bash theme={null}
adb kill-server
```
### Usage
Pull the image from the GitHub Container Registry:
```bash theme={null}
docker pull ghcr.io/droidrun/mobilerun:latest
```
Before running the container, note that the following options are always required when launching Mobilerun in Docker:
| Docker option | What it does | Why it matters for Mobilerun |
| ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `--group-add plugdev` | Adds the host user to the container’s `plugdev` group. | Gives the container permission to access USB devices (e.g., your Android phone) without requiring root. |
| `--device /dev/phone1/phone:/dev/phone` | Maps a specific USB device file from the host into the container. Thanks to our udev rule, the phone will always be mapped here. | Allows Mobilerun to see the phone as `/dev/phone` inside the container, so it can communicate with the device. |
| `--volume /dev/bus/usb:/dev/bus/usb` | Mounts the entire USB bus directory. | Provides access to all connected USB devices, enabling Mobilerun to discover and interact with the phone. |
| `--volume ~/.android:/home/mobilerun/.android` | Mounts the host’s Android configuration directory into the container. | Stores ADB keys and settings so that once you grant permission on the host, Mobilerun doesn’t prompt for it again inside the container. |
The Docker image uses the `mobilerun` entrypoint, which means you can use any of the CLI commands described in the [CLI Usage](/framework/guides/cli) section and append it to the following base command:
```bash theme={null}
docker run \
--group-add plugdev \
--device /dev/phone1/phone:/dev/phone \
--volume /dev/bus/usb:/dev/bus/usb \
--volume ~/.android:/home/mobilerun/.android \
ghcr.io/droidrun/mobilerun:latest \
```
#### Set Up the Portal APK
Simply run the container with the `setup` CLI command.
```bash theme={null}
docker run \
--group-add plugdev \
--device /dev/phone1/phone:/dev/phone \
--volume /dev/bus/usb:/dev/bus/usb \
--volume ~/.android:/home/mobilerun/.android \
ghcr.io/droidrun/mobilerun:latest \
setup
```
#### Verify the setup
Simply run the container with the `ping` CLI command.
```bash theme={null}
docker run \
--group-add plugdev \
--device /dev/phone1/phone:/dev/phone \
--volume /dev/bus/usb:/dev/bus/usb \
--volume ~/.android:/home/mobilerun/.android \
ghcr.io/droidrun/mobilerun:latest \
ping
```
#### Run some agents
Now you’re ready to control your device using Docker and natural language:
```bash theme={null}
# Using default configuration with Google API key
docker run --group-add plugdev --device /dev/phone1/phone:/dev/phone --volume /dev/bus/usb:/dev/bus/usb --volume ~/.android:/home/mobilerun/.android --env GOOGLE_API_KEY=your-api-key-here ghcr.io/droidrun/mobilerun:latest run "Open the settings app and tell me the Android version"
# Override provider and model
docker run --group-add plugdev --device /dev/phone1/phone:/dev/phone --volume /dev/bus/usb:/dev/bus/usb --volume ~/.android:/home/mobilerun/.android --env OPENAI_API_KEY=your-api-key-here ghcr.io/droidrun/mobilerun:latest run "Open a browser and search for mobilerun" -p OpenAI -m gpt-4o
# Use a locally-running LLM, example for LM Studio running in the LAN
docker run --group-add plugdev --device /dev/phone1/phone:/dev/phone --volume /dev/bus/usb:/dev/bus/usb --volume ~/.android:/home/mobilerun/.android --network host ghcr.io/droidrun/mobilerun:latest run "Open a browser and search for mobilerun" -p OpenAILike -m gpt-oss --api_base http://IP-of-LMStudio-server:PORT/v1
```
#### Troubleshooting
* Error response from daemon
If you encounter the following error:
```
docker: Error response from daemon: error gathering device information while adding custom '/dev/phone1/phone': no such file or directory
```
it usually indicates one of two things:
* The phone isn’t mounted at the expected custom path. Verify that `/dev/phone1/phone` exists.
* An ADB server is still running on the host. Stop it with `adb kill-server`.
***
## Next Steps
* [CLI Usage](/framework/guides/cli) - Dive into the various parameters and start building your own projects with Docker!
# Overview
Source: https://docs.mobilerun.ai/framework/guides/overview
Welcome to the Mobilerun Guides! This section provides step-by-step instructions and best practices for using Mobilerun. Each guide focuses on a specific aspect of the framework, from device setup to advanced automation patterns.
***
## Available Guides
### Getting Started
**[CLI Reference](./cli)** - Complete command-line interface guide
* All CLI commands (`run`, `setup`, `devices`, `connect`, `disconnect`, `ping`, `device`, `macro`, `doctor`, `tui`)
* Configuration overrides and flags
* Environment variables and API keys
* Common workflows and troubleshooting
**[Device Setup](./device-setup)** - Set up Android and iOS devices
* Portal app installation and configuration
* Accessibility service enablement
* TCP vs Content Provider communication
* Wireless debugging and multi-device management
**[Configuration System](/framework/sdk/configuration)** - Master the config-driven architecture
* YAML configuration structure
* LLM profiles per agent (Manager, Executor, FastAgent)
* Mixing LLM providers (OpenAI, Anthropic, Google, Ollama, DeepSeek)
* Prompts and app cards
***
### Templates
Explore real-world examples and starter projects in the [mobilerun-framework-examples repository](https://github.com/droidrun/mobilerun-framework-examples):
* **[LinkedInJobsScraper](https://github.com/droidrun/mobilerun-framework-examples/tree/main/LinkedInJobsScraper)** - Agentic workflow that searches LinkedIn for roles, evaluates matches, and prepares tailored applications
* **[LinkedInLeads](https://github.com/droidrun/mobilerun-framework-examples/tree/main/LinkedInLeads)** - End-to-end lead discovery and enrichment for LinkedIn companies and roles
* **[TwitterPost](https://github.com/droidrun/mobilerun-framework-examples/tree/main/TwitterPost)** - Finds trending topics, drafts posts, and generates images to publish on X/Twitter
* **[play2048](https://github.com/droidrun/mobilerun-framework-examples/tree/main/play2048)** - MobileAgent that plays the 2048 game on play2048.co
Each example includes a self-contained workflow with entrypoint, configuration, and sample data. See the [README](https://github.com/droidrun/mobilerun-framework-examples) for setup instructions and contribution guidelines.
These workflow examples target droidrun framework v0.4.0. See the repository's `legacy/` folder for examples built against earlier versions.
***
Need help? Join our [Discord community](https://discord.gg/ZZbKEZZkwK) for support and discussions.
# Overview
Source: https://docs.mobilerun.ai/framework/overview
Mobilerun is a powerful framework that enables you to control Android and iOS devices through intelligent LLM agents. Build sophisticated mobile automation workflows with natural language commands.
Get up and running with Mobilerun in minutes
View Templates for example Use Cases
Understand the hierarchical agent system
Explore the complete API documentation
## Core Features
Learn about the powerful features that make Mobilerun a comprehensive mobile automation framework. These capabilities enable you to build sophisticated, production-ready automation workflows.
Extract typed data with Pydantic
Store and manage API keys securely
Provide app-specific guidance
Extend capabilities with custom functions
## Runtime
Choose your preferred environment for running Mobilerun automations, from local physical devices to cloud-based solutions.
* Connect your own physical Android device for direct automation
* Access our managed cloud environment for instant mobile app automation without any setup.
# Quickstart
Source: https://docs.mobilerun.ai/framework/quickstart
Get up and running with Mobilerun quickly and effectively
This guide will help you get Mobilerun installed and running quickly, controlling your Android device through natural language in minutes.
This is the **open source Framework**. It runs locally on your machine against your own Android device over adb, and it uses your own LLM provider key such as `GOOGLE_API_KEY` or `OPENAI_API_KEY`. If you would rather use hosted devices and a managed agent with no local setup, use the [Cloud Quickstart](/cloud/quickstart) instead.
### Prerequisites
**Python 3.14 is not yet supported.** Installing Mobilerun on Python 3.14 will silently fall back to an older version (e.g. 0.3.9) due to a dependency that does not yet support Python 3.14. Use Python 3.11, 3.12, or 3.13.
Before installing Mobilerun, ensure you have:
1. **Python 3.11, 3.12, or 3.13** installed on your system
2. [Android Debug Bridge (adb)](https://developer.android.com/studio/releases/platform-tools) installed and configured
3. **Android device** with:
* [Developer options enabled](https://developer.android.com/studio/debug/dev-options)
* USB debugging enabled
* Connected via USB or on the same network (for wireless debugging)
### Installation
Mobilerun is installed using [`uv`](https://docs.astral.sh/uv/), a fast Python package installer and resolver.
**Install uv (if not already installed):**
```bash theme={null}
# macOS/Linux
curl -LsSf https://astral.sh/uv/install.sh | sh
# Windows (PowerShell)
powershell -c "irm https://astral.sh/uv/install.ps1 | iex"
```
**Choose your installation method:**
**For CLI usage only:**
```bash theme={null}
uv tool install mobilerun
```
**For CLI + Python code integration:**
```bash theme={null}
uv pip install mobilerun
```
Most LLM providers (Google Gemini, OpenAI, Ollama, OpenRouter) are included by default. For additional providers, install extras: `uv tool install 'mobilerun[anthropic,deepseek]'`.
### Set Up the Portal APK
Mobilerun requires the Portal app to be installed on your Android device for device control. The Portal app provides accessibility services that expose the UI accessibility tree, enabling the agent to see and interact with UI elements.
```bash theme={null}
mobilerun setup
```
This command automatically:
1. Downloads the latest Portal APK
2. Installs it on your connected device
3. Enables the accessibility service
### Test Connection
Verify that Mobilerun can communicate with your device:
```bash theme={null}
mobilerun ping
```
If successful, you'll see:
```
Portal is installed and accessible. You're good to go!
```
### Configure Your LLM
Run the configure wizard to choose your provider, auth method (API key or OAuth), and model:
```bash theme={null}
mobilerun configure
```
Alternatively, you can set an API key as an environment variable:
```bash theme={null}
# For Google Gemini (default)
export GOOGLE_API_KEY=your-api-key-here
# For OpenAI
export OPENAI_API_KEY=your-api-key-here
# For Anthropic Claude
export ANTHROPIC_API_KEY=your-api-key-here
```
### Run Your First Command via CLI
Now you're ready to control your device with natural language:
```bash theme={null}
# Using default configuration (Google Gemini)
mobilerun run "Open the settings app and tell me the Android version"
# Override provider and model
mobilerun run "Check the battery level" --provider OpenAI --model gpt-4o
# Enable vision mode (sends screenshots to LLM)
mobilerun run "What app is currently open?" --vision
# Enable reasoning mode (uses Manager-Executor workflow for complex tasks)
mobilerun run "Find a contact named John and send him an email" --reasoning
```
**Common CLI flags:**
* `--provider` - LLM provider (GoogleGenAI, OpenAI, Anthropic, etc.)
* `--model` - Model name (gemini-3.1-flash-lite-preview, gpt-4o, etc.)
* `--vision` - Enable screenshot processing
* `--reasoning` - Enable multi-agent planning mode
* `--steps N` - Maximum execution steps (default: 15)
* `--debug` - Enable detailed logging
### Create a Simple Agent via Script
For complex automation or integration into your Python projects, create a script:
```python theme={null}
import asyncio
from mobilerun import MobileAgent, MobileConfig
async def main():
# Use default configuration with built-in LLM profiles
config = MobileConfig()
# Create agent
# LLMs are automatically loaded from config.llm_profiles
agent = MobileAgent(
goal="Open Settings and check battery level",
config=config,
)
# Run agent
result = await agent.run()
# Check results (result is a ResultEvent object)
print(f"Success: {result.success}")
print(f"Reason: {result.reason}")
print(f"Steps: {result.steps}")
if __name__ == "__main__":
asyncio.run(main())
```
## Next Steps
Now that you've got Mobilerun running, explore these topics:
* Walk through a [Guide](/framework/guides/overview)
* Learn about [Agent Architecture](/framework/concepts/architecture)
* Customize the Agent [Configuration System](/framework/sdk/configuration)
* Guide the agent with [App Cards](/framework/features/app-cards)
***
# AndroidDriver
Source: https://docs.mobilerun.ai/framework/sdk/adb-tools
Raw Android device I/O via ADB + Portal.
## AndroidDriver
```python theme={null}
class AndroidDriver(DeviceDriver)
```
Raw Android device I/O via ADB and the Mobilerun Portal app.
AndroidDriver provides low-level device communication for Android devices through ADB (Android Debug Bridge). It supports both TCP communication and content provider modes via the Mobilerun Portal app. AndroidDriver declares its capabilities in the `supported` set, and unsupported methods raise `NotImplementedError`.
#### AndroidDriver.\_\_init\_\_
```python theme={null}
def __init__(
serial: str | None = None,
use_tcp: bool = False,
remote_tcp_port: int = 8080,
) -> None
```
Initialize the AndroidDriver instance.
**Arguments**:
* `serial` *str | None* - Device serial number (e.g., "emulator-5554", "192.168.1.100:5555"). If None, auto-detects the first available device.
* `use_tcp` *bool* - Whether to prefer TCP communication (default: False). TCP is faster but requires port forwarding. Falls back to content provider mode if TCP fails.
* `remote_tcp_port` *int* - TCP port for Portal app communication on device (default: 8080)
**Usage:**
```python theme={null}
from mobilerun.tools import AndroidDriver
# Auto-detect device
driver = AndroidDriver()
# Specific device
driver = AndroidDriver(serial="emulator-5554")
# TCP mode (faster communication, requires port forwarding)
driver = AndroidDriver(serial="emulator-5554", use_tcp=True)
```
**Supported methods:**
```python theme={null}
AndroidDriver.supported = {
"tap", "swipe", "input_text", "press_button", "drag",
"start_app", "install_app", "screenshot",
"get_ui_tree", "get_date", "get_apps", "list_packages",
}
AndroidDriver.supported_buttons = {"back", "home", "enter"}
```
**Notes:**
* Automatically sets up the Mobilerun Portal keyboard on `connect()` via `setup_keyboard()`
* Creates a PortalClient instance that handles TCP/content provider communication
* Device serial can be emulator name, USB serial, or TCP/IP address:port
* Must call `connect()` or `ensure_connected()` before using any methods
***
## Lifecycle Methods
#### AndroidDriver.connect
```python theme={null}
async def connect() -> None
```
Establish connection to the device. Discovers the ADB device, creates a PortalClient, and sets up the Portal keyboard.
**Usage:**
```python theme={null}
driver = AndroidDriver(serial="emulator-5554")
await driver.connect()
```
#### AndroidDriver.ensure\_connected
```python theme={null}
async def ensure_connected() -> None
```
Connect if not already connected. Safe to call multiple times.
***
## Input Action Methods
#### AndroidDriver.tap
```python theme={null}
async def tap(x: int, y: int) -> None
```
Tap at absolute pixel coordinates on the device screen.
**Arguments**:
* `x` *int* - X coordinate
* `y` *int* - Y coordinate
**Usage:**
```python theme={null}
await driver.tap(540, 960)
```
#### AndroidDriver.swipe
```python theme={null}
async def swipe(
x1: int,
y1: int,
x2: int,
y2: int,
duration_ms: float = 1000,
) -> None
```
Swipe from (x1, y1) to (x2, y2).
**Arguments**:
* `x1` *int* - Starting X coordinate
* `y1` *int* - Starting Y coordinate
* `x2` *int* - Ending X coordinate
* `y2` *int* - Ending Y coordinate
* `duration_ms` *float* - Duration of swipe in milliseconds (default: 1000)
**Usage:**
```python theme={null}
# Swipe up (scroll down content)
await driver.swipe(540, 1500, 540, 500, duration_ms=300)
# Swipe left
await driver.swipe(800, 960, 200, 960, duration_ms=250)
```
**Notes:**
* Duration is converted to seconds internally (dividing by 1000)
* Includes an async sleep matching the swipe duration for UI settling
#### AndroidDriver.input\_text
```python theme={null}
async def input_text(text: str, clear: bool = False) -> bool
```
Type text into the currently focused field.
**Arguments**:
* `text` *str* - Text to input. Supports Unicode and special characters.
* `clear` *bool* - Whether to clear existing text before inputting (default: False)
**Returns**:
* `bool` - True if input succeeded, False otherwise
**Usage:**
```python theme={null}
await driver.tap(540, 300) # Focus text field first
success = await driver.input_text("Hello World")
# Clear existing text and input new text
success = await driver.input_text("New text", clear=True)
```
**Notes:**
* Uses the Mobilerun Portal app keyboard for reliable text input via PortalClient
* Supports Unicode characters and special characters
#### AndroidDriver.press\_button
```python theme={null}
async def press_button(button: str) -> None
```
Press a named system button.
**Supported buttons:** `back`, `home`, `enter`
Raises `ValueError` if the button name is not in `supported_buttons`.
**Arguments**:
* `button` *str* - Button name (case-insensitive)
**Usage:**
```python theme={null}
await driver.press_button("enter")
await driver.press_button("home")
await driver.press_button("back")
```
#### AndroidDriver.drag
```python theme={null}
async def drag(
x1: int,
y1: int,
x2: int,
y2: int,
duration: float = 3.0,
) -> None
```
Drag from (x1, y1) to (x2, y2).
**Arguments**:
* `x1` *int* - Starting X coordinate
* `y1` *int* - Starting Y coordinate
* `x2` *int* - Ending X coordinate
* `y2` *int* - Ending Y coordinate
* `duration` *float* - Duration of drag in seconds (default: 3.0)
**Notes:**
* Currently raises `NotImplementedError` (declared in `supported` set but not yet implemented)
***
## App Management Methods
#### AndroidDriver.start\_app
```python theme={null}
async def start_app(package: str, activity: str | None = None) -> str
```
Launch an application on the device.
If activity is not provided, automatically resolves the main/launcher activity using `cmd package resolve-activity`.
**Arguments**:
* `package` *str* - Package name (e.g., "com.android.settings", "com.google.android.apps.messaging")
* `activity` *str | None* - Optional activity name (e.g., ".Settings"). If None, auto-detects the main launcher activity.
**Returns**:
* `str` - Result message indicating success or error
**Usage:**
```python theme={null}
# Auto-detect main activity
result = await driver.start_app("com.android.settings")
# Specific activity
result = await driver.start_app("com.android.settings", ".Settings")
```
#### AndroidDriver.install\_app
```python theme={null}
async def install_app(path: str, **kwargs) -> str
```
Install an APK on the device.
**Arguments**:
* `path` *str* - Path to the APK file on the local machine
* `reinstall` *bool* - Whether to reinstall if app already exists (default: False)
* `grant_permissions` *bool* - Whether to grant all permissions automatically (default: True)
**Returns**:
* `str` - Result message indicating success or error
**Usage:**
```python theme={null}
result = await driver.install_app("/path/to/app.apk")
result = await driver.install_app("/path/to/app.apk", reinstall=True)
```
#### AndroidDriver.list\_packages
```python theme={null}
async def list_packages(include_system: bool = False) -> List[str]
```
Return installed package names.
**Arguments**:
* `include_system` *bool* - Whether to include system apps (default: False)
**Returns**:
* `List[str]` - List of package names
#### AndroidDriver.get\_apps
```python theme={null}
async def get_apps(include_system: bool = True) -> List[Dict[str, str]]
```
Return installed apps as list of dicts with 'package' and 'label' keys.
**Arguments**:
* `include_system` *bool* - Whether to include system apps (default: True)
**Returns**:
* `List[Dict[str, str]]` - List of dictionaries containing 'package' and 'label' keys
***
## State and Observation Methods
#### AndroidDriver.screenshot
```python theme={null}
async def screenshot(hide_overlay: bool = True) -> bytes
```
Capture the current screen as raw PNG bytes.
**Arguments**:
* `hide_overlay` *bool* - Whether to hide Portal app overlay elements during screenshot (default: True)
**Returns**:
* `bytes` - Raw PNG image data
**Usage:**
```python theme={null}
png_bytes = await driver.screenshot()
with open("screenshot.png", "wb") as f:
f.write(png_bytes)
```
#### AndroidDriver.get\_ui\_tree
```python theme={null}
async def get_ui_tree() -> Dict[str, Any]
```
Return the raw UI / accessibility tree from the device.
Returns a dictionary containing both the accessibility tree and phone state data from the Portal app.
**Returns**:
* `Dict[str, Any]` - Raw UI tree data from the device
#### AndroidDriver.get\_date
```python theme={null}
async def get_date() -> str
```
Get the current date and time on the device.
**Returns**:
* `str` - Date and time string from device
**Usage:**
```python theme={null}
date = await driver.get_date()
print(f"Device date: {date}")
# Output: "Thu Jan 16 14:30:25 UTC 2025"
```
***
## Properties
**Instance variables:**
* `device` - ADB device instance (from async\_adbutils)
* `portal` - PortalClient instance for device communication (TCP or content provider mode)
* `supported` - Set of supported method names for capability checking
***
## Notes
* **Portal app required**: The Mobilerun Portal app must be installed and accessibility service enabled on the device
* **TCP vs Content Provider**: TCP is faster but requires port forwarding (`adb forward tcp:8080 tcp:8080`). Content provider is the fallback mode using ADB shell commands.
* **Capability checking**: Check `"method_name" in driver.supported` to determine if a method is available before calling it
* **Async-only**: All methods are async and must be awaited
* **No element resolution**: AndroidDriver provides raw device I/O only. Element resolution (by index) is handled by `UIState` via the `StateProvider` layer.
***
## Example Workflow
```python theme={null}
import asyncio
from mobilerun.tools import AndroidDriver
async def main():
# Initialize driver
driver = AndroidDriver(serial="emulator-5554", use_tcp=True)
await driver.connect()
# Start Chrome app
result = await driver.start_app("com.android.chrome")
print(result)
# Get UI tree (raw data)
tree = await driver.get_ui_tree()
# Tap at coordinates
await driver.tap(540, 300)
# Input text
await driver.input_text("Mobilerun framework")
# Press enter
await driver.press_button("enter")
# Take screenshot
png_bytes = await driver.screenshot()
with open("search_result.png", "wb") as f:
f.write(png_bytes)
asyncio.run(main())
```
**Note:** For higher-level interactions with element indexing and structured results, use action functions with `ActionContext` (see [MobileAgent](/framework/sdk/droid-agent)) rather than calling the driver directly.
# DeviceDriver Base Class
Source: https://docs.mobilerun.ai/framework/sdk/base-tools
Base class defining the interface for all device drivers.
## DeviceDriver
```python theme={null}
class DeviceDriver
```
Base class for all device drivers.
Every method raises `NotImplementedError` by default. Concrete drivers override the methods they support and declare them in the `supported` class-level set. This allows capability checking at runtime without introspection.
***
## Quick Reference
**Driver Methods:**
* `connect()`, `ensure_connected()`, `tap()`, `swipe()`, `input_text()`, `press_button()`, `drag()`, `start_app()`, `install_app()`, `get_apps()`, `list_packages()`, `screenshot()`, `get_ui_tree()`, `get_date()`
**Key Attributes:**
* `supported`: `set[str]` - Set of method names the driver implements. Check membership before calling.
* `supported_buttons`: `set[str]` - Set of button names accepted by `press_button()` (e.g. `{"back", "home", "enter"}`).
***
## Architecture
The tools architecture follows a multi-layer pattern:
1. **DeviceDriver** (`tools/driver/base.py`): Base class for raw device I/O. Methods raise `NotImplementedError` by default.
2. **Driver Implementations**: Platform-specific drivers
* `AndroidDriver` (`tools/driver/android.py`): Android devices via ADB + Portal app
* `IOSDriver` (`tools/driver/ios.py`): iOS devices via HTTP REST API to Portal app
* `StealthDriver` (`tools/driver/stealth.py`): Wraps another driver, adds human-like timing jitter
* `RecordingDriver` (`tools/driver/recording.py`): Wraps another driver with trajectory recording
* `CloudDriver` (`tools/driver/cloud.py`): Cloud-hosted device driver
3. **StateProvider** (`tools/ui/provider.py`): Fetches raw data from a driver, applies filters/formatters, produces `UIState`
4. **UIState** (`tools/ui/state.py`): Parsed UI elements with element resolution (`get_element()`, `get_element_coords()`, `get_element_info()`, `get_clear_point()`, `convert_point()`)
5. **ToolRegistry** (`agent/tool_registry.py`): Central registry of all agent-callable tools
6. **ActionContext** (`agent/action_context.py`): Dependency bag passed as `ctx` kwarg to action functions
7. **ActionResult** (`agent/action_result.py`): Structured return type (`success: bool`, `summary: str`)
**Key Components:**
* **DeviceDriver**: Raw I/O layer, no element indexing, no event emission
* **StateProvider**: Orchestrates fetching and parsing device state into `UIState`
* **UIState**: Element lookup by index, coordinate conversion, formatted text output
* **ActionContext**: Bundles `driver`, `ui`, `shared_state`, `state_provider` for action functions
* **ToolRegistry**: Registers action functions and custom tools for agent use
This design ensures:
* Clean separation between device I/O, UI state management, and agent logic
* Easy addition of new device types by implementing a new driver
* Capability detection via the `supported` set
* Structured results via `ActionResult`
***
## Common Interface
All DeviceDriver implementations may provide these methods (check `supported` set for availability):
### Lifecycle
* `connect() -> None` - Establish connection to the device
* `ensure_connected() -> None` - Connect if not already connected
### Input Actions
* `tap(x: int, y: int) -> None` - Tap at absolute pixel coordinates
* `swipe(x1: int, y1: int, x2: int, y2: int, duration_ms: float = 1000) -> None` - Swipe gesture
* `drag(x1: int, y1: int, x2: int, y2: int, duration: float = 3.0) -> None` - Drag gesture
* `input_text(text: str, clear: bool = False, stealth: bool = False, wpm: int = 0) -> bool` - Text input into focused field. `stealth` enables human-like typing delays; `wpm` sets the typing speed in words per minute (0 = instant).
* `press_button(button: str) -> None` - Press a named button (e.g. back, home, enter). Raises `ValueError` if not in `supported_buttons`.
### App Management
* `start_app(package: str, activity: str | None = None) -> str` - Launch app
* `install_app(path: str, **kwargs) -> str` - Install app
* `list_packages(include_system: bool = False) -> List[str]` - List packages
* `get_apps(include_system: bool = True) -> List[Dict[str, str]]` - Get apps with labels
### State / Observation
* `screenshot(hide_overlay: bool = True) -> bytes` - Capture screen as PNG bytes
* `get_ui_tree() -> Dict[str, Any]` - Get raw UI / accessibility tree
* `get_date() -> str` - Get device date/time
***
## StateProvider
```python theme={null}
class StateProvider:
def __init__(self, driver: DeviceDriver): ...
async def get_state(self) -> UIState: ...
```
Base class for state providers. Subclass to support different platforms. Declares a `supported` set for capability checking (e.g. `{"element_index", "convert_point"}`).
### AndroidStateProvider
```python theme={null}
class AndroidStateProvider(StateProvider)
```
Fetches state from an Android device via `driver.get_ui_tree()`. Includes retry logic (3 attempts). Applies tree filters and formatters to produce a `UIState` snapshot. Constructor accepts `stealth: bool` to select `StealthUIState` (randomized tap coordinates within element bounds) vs regular `UIState`.
***
## UIState
```python theme={null}
class UIState
```
Holds parsed UI elements for a single device state snapshot.
**Key Methods:**
* `get_element(index: int) -> Dict | None` - Recursively find an element by its index
* `get_element_coords(index: int) -> Tuple[int, int]` - Return the centre (x, y) of an element. Raises `ValueError` when element is missing or has no bounds.
* `get_element_info(index: int) -> Dict` - Return element metadata (text, className, type, child\_texts)
* `get_clear_point(index: int) -> Tuple[int, int]` - Find a tap point that avoids overlapping elements (falls back to centre)
* `convert_point(x: int, y: int) -> Tuple[int, int]` - Convert point to absolute pixels if normalized mode is active
**Key Attributes:**
* `elements` - List of parsed UI elements
* `formatted_text` - Formatted text representation of the UI tree
* `focused_text` - Text of the currently focused element
* `phone_state` - Dict with current activity, keyboard visibility, etc.
* `screen_width` / `screen_height` - Device screen dimensions
* `use_normalized` - Whether normalized coordinate mode is active
***
## ActionContext
```python theme={null}
class ActionContext
```
Everything an action function needs to interact with the device.
**Attributes:**
* `driver` - `DeviceDriver` instance for raw device I/O
* `ui` - `UIState` instance for element resolution (refreshed each step)
* `shared_state` - `MobileAgentState` for shared agent state
* `state_provider` - `StateProvider` for fetching fresh UI state
* `app_opener_llm` - LLM instance for app opening workflow (optional)
* `credential_manager` - CredentialManager instance (optional)
* `streaming` - Whether streaming is enabled
***
## ActionResult
```python theme={null}
@dataclass
class ActionResult:
success: bool
summary: str
```
Structured return type from action functions. The `summary` field is what the agent sees.
***
## Action Functions
Action functions live in `agent/utils/actions.py` and follow this pattern:
```python theme={null}
async def click(index: int, *, ctx: ActionContext) -> ActionResult:
"""Click the element with the given index."""
x, y = ctx.ui.get_element_coords(index)
await ctx.driver.tap(x, y)
return ActionResult(success=True, summary=f"Clicked on element at ({x}, {y})")
```
**Available actions:**
* `click(index)` - Click UI element by index
* `click_at(x, y)` - Click at screen coordinates
* `click_area(x1, y1, x2, y2)` - Click center of area defined by coordinates
* `long_press(index)` - Long press UI element by index
* `long_press_at(x, y)` - Long press at screen coordinates
* `type(text, index, clear=False)` - Input text into element (set `clear=True` to clear field first)
* `type_secret(secret_id, index)` - Input a credential secret into element by index
* `swipe(coordinate, coordinate2, duration=1.0)` - Swipe gesture between two coordinate lists
* `system_button(button)` - Press system buttons (back, home, enter)
* `open_app(text)` - Open app by name or description
* `wait(duration=1.0)` - Wait for a duration in seconds
* `remember(information)` - Store info in agent memory
* `complete(success, message)` - Mark task as finished
**Coordinate tools (`click_at`, `click_area`, `long_press_at`) are disabled by default.** Enabling vision auto-unmasks `click_at` on both Android and iOS (non-normalized coordinates only); `vision_only` mode auto-unmasks all three. To enable `click_area` and `long_press_at` under standard vision, set `disabled_tools: []` in your `ToolsConfig`. See [Vision Mode](/framework/features/vision) for details.
***
## ToolRegistry
```python theme={null}
class ToolRegistry
```
Central registry of all agent-callable tools.
**Methods:**
* `register(name, fn, params, description, deps=None)` - Register a single tool with optional capability dependencies
* `register_from_dict(tools_dict)` - Register tools from `{"name": {"parameters": {...}, "description": "...", "function": callable, "deps": set}}` format
* `disable(tool_names)` - Remove tools by name (silently ignores unknown names)
* `disable_unsupported(capabilities)` - Remove tools whose `deps` are not satisfied by the given capabilities set
* `execute(name, args, ctx, workflow_ctx=None)` - Dispatch action by name, returns `ActionResult`
* `get_tool_descriptions_xml(exclude=None)` - Build XML `` block for FastAgent
* `get_tool_descriptions_text(exclude=None)` - Build text descriptions for executor prompts
* `get_param_types(exclude=None)` - Build flat `{param_name: type_string}` map for XML coercion
* `get_signatures(exclude=None)` - Return `{name: {parameters, description}}` dict for prompt building
***
## Custom Tool Integration
### Adding Custom Tools
```python theme={null}
def my_custom_tool(param: str, **kwargs) -> str:
"""Custom tool description."""
return f"Result: {param}"
custom_tools = {
"my_custom_tool": {
"parameters": {
"param": {"type": "string", "required": True},
},
"description": "Custom tool description with usage example",
"function": my_custom_tool
}
}
agent = MobileAgent(
goal="Do something",
config=config,
custom_tools=custom_tools
)
```
***
## Platform Comparison
| Feature | AndroidDriver | IOSDriver |
| ------------- | -------------------------------- | --------------------------------------- |
| Connection | ADB + Portal (USB/TCP) | HTTP (Portal app) |
| tap | Absolute coordinates | Absolute coordinates |
| swipe | Coordinate-based | Direction-based |
| drag | Declared but not yet implemented | Not supported |
| input\_text | With clear support | No clear support |
| press\_button | back, home, enter | home only (back/enter raise ValueError) |
| screenshot | PNG via Portal | PNG via HTTP |
| get\_ui\_tree | Accessibility tree + phone state | Accessibility tree + phone state |
| get\_date | Via ADB shell | Not available (returns empty) |
| get\_apps | Full packages with labels | Bundle identifiers only |
***
## Best Practices
### 1. Check supported methods before calling
```python theme={null}
if "get_date" in driver.supported:
date = await driver.get_date()
else:
date = "Unknown"
```
### 2. Use ActionContext for agent-level interactions
```python theme={null}
# Action functions use ctx for all device interaction
async def my_action(param: str, *, ctx: ActionContext) -> ActionResult:
x, y = ctx.ui.get_element_coords(5)
await ctx.driver.tap(x, y)
return ActionResult(success=True, summary="Done")
```
### 3. Use StateProvider for UI state
```python theme={null}
from mobilerun.tools import AndroidStateProvider, AndroidDriver
# StateProvider handles fetching + parsing + retries
provider = AndroidStateProvider(driver, tree_filter=my_filter, tree_formatter=my_formatter)
ui_state = await provider.get_state()
# UIState provides element lookup
element = ui_state.get_element(5)
x, y = ui_state.get_element_coords(5)
```
***
## Error Handling
Driver methods use consistent error handling:
**Raises NotImplementedError:**
```python theme={null}
# Methods not in `supported` set raise NotImplementedError
try:
await driver.drag(100, 500, 100, 100)
except NotImplementedError:
print("Drag not supported on this driver")
```
**ActionResult for action functions:**
```python theme={null}
result = await click(5, ctx=ctx)
if not result.success:
print(f"Action failed: {result.summary}")
```
***
## See Also
* [AndroidDriver API](/framework/sdk/adb-tools) - Android driver implementation
* [IOSDriver API](/framework/sdk/ios-tools) - iOS driver implementation
* [MobileAgent API](/framework/sdk/droid-agent) - Agent integration
* [Configuration](/framework/sdk/configuration) - Configuration reference
# Configuration
Source: https://docs.mobilerun.ai/framework/sdk/configuration
Complete MobileAgent configuration guide - all parameters, minimal examples
## Quick Start
```python theme={null}
from mobilerun import MobileAgent, MobileConfig
# Minimal (uses defaults)
agent = MobileAgent(goal="Open settings")
result = await agent.run()
# Load from config.yaml
config = MobileConfig.from_yaml("config.yaml")
agent = MobileAgent(goal="Open settings", config=config)
result = await agent.run()
```
***
## MobileAgent Parameters
### Required
```python theme={null}
MobileAgent(
goal="Your task", # REQUIRED: Task description
)
```
### Optional Parameters
| Parameter | Type | Default | Description |
| ---------------- | -------------------------------------- | ------- | ---------------------------------------------------------------------------- |
| `config` | `MobileConfig \| None` | `None` | Full config object (loads LLMs from profiles if `llms` not provided) |
| `llms` | `dict[str, LLM] \| LLM \| None` | `None` | LLM(s) - dict for per-agent, single LLM for all, or None to load from config |
| `custom_tools` | `dict` | `None` | Custom tool definitions |
| `credentials` | `Union[dict, CredentialManager, None]` | `None` | Dict of credential secrets or CredentialManager instance |
| `variables` | `dict \| None` | `None` | Custom variables accessible during execution |
| `output_model` | `Type[BaseModel] \| None` | `None` | Pydantic model for structured output extraction |
| `prompts` | `dict[str, str] \| None` | `None` | Custom Jinja2 prompt templates (NOT file paths) |
| `driver` | `DeviceDriver \| None` | `None` | Pre-configured device driver instance (AndroidDriver or IOSDriver) |
| `state_provider` | `StateProvider \| None` | `None` | Pre-configured state provider instance |
| `timeout` | `int` | `1000` | Workflow timeout in seconds |
***
## Configuration Classes
### AgentConfig
```python theme={null}
from mobilerun import AgentConfig, FastAgentConfig, ManagerConfig, ExecutorConfig, AppCardConfig
AgentConfig(
# Core settings
name="mobilerun", # Agent name (use "mobilerun" for default agent)
max_steps=15, # Max execution steps
reasoning=False, # Enable Manager/Executor workflow
streaming=True, # Stream LLM responses
after_sleep_action=1.0, # Wait after actions (seconds)
wait_for_stable_ui=0.3, # Wait for UI to stabilize (seconds)
use_normalized_coordinates=False, # Use normalized coordinates instead of absolute pixels
model_screenshot_max_side=None, # Optional cap (px, long edge) on the model-facing screenshot
# Sub-configs
fast_agent=FastAgentConfig(...),
manager=ManagerConfig(...),
executor=ExecutorConfig(...),
app_cards=AppCardConfig(...),
)
```
**FastAgentConfig**
```python theme={null}
FastAgentConfig(
vision=False, # Enable screenshots
parallel_tools=True, # Encourage multiple tool calls in a single response
system_prompt="config/prompts/fast_agent/system.jinja2", # Path to system prompt template
user_prompt="config/prompts/fast_agent/user.jinja2", # Path to user prompt template
)
```
**ManagerConfig**
```python theme={null}
ManagerConfig(
vision=False, # Enable screenshots
system_prompt="config/prompts/manager/system.jinja2", # Path to system prompt template
stateless=False, # Use StatelessManagerAgent (no conversation history)
)
```
**ExecutorConfig**
```python theme={null}
ExecutorConfig(
vision=False, # Enable screenshots
system_prompt="config/prompts/executor/system.jinja2", # Path to system prompt template
)
```
**AppCardConfig**
```python theme={null}
AppCardConfig(
enabled=True, # Enable app-specific instructions
mode="local", # "local" | "server" | "composite"
app_cards_dir="config/app_cards", # Directory for app card files
server_url=None, # Server URL (for server/composite modes)
server_timeout=2.0, # Server request timeout (seconds)
server_max_retries=2, # Server retry attempts
)
```
***
### DeviceConfig
```python theme={null}
from mobilerun import DeviceConfig
DeviceConfig(
serial=None, # Device serial/IP (None = auto-detect)
use_tcp=False, # TCP vs content provider communication
platform="android", # "android" or "ios"
auto_setup=True, # Auto-install/fix Portal APK before each run
auth_token=None, # Bearer token for a mobilerun-ios --local server
# started with --local-token. Loopback servers need
# no token. MOBILERUN_DEVICE_TOKEN env overrides this.
)
```
***
### LoggingConfig
```python theme={null}
from mobilerun import LoggingConfig
LoggingConfig(
debug=False, # Enable debug logs
save_trajectory="none", # "none" | "step" | "action"
trajectory_path="trajectories", # Directory for trajectory files
rich_text=False, # Rich text formatting in logs
trajectory_gifs=True, # Save trajectory as animated GIFs
)
```
***
### TracingConfig
```python theme={null}
from mobilerun import TracingConfig
TracingConfig(
enabled=False, # Enable tracing
provider="phoenix", # "phoenix" or "langfuse"
langfuse_screenshots=False, # Upload screenshots to Langfuse (if enabled)
# Langfuse settings (only used if provider="langfuse")
langfuse_secret_key="", # LANGFUSE_SECRET_KEY env var
langfuse_public_key="", # LANGFUSE_PUBLIC_KEY env var
langfuse_host="", # LANGFUSE_HOST env var (e.g., "https://cloud.langfuse.com")
langfuse_user_id="anonymous", # User ID for Langfuse tracing
langfuse_session_id="", # Empty = auto-generate UUID; custom value to persist across runs
)
```
***
### TelemetryConfig
```python theme={null}
from mobilerun import TelemetryConfig
TelemetryConfig(
enabled=True, # Enable anonymous telemetry
)
```
***
### ToolsConfig
```python theme={null}
from mobilerun import ToolsConfig
ToolsConfig(
disabled_tools=["click_at", "click_area", "long_press_at"], # Tools to disable (default disables coordinate-based tools)
stealth=False, # Enable stealth mode (human-like timing + randomized coordinates)
)
```
**Example - Disable specific tools:**
```yaml theme={null}
tools:
disabled_tools:
- long_press
- wait
stealth: false
```
Disabled tools will not be available to agents during execution. The default `disabled_tools` list disables `click_at`, `click_area`, and `long_press_at`. Enabling vision auto-unmasks `click_at` on both Android and iOS (non-normalized coordinates only); `vision_only` mode auto-unmasks all three. To enable `click_area` and `long_press_at` under standard vision, set `disabled_tools: []`. See [Vision Mode](/framework/features/vision) for details.
***
### CredentialsConfig
```python theme={null}
from mobilerun import CredentialsConfig
CredentialsConfig(
enabled=True, # Enable credential manager
file_path="config/credentials.yaml", # Path to credentials file
)
```
***
## LLM Configuration
### Single LLM (All Agents)
```python theme={null}
from llama_index.llms.google_genai import GoogleGenAI
llm = GoogleGenAI(model="gemini-3.1-flash-lite-preview", temperature=0.2)
agent = MobileAgent(goal="...", llms=llm)
```
### Per-Agent LLMs
```python theme={null}
from llama_index.llms.openai import OpenAI
from llama_index.llms.google_genai import GoogleGenAI
agent = MobileAgent(
goal="...",
llms={
"manager": OpenAI(model="gpt-4o"), # Planning
"executor": GoogleGenAI(model="gemini-3.1-flash-lite-preview"), # Action selection
"fast_agent": GoogleGenAI(model="gemini-3.1-flash-lite-preview"), # Fast Agent: Direct execution (XML tool-calling)
"app_opener": OpenAI(model="gpt-4o-mini"), # App launching
"structured_output": GoogleGenAI(model="gemini-3.1-flash-lite-preview"), # Output extraction
}
)
```
**LLM Keys:**
* `manager` - Planning (reasoning mode only)
* `executor` - Action selection (reasoning mode only)
* `fast_agent` - Fast Agent: Direct execution (XML tool-calling)
* `app_opener` - App launching helper
* `structured_output` - Final output extraction
***
## Custom Tools
```python theme={null}
def my_tool(param: str, **kwargs) -> str:
"""Tool description."""
return f"Result: {param}"
agent = MobileAgent(
goal="...",
custom_tools={
"my_tool": {
"parameters": {
"param": {"type": "string", "required": True},
},
"description": "Tool description with usage example",
"function": my_tool
}
}
)
```
***
## Credentials
### Dict Format (Recommended)
```python theme={null}
agent = MobileAgent(
goal="...",
credentials={
"USERNAME": "alice@example.com",
"PASSWORD": "secret123"
}
)
# Agent can call type_secret("USERNAME", index) and type_secret("PASSWORD", index)
```
### Config Format
```python theme={null}
from mobilerun import MobileConfig, CredentialsConfig
config = MobileConfig(
credentials=CredentialsConfig(
enabled=True,
file_path="config/credentials.yaml"
)
)
agent = MobileAgent(
goal="...",
config=config
)
```
***
## Custom Variables
```python theme={null}
agent = MobileAgent(
goal="...",
variables={
"api_url": "https://api.example.com",
"user_id": "12345",
"custom_data": {"key": "value"}
}
)
# Access in shared_state.custom_variables
```
***
## Structured Output
```python theme={null}
from pydantic import BaseModel
class FlightInfo(BaseModel):
airline: str
flight_number: str
confirmation_code: str
agent = MobileAgent(
goal="Book a flight and extract details",
output_model=FlightInfo
)
result = await agent.run()
print(result.structured_output.airline) # Typed output
```
***
## Custom Prompts
```python theme={null}
custom_prompt = """
You are an expert mobile agent.
Goal: {{ instruction }}
Be precise and efficient.
"""
agent = MobileAgent(
goal="...",
prompts={
"fast_agent_system": custom_prompt,
"fast_agent_user": "...",
"manager_system": "...",
"executor_system": "...",
}
)
```
**Template Variables:**
* `{{ instruction }}` - User's goal
* `{{ device_date }}` - Device date/time
* `{{ app_card }}` - App-specific instructions
* `{{ state }}` - Device state
* `{{ history }}` - Action history
***
## Complete Example
```python theme={null}
from mobilerun import (
MobileAgent, MobileConfig,
AgentConfig, FastAgentConfig, DeviceConfig, LoggingConfig, TracingConfig
)
from llama_index.llms.openai import OpenAI
from llama_index.llms.google_genai import GoogleGenAI
from pydantic import BaseModel
# Structured output
class Output(BaseModel):
name: str
value: int
# Custom tool
def send_email(to: str, subject: str, **kwargs) -> str:
"""Send email."""
return f"Sent to {to}"
# Build configuration
config = MobileConfig(
agent=AgentConfig(
max_steps=30,
reasoning=True,
after_sleep_action=1.5,
fast_agent=FastAgentConfig(vision=True)
),
device=DeviceConfig(
serial="emulator-5554",
platform="android",
use_tcp=False
),
logging=LoggingConfig(
debug=True,
save_trajectory="step",
trajectory_gifs=True
),
tracing=TracingConfig(enabled=True),
)
agent = MobileAgent(
goal="Complex task",
config=config,
# LLMs
llms={
"manager": OpenAI(model="gpt-4o"), # Planning
"executor": GoogleGenAI(model="gemini-3.1-flash-lite-preview"), # Action selection
"fast_agent": GoogleGenAI(model="gemini-3.1-flash-lite-preview"), # Fast Agent: Direct execution (XML tool-calling)
"app_opener": OpenAI(model="gpt-4o-mini"), # App launching
"structured_output": GoogleGenAI(model="gemini-3.1-flash-lite-preview"), # Output extraction
},
# Custom tools
custom_tools={
"send_email": {
"parameters": {
"to": {"type": "string", "required": True},
"subject": {"type": "string", "required": True},
},
"description": "Send email to recipient with subject",
"function": send_email
}
},
# Credentials
credentials={"USERNAME": "alice", "PASSWORD": "secret"},
# Variables
variables={"api_url": "https://api.example.com"},
# Structured output
output_model=Output,
# Timeout
timeout=600
)
result = await agent.run()
```
***
## YAML Config (CLI)
For CLI usage, create `config.yaml`:
```yaml theme={null}
agent:
name: mobilerun
max_steps: 15
reasoning: false
streaming: true
after_sleep_action: 1.0
wait_for_stable_ui: 0.3
use_normalized_coordinates: false
# model_screenshot_max_side: 1280 # Optional cap for local vision models
fast_agent:
vision: false
parallel_tools: true
system_prompt: config/prompts/fast_agent/system.jinja2
user_prompt: config/prompts/fast_agent/user.jinja2
manager:
vision: false
system_prompt: config/prompts/manager/system.jinja2
stateless: false
executor:
vision: false
system_prompt: config/prompts/executor/system.jinja2
app_cards:
enabled: true
mode: local
app_cards_dir: config/app_cards
server_url: null
server_timeout: 2.0
server_max_retries: 2
llm_profiles:
manager:
provider: GoogleGenAI
model: gemini-3.1-flash-lite-preview
temperature: 0.2
kwargs:
max_tokens: 8192
executor:
provider: GoogleGenAI
model: gemini-3.1-flash-lite-preview
temperature: 0.1
kwargs:
max_tokens: 4096
fast_agent: # Fast Agent: Direct execution (XML tool-calling)
provider: GoogleGenAI
model: gemini-3.1-flash-lite-preview
temperature: 0.2
kwargs:
max_tokens: 8192
app_opener:
provider: OpenAI
model: gpt-4o-mini
temperature: 0.0
structured_output:
provider: GoogleGenAI
model: gemini-3.1-flash-lite-preview
temperature: 0.0
device:
serial: null
platform: android
use_tcp: false
auto_setup: true # Auto-install/fix Portal APK before each run
auth_token: null # Token for mobilerun-ios --local (iOS only).
# MOBILERUN_DEVICE_TOKEN env var overrides this.
tools:
disabled_tools:
- click_at
- click_area
- long_press_at
stealth: false
telemetry:
enabled: true
tracing:
enabled: false
provider: phoenix # "phoenix" or "langfuse"
langfuse_screenshots: false # Upload screenshots to Langfuse
langfuse_secret_key: ""
langfuse_public_key: ""
langfuse_host: ""
langfuse_user_id: anonymous
langfuse_session_id: ""
logging:
debug: false
save_trajectory: none
trajectory_path: trajectories
rich_text: false
trajectory_gifs: true
credentials:
enabled: false
file_path: config/credentials.yaml
```
### Ollama profiles
Ollama profiles accept the same portable `kwargs` as other providers — `max_tokens` and `context_window` work consistently across providers, and Mobilerun translates them to Ollama's native parameters at runtime.
```yaml theme={null}
llm_profiles:
manager:
provider: Ollama
model: qwen3:8b
base_url: http://localhost:11434
kwargs:
max_tokens: 2048 # Caps output length (mapped to Ollama's num_predict)
context_window: 32768 # Controls KV cache size (mapped to num_ctx)
```
| Key | Default for Ollama | Notes |
| ---------------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `max_tokens` | unset (no cap) | Caps generated tokens. If you set `additional_kwargs.num_predict`, that explicit value wins. |
| `context_window` | `32768` | KV cache size. Set `-1` to use the model's maximum (preallocates the full KV cache and can spill to CPU on large-context models). An explicit `additional_kwargs.num_ctx` is mirrored into `context_window` automatically. |
The 32K default keeps Ollama on the GPU for most models. If you need the model's full context, set `context_window: -1` explicitly.
***
## CLI Overrides
```bash theme={null}
# Override agent settings
mobilerun run "Task" --steps 30 --reasoning --vision
# Override device
mobilerun run "Task" --device emulator-5554 --tcp
# Override LLM (applies to ALL agents)
mobilerun run "Task" --provider GoogleGenAI --model gemini-3.1-flash-lite-preview
# Override logging
mobilerun run "Task" --debug --save-trajectory action --tracing
# Custom config file
mobilerun run "Task" --config /path/to/config.yaml
```
**All CLI Flags:**
* `--config PATH` - Custom config file
* `--device SERIAL` - Device serial/IP
* `--agent NAME` - External agent to use. Not yet supported — reserved for future use.
* `--provider PROVIDER` - LLM provider (OpenAI, Ollama, Anthropic, GoogleGenAI, DeepSeek)
* `--model MODEL` - LLM model name
* `--temperature FLOAT` - LLM temperature
* `--steps INT` - Max steps
* `--base_url URL` - API base URL (for Ollama/OpenRouter)
* `--api_base URL` - API base URL (for OpenAI-like)
* `--vision/--no-vision` - Enable/disable vision for all agents
* `--reasoning/--no-reasoning` - Enable/disable reasoning mode
* `--tracing/--no-tracing` - Enable/disable tracing
* `--debug/--no-debug` - Enable/disable debug logs
* `--tcp/--no-tcp` - Enable/disable TCP communication
* `--save-trajectory none|step|action` - Trajectory saving level
* `--ios` - Run on iOS device
***
## Environment Variables
Set API keys via environment variables:
```bash theme={null}
export GOOGLE_API_KEY=your-key
export OPENAI_API_KEY=your-key
export ANTHROPIC_API_KEY=your-key
export DEEPSEEK_API_KEY=your-key
export MOBILERUN_CONFIG=/path/to/config.yaml # Custom config path
```
# MobileAgent
Source: https://docs.mobilerun.ai/framework/sdk/droid-agent
MobileAgent - A wrapper class that coordinates the planning and execution of tasks to achieve a user's goal on an Android or iOS device.
## MobileAgent
```python theme={null}
class MobileAgent(Workflow)
```
A wrapper class that coordinates between agents to achieve a user's goal.
**Architecture:**
* When `reasoning=False`: Uses FastAgent directly for immediate execution
* When `reasoning=True`: Uses ManagerAgent (planning) + ExecutorAgent (actions)
#### MobileAgent.\_\_init\_\_
```python theme={null}
def __init__(
goal: str,
config: MobileConfig | None = None,
llms: dict[str, LLM] | LLM | None = None,
custom_tools: dict = None,
credentials: Union[dict, CredentialManager, None] = None,
variables: dict | None = None,
output_model: Type[BaseModel] | None = None,
prompts: dict[str, str] | None = None,
driver: "DeviceDriver | None" = None,
state_provider: "StateProvider | None" = None,
timeout: int = 1000,
)
```
Initialize the MobileAgent wrapper.
**Arguments**:
* `goal` *str* - User's goal or command to execute
* `config` *MobileConfig | None* - Full configuration object (required if llms not provided). Contains agent settings, LLM profiles, device config, and more.
* `llms` *dict\[str, LLM] | LLM | None* - Optional LLM configuration:
* `dict[str, LLM]`: Agent-specific LLMs with keys: "manager", "executor", "fast\_agent", "app\_opener", "structured\_output"
* `LLM`: Single LLM instance used for all agents
* `None`: LLMs will be loaded from config.llm\_profiles
* `custom_tools` *dict* - Custom tool definitions. Format: `{"tool_name": {"parameters": {...}, "description": "...", "function": callable}}`. These are merged with auto-generated credential tools.
* `credentials` *Union\[dict, CredentialManager, None]* - Direct credential mapping `{"SECRET_ID": "value"}`, a CredentialManager instance, or None. If None, credentials will be loaded from config.credentials if available.
* `variables` *dict | None* - Custom variables accessible throughout execution. Available in shared\_state.custom\_variables.
* `output_model` *Type\[BaseModel] | None* - Pydantic model for structured output extraction from final answer. If provided, the final answer will be parsed into this model.
* `prompts` *dict\[str, str] | None* - Custom Jinja2 prompt templates to override defaults. Keys: "fast\_agent\_system", "fast\_agent\_user", "manager\_system", "executor\_system". Values: Jinja2 template strings (NOT file paths).
* `driver` *DeviceDriver | None* - Pre-configured device driver instance (AndroidDriver or IOSDriver). If None, a driver will be created from config.
* `state_provider` *StateProvider | None* - Pre-configured state provider instance. If None, a state provider will be created from config.
* `timeout` *int* - Workflow timeout in seconds (default: 1000)
**Basic initialization pattern (recommended):**
```python theme={null}
from mobilerun import MobileAgent
from mobilerun.config_manager import MobileConfig
# Initialize with default config
config = MobileConfig()
# Create agent (LLMs loaded from config.llm_profiles)
agent = MobileAgent(
goal="Open Chrome and search for Mobilerun",
config=config
)
# Run agent
result = await agent.run()
```
**Loading from YAML (optional):**
```python theme={null}
from mobilerun import MobileAgent
from mobilerun.config_manager import MobileConfig
# Load config from config.yaml
config = MobileConfig.from_yaml("config.yaml")
# Create agent (LLMs loaded from config.llm_profiles)
agent = MobileAgent(
goal="Open Chrome and search for Mobilerun",
config=config
)
# Run agent
result = await agent.run()
```
**Custom LLM dictionary pattern:**
```python theme={null}
from mobilerun import MobileAgent
from mobilerun.config_manager import MobileConfig
from llama_index.llms.openai import OpenAI
from llama_index.llms.anthropic import Anthropic
# Initialize config
config = MobileConfig()
# Create custom LLMs
llms = {
"manager": Anthropic(model="claude-sonnet-4-5-latest", temperature=0.2),
"executor": Anthropic(model="claude-sonnet-4-5-latest", temperature=0.1),
"fast_agent": OpenAI(model="gpt-4o", temperature=0.2),
"app_opener": OpenAI(model="gpt-4o-mini", temperature=0.0),
"structured_output": OpenAI(model="gpt-4o-mini", temperature=0.0),
}
# Create agent with custom LLMs
agent = MobileAgent(
goal="Send a message to John",
llms=llms,
config=config
)
result = await agent.run()
```
**Single LLM pattern:**
```python theme={null}
from mobilerun import MobileAgent
from mobilerun.config_manager import MobileConfig
from llama_index.llms.openai import OpenAI
# Initialize config
config = MobileConfig()
# Use same LLM for all agents
llm = OpenAI(model="gpt-4o", temperature=0.2)
agent = MobileAgent(
goal="Take a screenshot and save it",
llms=llm,
config=config
)
result = await agent.run()
```
**Custom tools and credentials:**
```python theme={null}
from mobilerun import MobileAgent
from mobilerun.config_manager import MobileConfig
# Initialize config
config = MobileConfig()
# Define custom tool
def search_database(query: str) -> str:
"""Search the local database."""
# Your implementation
return f"Results for: {query}"
custom_tools = {
"search_database": {
"parameters": {
"query": {"type": "string", "required": True},
},
"description": "Search the local database for information",
"function": search_database
}
}
# Provide credentials directly
credentials = {
"GMAIL_USERNAME": "user@gmail.com",
"GMAIL_PASSWORD": "secret123"
}
agent = MobileAgent(
goal="Search database and email results",
config=config,
custom_tools=custom_tools,
credentials=credentials
)
result = await agent.run()
```
**Structured output extraction:**
```python theme={null}
from mobilerun import MobileAgent
from mobilerun.config_manager import MobileConfig
from pydantic import BaseModel, Field
# Initialize config
config = MobileConfig()
# Define output schema
class WeatherInfo(BaseModel):
"""Weather information."""
temperature: float = Field(description="Temperature in Celsius")
condition: str = Field(description="Weather condition")
humidity: int = Field(description="Humidity percentage")
agent = MobileAgent(
goal="Open weather app and get current weather",
config=config,
output_model=WeatherInfo
)
result = await agent.run()
# Access structured output
if result.success and result.structured_output:
weather = result.structured_output # WeatherInfo object
print(f"Temperature: {weather.temperature}°C")
print(f"Condition: {weather.condition}")
```
#### MobileAgent.run
```python theme={null}
async def run(*args, **kwargs) -> ResultEvent
```
Run the MobileAgent workflow.
**Returns**:
* `ResultEvent` - Result object with the following attributes:
* `success` (bool): True if task completed successfully
* `reason` (str): Success message or failure reason
* `steps` (int): Number of steps executed
* `structured_output` (Any): Parsed Pydantic model (if output\_model provided, otherwise None)
**Usage:**
```python theme={null}
from mobilerun import MobileAgent
from mobilerun.config_manager import MobileConfig
# Initialize config
config = MobileConfig()
# Create and run agent
agent = MobileAgent(goal="...", config=config)
result = await agent.run()
print(f"Success: {result.success}")
print(f"Reason: {result.reason}")
print(f"Steps: {result.steps}")
```
**Streaming events:**
```python theme={null}
from mobilerun import MobileAgent
from mobilerun.config_manager import MobileConfig
# Initialize config
config = MobileConfig()
agent = MobileAgent(goal="...", config=config)
# Stream events as they occur
async for event in agent.run_event_stream():
if isinstance(event, ManagerInputEvent):
print("Manager is planning...")
elif isinstance(event, ExecutorInputEvent):
print("Executor is taking action...")
elif isinstance(event, ToolExecutionEvent):
print(f"Tool executed: {event.tool_name} - {event.summary}")
elif isinstance(event, ResultEvent):
# Final result
print(f"Success: {event.success}")
print(f"Reason: {event.reason}")
```
#### MobileAgent.send\_user\_message
```python theme={null}
def send_user_message(message: str) -> QueuedUserMessage
```
Inject an external user message into the running workflow. The message is queued and consumed by the active agent (FastAgent or Manager) at its next step.
**Arguments**:
* `message` *str* - The message to inject
**Returns**:
* `QueuedUserMessage` - Object with `id`, `message`, and `queued_at_step` fields
**Usage:**
```python theme={null}
import asyncio
agent = MobileAgent(goal="...", config=config)
# Start the agent as a background task so it begins executing
task = asyncio.create_task(agent.run())
# Wait for the agent to be running before injecting messages.
# In practice, call send_user_message from an external trigger
# (e.g., a UI callback or API endpoint) once the agent is active.
await asyncio.sleep(1)
# Inject a message mid-run
queued = agent.send_user_message("Actually, search for 'Python' instead")
result = await task
```
If the agent has already finished or the message arrives at `max_steps`, the message will be dropped and an `ExternalUserMessageDroppedEvent` is emitted. Pending messages also block `complete()` until they are consumed.
## Event Types
MobileAgent emits various events during execution:
**Workflow Events:**
* `StartEvent` - Workflow started
* `ManagerInputEvent` - Manager planning phase started
* `ManagerContextEvent` - Manager received context for planning
* `ManagerResponseEvent` - Manager intermediate response
* `ManagerPlanEvent` - Manager created a plan
* `ManagerPlanDetailsEvent` - Manager plan details
* `ExecutorInputEvent` - Executor action phase started
* `ExecutorContextEvent` - Executor received context
* `ExecutorResponseEvent` - Executor intermediate response
* `ExecutorActionEvent` - Executor action details
* `ExecutorActionResultEvent` - Executor action result details
* `ExecutorResultEvent` - Executor completed an action
* `ExternalUserMessageAppliedEvent` - External message consumed by agent
* `ExternalUserMessageDroppedEvent` - External message dropped (e.g., at max steps)
* `FastAgentExecuteEvent` - FastAgent started (direct mode)
* `FastAgentResultEvent` - FastAgent completed
* `FinalizeEvent` - Workflow finalizing
* `StopEvent` - Workflow completed
**Common Events:**
* `ToolExecutionEvent` - Emitted by ToolRegistry after every tool dispatch (contains `tool_name`, `tool_args`, `success`, `summary`)
* `ScreenshotEvent` - Screenshot captured
* `RecordUIStateEvent` - UI state recorded
## Configuration
MobileAgent uses a hierarchical configuration system. See the [Configuration Guide](/framework/sdk/configuration) for details.
**Key configuration options:**
```yaml theme={null}
agent:
max_steps: 15 # Maximum execution steps
reasoning: false # Enable Manager/Executor workflow
fast_agent:
vision: false # Enable screenshot analysis
manager:
vision: false # Enable screenshot analysis
executor:
vision: false # Enable screenshot analysis
device:
serial: null # Device serial (null = auto-detect)
platform: android # "android" or "ios"
use_tcp: false # TCP vs content provider
logging:
debug: false # Debug logging
save_trajectory: none # Trajectory saving: "none", "step", "action"
tracing:
enabled: false # Arize Phoenix tracing
```
## Advanced Usage
**Custom Tools instance:**
```python theme={null}
from mobilerun import MobileAgent, DeviceConfig
from mobilerun.config_manager import MobileConfig
# Initialize config with device settings
device_config = DeviceConfig(serial="emulator-5554", use_tcp=True)
config = MobileConfig(device=device_config)
agent = MobileAgent(
goal="Open settings",
config=config,
)
result = await agent.run()
```
**Custom variables:**
```python theme={null}
from mobilerun import MobileAgent
from mobilerun.config_manager import MobileConfig
# Initialize config
config = MobileConfig()
agent = MobileAgent(
goal="Complete task using context",
config=config,
variables={
"user_name": "Alice",
"project_id": "12345",
"api_endpoint": "https://api.example.com"
}
)
result = await agent.run()
```
Variables are accessible in shared\_state.custom\_variables throughout execution and can be referenced in custom tools or scripts.
**Custom prompts:**
```python theme={null}
from mobilerun import MobileAgent
from mobilerun.config_manager import MobileConfig
# Initialize config
config = MobileConfig()
# Override default prompts with custom Jinja2 templates
custom_prompts = {
"fast_agent_system": "You are a specialized agent for {{ platform }} devices...",
"manager_system": "You are a planning agent. Your goal: {{ instruction }}..."
}
agent = MobileAgent(
goal="Complete specialized task",
config=config,
prompts=custom_prompts
)
result = await agent.run()
```
Available prompt keys: "fast\_agent\_system", "fast\_agent\_user", "manager\_system", "executor\_system"
## Notes
* **Config requirement**: Either `config` or `llms` must be provided. If `llms` is not provided, `config` is required to load LLMs from profiles.
* **Vision mode**: Enabling vision (agent\_config.\*.vision = True) increases token usage as screenshots are sent to the LLM.
* **Reasoning mode**: `reasoning=True` uses Manager/Executor workflow for complex planning. `reasoning=False` uses FastAgent for direct execution.
* **Timeout**: Default is 1000 seconds. Increase for long-running tasks.
* **Credentials**: When credentials are provided, the `type_secret(secret_id, index)` tool is automatically registered. The agent never sees the actual secret values, only the secret IDs.
# IOSDriver
Source: https://docs.mobilerun.ai/framework/sdk/ios-tools
# IOSDriver API Reference
## IOSDriver
```python theme={null}
class IOSDriver(DeviceDriver)
```
iOS device driver communicating via HTTP REST to the iOS Portal app.
**Unsupported methods**:
* `drag()` — not in `supported` set
* `install_app()` — not in `supported` set
* `press_button()` — `home` only
#### IOSDriver.\_\_init\_\_
```python theme={null}
def __init__(
url: str,
bundle_identifiers: List[str] | None = None
) -> None
```
Initialize the IOSDriver instance.
**Arguments**:
* `url` *str* - iOS Portal URL (e.g., "[http://127.0.0.1:6643](http://127.0.0.1:6643)")
* `bundle_identifiers` *List\[str] | None* - Optional list of custom app bundle identifiers
**Usage:**
```python theme={null}
from mobilerun.tools.driver import IOSDriver
# Connect via iproxy
driver = IOSDriver(url="http://127.0.0.1:6643")
# With specific bundle identifiers
driver = IOSDriver(
url="http://127.0.0.1:6643",
bundle_identifiers=["com.example.app1", "com.example.app2"]
)
```
**Supported methods:**
```python theme={null}
IOSDriver.supported = {
"tap", "swipe", "input_text", "press_button",
"start_app", "screenshot", "get_ui_tree",
"list_packages", "get_apps", "get_date",
}
IOSDriver.supported_buttons = {"home"}
```
**Setup Requirements:**
1. Build and run the iOS Portal via Xcode (runs as a UI test)
2. Forward port with `iproxy 6643 6643`
3. Use `http://127.0.0.1:6643` as the URL
See [Device Setup — iOS](/framework/guides/device-setup) for full instructions.
***
## Lifecycle Methods
#### IOSDriver.connect
```python theme={null}
async def connect() -> None
```
Create an HTTP client and verify connectivity by calling `/device/date`.
#### IOSDriver.ensure\_connected
```python theme={null}
async def ensure_connected() -> None
```
Connect if not already connected. Safe to call multiple times.
***
## Input Action Methods
#### IOSDriver.tap
```python theme={null}
async def tap(x: int, y: int) -> None
```
Tap at coordinates.
**Arguments**:
* `x` *int* - X coordinate
* `y` *int* - Y coordinate
**Usage:**
```python theme={null}
await driver.tap(200, 400)
```
#### IOSDriver.swipe
```python theme={null}
async def swipe(
x1: int,
y1: int,
x2: int,
y2: int,
duration_ms: float = 1000,
) -> None
```
Swipe from one point to another.
**Arguments**:
* `x1` *int* - Starting X coordinate
* `y1` *int* - Starting Y coordinate
* `x2` *int* - Ending X coordinate
* `y2` *int* - Ending Y coordinate
* `duration_ms` *float* - Duration in milliseconds
**Usage:**
```python theme={null}
# Swipe up (scroll down)
await driver.swipe(200, 800, 200, 200)
# Swipe left
await driver.swipe(600, 400, 100, 400)
```
#### IOSDriver.input\_text
```python theme={null}
async def input_text(text: str, clear: bool = False) -> bool
```
Input text into the currently focused element.
**Arguments**:
* `text` *str* - Text to input (supports Unicode)
* `clear` *bool* - Clear existing text before input
**Returns**:
* `bool` - True if input succeeded, False otherwise
**Usage:**
```python theme={null}
# Tap text field first
await driver.tap(200, 400)
# Input text
success = await driver.input_text("Hello World")
# Clear field and type new text
success = await driver.input_text("new text", clear=True)
```
#### IOSDriver.press\_button
```python theme={null}
async def press_button(button: str) -> None
```
Press a named system button.
**Supported buttons:** `home`
Raises `ValueError` for unsupported button names.
**Arguments**:
* `button` *str* - Button name (case-insensitive)
**Usage:**
```python theme={null}
await driver.press_button("home")
```
***
## App Management Methods
#### IOSDriver.start\_app
```python theme={null}
async def start_app(package: str, activity: str | None = None) -> str
```
Launch an app by bundle identifier.
**Arguments**:
* `package` *str* - Bundle identifier (e.g., "com.apple.MobileSMS")
* `activity` *str | None* - Ignored on iOS (for API compatibility)
**Returns**:
* `str` - Result message
**Common bundle identifiers:**
* Messages: `com.apple.MobileSMS`
* Safari: `com.apple.mobilesafari`
* Settings: `com.apple.Preferences`
* Calendar: `com.apple.mobilecal`
* Photos: `com.apple.mobileslideshow`
* Maps: `com.apple.Maps`
* Contacts: `com.apple.MobileAddressBook`
**Usage:**
```python theme={null}
result = await driver.start_app("com.apple.MobileSMS")
result = await driver.start_app("com.apple.mobilesafari")
```
#### IOSDriver.list\_packages
```python theme={null}
async def list_packages(include_system: bool = False) -> List[str]
```
List known bundle identifiers.
**Arguments**:
* `include_system` *bool* - Include system apps (default: False)
**Returns**:
* `List[str]` - List of bundle identifiers
**Notes:**
* Returns union of `bundle_identifiers` + system apps (if included)
* Does not query device for installed apps
#### IOSDriver.get\_apps
```python theme={null}
async def get_apps(include_system: bool = True) -> List[Dict[str, str]]
```
Return known apps as list of dicts with `package` (bundle identifier) and `label` (human-readable name).
System apps are mapped to friendly names (e.g., `com.apple.mobilesafari` → `Safari`). Third-party bundle identifiers are humanized from the last segment.
**Arguments**:
* `include_system` *bool* - Include system apps (default: True)
**Returns**:
* `List[Dict[str, str]]` - List of dicts with 'package' and 'label' keys
***
## State and Observation Methods
#### IOSDriver.screenshot
```python theme={null}
async def screenshot(hide_overlay: bool = True) -> bytes
```
Capture device screen as raw PNG bytes.
**Arguments**:
* `hide_overlay` *bool* - Unused on iOS (for API compatibility)
**Returns**:
* `bytes` - Raw PNG image data
**Usage:**
```python theme={null}
png_bytes = await driver.screenshot()
with open("screenshot.png", "wb") as f:
f.write(png_bytes)
```
#### IOSDriver.get\_ui\_tree
```python theme={null}
async def get_ui_tree() -> Dict[str, Any]
```
Return unified state from the iOS portal.
Mobilerun requests `GET /state?timeout=4`, giving the portal a single 4-second state collection budget. There is no Mobilerun-side retry on iOS state fetches.
**Returns**:
Dictionary with:
* `a11y_tree` - The accessibility tree elements
* `phone_state` - Dict with `currentApp` and `keyboardVisible`
* `device_context` - Additional device context
**Usage:**
```python theme={null}
tree = await driver.get_ui_tree()
print(tree["phone_state"]["currentApp"])
```
#### IOSDriver.get\_date
```python theme={null}
async def get_date() -> str
```
Get the current date and time from the device.
**Returns**:
* `str` - Date string, or empty string on failure
***
## Unsupported Methods
The following DeviceDriver methods are **not in `supported`** and will raise `NotImplementedError`:
#### drag()
Not supported on iOS.
#### install\_app()
Not supported on iOS.
***
## Instance Properties
```python theme={null}
driver.url # iOS Portal URL
driver.bundle_identifiers # Custom bundle IDs
driver.supported # Set of supported method names
driver.supported_buttons # Set of supported button names
```
***
## Example Usage
```python theme={null}
import asyncio
from mobilerun.tools.driver import IOSDriver
async def main():
# Initialize and connect
driver = IOSDriver(url="http://127.0.0.1:6643")
await driver.connect()
# Launch Messages
result = await driver.start_app("com.apple.MobileSMS")
print(result)
# Get UI tree
tree = await driver.get_ui_tree()
print(tree["phone_state"]["currentApp"])
# Tap at coordinates
await driver.tap(200, 400)
# Input text
await driver.input_text("Hello from Mobilerun!")
# Take screenshot
png_bytes = await driver.screenshot()
with open("screenshot.png", "wb") as f:
f.write(png_bytes)
asyncio.run(main())
```
**Note:** For higher-level interactions with element indexing and structured results, use action functions with `ActionContext` (see [MobileAgent](/framework/sdk/droid-agent)) rather than calling the driver directly.
***
## See Also
* [AndroidDriver](/framework/sdk/adb-tools) - Android device driver
* [DeviceDriver Base Class](/framework/sdk/base-tools) - Base class and architecture reference
* [Device Setup — iOS](/framework/guides/device-setup) - Full setup instructions
# Reference
Source: https://docs.mobilerun.ai/framework/sdk/reference
Complete API reference for Mobilerun components and tools
## Overview
The Mobilerun SDK provides a comprehensive set of APIs for building mobile automation workflows with AI agents. This reference documentation covers all major components and tools.
***
## Core Components
Main agent coordinator with multi-agent orchestration
Android device driver via ADB
iOS device driver and automation
Base class, StateProvider, UIState, ActionContext, and ToolRegistry
***
## Configuration
MobileConfig API and YAML configuration reference
***
## API Documentation
Detailed API documentation for each component is available in the sections linked above. Each page includes:
* Class/function signatures
* Parameter descriptions
* Return types
* Usage examples
* Best practices
For conceptual guides and tutorials, see the [Guides](/framework/guides/overview) section.
# Cloud Phone Setup
Source: https://docs.mobilerun.ai/guides/cloud-phone-setup
Best practices for setting up and using your Cloud Phone for reliable, stealth automation.
Your Cloud Phone is a high-performance virtual Android device with dedicated resources, persistent state, and advanced automation capabilities. It provides a persistent environment and profile support so you can manage multiple device identities from a single subscription.
The Cloud Phone is a [dedicated virtual device](/device-types#cloud-phone) in Mobilerun. This guide walks you through the best practices for getting the most out of it.
## 1. Create a Google Account Beforehand
Before your Cloud Phone is ready, prepare a Google account that you will use on the device. Having this ready in advance lets you set up the phone quickly once provisioning completes.
* Create a Google account at [accounts.google.com](https://accounts.google.com)
* Keep the credentials accessible. You will need them to sign into Google Play and other Google services on the device
Having credentials pre configured in Mobilerun means the agent can handle Google sign-in automatically when needed.
## 2. Configure a Proxy
A proxy is **mandatory** for every Cloud Phone. The device cannot be provisioned without one attached. You need to **bring your own** SOCKS5 proxy. Mobilerun does not provide managed proxies for Cloud Phones.
1. Open the [Proxies](/proxies) tab in the dashboard
2. Register your SOCKS5 proxy (host, port, and credentials)
3. Select that proxy when creating the device
See the [Proxies](/proxies) page for details on the supported proxy types and how Smart IP automatically aligns the device's location, timezone, and language with the proxy country.
Always ensure your proxy country matches the persona and locale you want the device to present. A mismatch is a common detection signal.
## 3. Wait for Provisioning
After purchasing your Cloud Phone and attaching a proxy, the device needs to be initialized before you can use it. This process spins up the virtual device, installs the base system, and prepares it for your account.
* **Do not assume the phone is broken if it doesn't appear instantly.** Provisioning takes some time.
* You can monitor the status of your device in the [Devices](/devices) tab. Once the device shows as **Ready**, you can start using it.
* If provisioning seems stuck or takes unusually long, reach out on [Discord](https://discord.gg/droidrun) or email us at [contact@mobilerun.ai](mailto:contact@mobilerun.ai) and we'll investigate.
Provisioning typically completes within a few minutes, but can occasionally take longer. Please wait for the device to show as **Ready** before reporting an issue.
## 4. Install Apps via Google Play Store
Always install apps through the **Google Play Store** on the device. Do **not** download APKs from third party sources like APKMirror.
**Why this matters:**
* Third party APKs can be incompatible with the device and cause it to crash or become unresponsive
* Play Store apps are verified, correctly signed, and receive automatic updates
* Using Play Store is the normal behavior of a real user, which supports stealth
**How to install apps:**
1. Open Google Play Store on the device
2. Sign in with your Google account
3. Search for and install the app you need
You can also use the [Apps](/apps) tab in Mobilerun to manage your app library and deploy apps to your device.
## Quick Setup Checklist
Use this checklist to get your Cloud Phone production ready:
* Prepare a Google account before the device is ready
* Register your own SOCKS5 proxy in the [Proxies](/proxies) tab and attach it to the device
* Wait for provisioning to complete
* Sign into Google Play Store on the device
* Install apps only through Google Play Store
## Need Help?
If you run into any issues or need further assistance, reach out to us:
* **Discord:** [discord.gg/droidrun](https://discord.gg/droidrun)
* **Email:** [contact@mobilerun.ai](mailto:contact@mobilerun.ai)
# Connect an Android
Source: https://docs.mobilerun.ai/guides/connect-android
Bring your own Android phone into Mobilerun using the Mobilerun Portal app.
Use the **Mobilerun Portal** Android app to connect your own phone to Mobilerun. Once connected, it shows up as a [Personal Phone](/device-types#personal-phone) on the [Devices](https://cloud.mobilerun.ai/devices) page and can run tasks from the Playground or API.
## Requirements
* A Mobilerun account with a [Personal Phone](/device-types#personal-phone) subscription
* An Android phone with a stable internet connection
***
## 1. Install the Mobilerun Portal app
Grab the latest APK from the [releases page](https://github.com/droidrun/mobilerun-portal/releases) and install it on your phone. Alternatively, clone the [Mobilerun Portal repo](https://github.com/droidrun/mobilerun-portal) and build it yourself with Gradle or ADB.
If Google Play Protect blocks the install, temporarily pause scanning: **Play Store → Profile icon → Play Protect → Settings (gear) → Pause "Scan apps with Play Protect"**. If a **More details** option appears when installing the APK, tap it and choose **Install anyway**.
***
## 2. Grant permissions
Open the app and approve the prompts it requests:
1. Enable the **Mobilerun Portal** accessibility service when prompted.
2. In the app, open **Settings** and allow:
* **Notifications**
* **Auto-accept Screen Share**
* **Install Auto-Accept** (also enable **Install unknown apps** at the system level)
These let Mobilerun stream the screen and install apps without manual taps during a task.
***
## 3. Connect to Mobilerun
1. On the Portal main screen, press **Connect to Mobilerun**.
2. If prompted, sign in with the same email you used for your Mobilerun account.
3. Keep the app running — the phone stays online only while the Portal is active.
***
## Verify it worked
Open [Devices](https://cloud.mobilerun.ai/devices) in the Mobilerun dashboard — your phone should appear as a connected Personal Phone. Start a task from the Playground and select it to confirm end-to-end control.
Because this is your physical phone, it keeps its own local state across tasks (installed apps, logged-in accounts, etc.).
***
## Disconnect
Press **Disconnect** in the Portal main screen to take the phone offline.
***
## Need Help?
If you run into any issues or need further assistance, reach out to us:
* **Discord:** [discord.gg/droidrun](https://discord.gg/droidrun)
* **Email:** [contact@mobilerun.ai](mailto:contact@mobilerun.ai)
# Connect an iPhone
Source: https://docs.mobilerun.ai/guides/connect-iphone
Bring your own iPhone into Mobilerun cloud using the mobilerun-ios CLI.
Use the `mobilerun-ios` CLI to connect your own iPhone to Mobilerun so it can run tasks from the Playground or API. This guide is for developers with a Mac, Xcode, and an iPhone they can plug in over USB. Once connected, the iPhone shows up as a [Personal Phone](/device-types#personal-phone) on the [Devices](https://cloud.mobilerun.ai/devices) page.
## Requirements
* A Mobilerun account with a [Personal Phone](/device-types#personal-phone) subscription
* macOS with Xcode installed
* An iPhone connected via USB
* An Apple ID with signing capability (personal team is fine)
Signing with a personal / free team comes with Apple-imposed limits: builds expire after **7 days**, after which you need to rebuild in Xcode and re-trust the certificate on the device, and you can sign for at most **3 devices**. A paid Apple Developer account (\$99/year) lifts both limits.
***
## 1. Enable Developer Mode on your iPhone
Developer Mode lets Xcode install and launch development builds (like WebDriverAgent) on your device.
1. On the iPhone, open **Settings → Privacy & Security → Developer Mode**.
2. Toggle **Developer Mode** on.
3. Restart the iPhone when prompted, then confirm the prompt after it boots.
The **Developer Mode** entry only appears after the iPhone has been connected at least once to a Mac running Xcode. If you don't see it, plug the device in, open Xcode, then check Settings again.
***
## 2. Configure your iPhone for long-lived connections
A Personal Phone is meant to stay online for hours or days at a time. Three iPhone settings make the difference between a connection that survives and one that needs constant babysitting:
### Disable Auto-Lock
When the iPhone locks, the screen goes black and the session goes offline. Set **Settings → Display & Brightness → Auto-Lock** to **Never**.
### Enable automatic time zone
Set **Settings → General → Date & Time → Set Automatically** to on. This keeps the device's time zone in sync with its (simulated) location. Writing the time zone manually does not work on non-jailbroken devices, so automatic time zone is the only reliable way to keep it matching the location.
### Remove the passcode (for unattended 24/7 setups)
Only do this if you plan to leave the device running around the clock with nobody nearby to unlock it. For supervised use, keep your passcode and skip this.
After running for a while, the XCTest session that hosts WebDriverAgent crashes or expires and has to be restarted. Mobilerun restarts it automatically — but only if the device has no passcode. With a passcode set, the relaunched session lands on the lock screen and the connection stays down until someone enters the passcode by hand. If you're around to unlock the device when that happens, you don't need to remove it.
For a fully unattended device, turn the passcode off under **Settings → Face ID & Passcode → Turn Passcode Off**.
Removing the passcode also disables Face ID, Apple Pay, and some iCloud features. Use a dedicated automation device rather than your daily phone.
***
## 3. Download Mobilerun WebDriverAgent
Mobilerun controls your iPhone through WebDriverAgent (WDA), an open-source project that exposes input injection and screenshots on iOS. We maintain our own fork, **Mobilerun WebDriverAgent**, tuned for exactly this job: higher-quality streaming, lower-latency accessibility tree fetching and touch input, with more features landing over time.
Clone it:
```bash theme={null}
git clone --branch v1.1.0 https://github.com/droidrun/WebDriverAgent
```
The upstream [Appium WebDriverAgent](https://github.com/appium/WebDriverAgent) works too, but with higher latency and lower streaming FPS.
***
## 4. Build and install WebDriverAgent on your iPhone with Xcode
This is the trickiest step. Take it slow — most connection issues later on trace back to signing or trust problems here.
1. Open `WebDriverAgent.xcodeproj` in Xcode.
2. In the top bar, select your connected iPhone as the run destination.
3. Choose the **WebDriverAgentRunner** scheme.
4. Set up signing for the runner and its extension target. For both `WebDriverAgentRunner` and `WebDriverAgentBroadcast`, select the target, open the **Signing & Capabilities** tab, and:
* Tick **Automatically manage signing**.
* Pick your Apple ID / Team from the dropdown.
`WebDriverAgentBroadcast` is an app extension that ships inside the runner app — it's what enables high-FPS screen streaming. It builds and embeds automatically; it just needs a team selected so Xcode can sign it. Skipping it fails the build with "Signing for 'WebDriverAgentBroadcast' requires a development team."
If Xcode shows "Failed to register bundle identifier," the **Bundle Identifier** needs to be unique per Apple ID. Append a suffix to the `WebDriverAgentRunner` target's bundle ID (e.g. your initials: `com.facebook.WebDriverAgentRunner.`) and mirror it in the extension target, keeping its suffix pattern intact: `WebDriverAgentBroadcast` → `com.facebook.WebDriverAgentRunner..xctrunner.broadcast`.
If the extension ID doesn't follow the runner's, the app installs with an extension whose provisioning profile no longer matches, and iOS rejects the install.
5. Press **Cmd+U** to build-for-testing. Xcode builds the runner with the extension embedded and installs it onto the iPhone.
6. On the iPhone, trust the developer certificate: **Settings → General → VPN & Device Management** → tap your team → **Trust**.
The first Cmd+U run often fails with "Could not launch WebDriverAgentRunner" because the certificate isn't trusted yet. Trust the certificate on the device (step 6) and press **Cmd+U** again.
***
## 5. Install the mobilerun-ios CLI
Pick whichever you prefer:
```bash Homebrew theme={null}
brew install droidrun/tap/mobilerun-ios
```
```bash curl theme={null}
curl -fsSL https://github.com/droidrun/mobilerun-ios-releases/releases/latest/download/install.sh | sh
```
Both fetch the latest release, verify the checksum, and drop the binary on your `PATH` (`brew` puts it in the Homebrew prefix; the `curl` installer uses `/usr/local/bin` and may prompt for sudo).
To install into a user-writable directory with the `curl` installer, override `BINDIR`:
```bash theme={null}
curl -fsSL https://github.com/droidrun/mobilerun-ios-releases/releases/latest/download/install.sh | BINDIR=$HOME/.local/bin sh
```
Verify the install:
```bash theme={null}
mobilerun-ios --version
```
***
## 6. Log in to Mobilerun
```bash theme={null}
mobilerun-ios login
```
The CLI prints a short code and opens the verification page in your browser. Approve the code and the CLI stores the session token under `~/.mobilerun-ios/config.yaml` (chmod 0600). Confirm with:
```bash theme={null}
mobilerun-ios whoami
```
Run `mobilerun-ios logout` to clear the stored token.
For CI/headless setups where opening a browser isn't possible, skip `login` and export an API key from the [API Keys](/api-keys) tab instead:
```bash theme={null}
export MOBILERUN_IOS_TOKEN=dr_sk_your_api_key
```
***
## 7. List your connected iPhones
With the iPhone plugged in and unlocked, list everything `mobilerun-ios` can see:
```bash theme={null}
mobilerun-ios list
```
```text theme={null}
UDID NAME TYPE OS STATE PORTAL
00008020-001A2B3C4D5E6F70 My iPhone real 18.2 ready —
```
Copy the UDID of the device you want to connect. The `PORTAL` column shows `running (pid=…)` once a background portal is up (see below).
***
## 8. Start the connection
Connect the iPhone by passing its UDID:
```bash theme={null}
mobilerun-ios 00008020-001A2B3C4D5E6F70
```
Leave the UDID off to connect every attached iPhone:
```bash theme={null}
mobilerun-ios
```
The command opens a WebSocket to Mobilerun cloud and keeps the device online for as long as it runs. Press **Ctrl+C** to end the session.
### Run in the background
Pass `-d` / `--detach` to release the terminal while the portal stays up:
```bash theme={null}
mobilerun-ios -d 00008020-001A2B3C4D5E6F70
```
The CLI prints a PID and log path, then exits. `mobilerun-ios list` shows the portal as `running` in the `PORTAL` column. Stop it later with:
```bash theme={null}
mobilerun-ios stop 00008020-001A2B3C4D5E6F70
```
Or `mobilerun-ios stop` (no UDID) to stop every background portal.
***
## Verify it worked
Open [Devices](https://cloud.mobilerun.ai/devices) in the Mobilerun dashboard — your iPhone should show up as a connected Personal Phone. Start a task from the Playground and select it to confirm end-to-end control.
***
## Troubleshooting
The CLI surfaces this as:
```text theme={null}
failed to start agent: failed to wait for WebDriverAgent: timed out waiting for WebDriverAgent to be ready
```
Re-sign the `WebDriverAgentRunner` target in Xcode and make sure the developer certificate is trusted under **Settings → General → VPN & Device Management**, then press **Cmd+U** again.
Confirm your USB cable supports data (not charging-only), the iPhone is unlocked, and you accepted the **Trust This Computer** prompt.
Re-run `mobilerun-ios login` and confirm with `mobilerun-ios whoami`. For API-key setups, verify the token in the [API Keys](/api-keys) tab and that `MOBILERUN_IOS_TOKEN` is exported in the same shell.
Auto-lock put the device to sleep. Disable it under **Settings → Display & Brightness → Auto-Lock → Never** while the iPhone is connected to Mobilerun (see [step 2](#2-configure-your-iphone-for-long-lived-connections)).
The XCTest session hosting WebDriverAgent expired and was restarted, but a passcode is blocking the relaunch. Unlock the device to let the session reconnect. For an unattended 24/7 device where no one is around to do that, remove the passcode (see [step 2](#2-configure-your-iphone-for-long-lived-connections)) so the session can heal itself automatically.
***
## Known issues
* **TikTok crashes automation.** Dumping TikTok's accessibility tree times out, which crashes the Mobilerun automation session. Avoid tasks that drive the TikTok app for now.
***
## Need Help?
If you run into any issues or need further assistance, reach out to us:
* **Discord:** [discord.gg/droidrun](https://discord.gg/droidrun)
* **Email:** [contact@mobilerun.ai](mailto:contact@mobilerun.ai)
# Embed a device stream in your app
Source: https://docs.mobilerun.ai/guides/embed-device-stream
Build a custom device experience with @mobilerun/react
Use [`@mobilerun/react`](https://www.npmjs.com/package/@mobilerun/react) to add a
live, interactive Mobilerun cloud device to your React application.
The package provides:
* [`DeviceStream`](https://github.com/droidrun/mobilerun-react) for video, touch,
keyboard input, and reconnection
* `NavigationBar` for Android Back, Home, and Recents controls
* `RemoteControlHandle` for actions such as opening a URL or taking a screenshot
* A prebuilt stylesheet that you can customize with CSS variables
Your application provides the surrounding layout, branding, loading states, and
access control.
## Keep the API key in your backend
`MOBILERUN_CLOUD_API_KEY` is an account-level secret. Never include it in browser
code, public environment variables, or a client-side SDK instance.
In a multi-user application, your backend must authenticate the current user and
verify that the user may access the requested device before returning stream
credentials.
The browser does not need the account API key. It only needs the `streamUrl` and
`streamToken` for the selected device:
```mermaid theme={null}
flowchart LR
Browser["React application"] -->|"Authenticated request"| Backend["Your backend"]
Backend -->|"Account API key"| API["Mobilerun Cloud API"]
API -->|"streamUrl + streamToken"| Backend
Backend -->|"Device credentials"| Browser
Browser -->|"WebRTC stream and controls"| Device["Mobilerun device"]
```
The stream token is device-scoped, but it is still sensitive. Do not store it in
local storage or send it to logs, analytics, traces, or error-reporting tools.
## Install
```bash theme={null}
npm install @mobilerun/react @mobilerun/sdk react react-dom
```
Import the package stylesheet once in your application:
```tsx app/layout.tsx theme={null}
import '@mobilerun/react/styles.css';
import './globals.css';
```
## Create a backend credentials endpoint
The following Next.js route uses `@mobilerun/sdk` only on the server. Replace the
example authentication imports with your application's own authorization layer.
```ts app/api/device-stream/[deviceId]/route.ts theme={null}
import Mobilerun from '@mobilerun/sdk';
import { type NextRequest, NextResponse } from 'next/server';
import { canAccessDevice, getCurrentUser } from '@/server/auth';
const apiKey = process.env.MOBILERUN_CLOUD_API_KEY;
if (!apiKey) {
throw new Error('MOBILERUN_CLOUD_API_KEY is not configured');
}
const mobilerun = new Mobilerun({ apiKey });
export async function GET(
request: NextRequest,
context: { params: Promise<{ deviceId: string }> },
) {
const user = await getCurrentUser(request);
if (!user) {
return NextResponse.json({ error: 'unauthorized' }, { status: 401 });
}
const { deviceId } = await context.params;
// A device ID is not authorization. Enforce your application's ownership or
// membership rules before retrieving credentials with the account API key.
if (!(await canAccessDevice(user.id, deviceId))) {
return NextResponse.json({ error: 'forbidden' }, { status: 403 });
}
try {
const device = await mobilerun.devices.retrieve(deviceId);
return NextResponse.json(
{
streamUrl: device.streamUrl || null,
streamToken: device.streamToken || null,
state: device.state,
},
{ headers: { 'Cache-Control': 'no-store' } },
);
} catch (error) {
const status =
error instanceof Mobilerun.APIError && error.status === 404 ? 404 : 502;
return NextResponse.json(
{ error: status === 404 ? 'device_not_found' : 'stream_unavailable' },
{ status },
);
}
}
```
This endpoint returns only the fields required by the stream. It does not expose
the account API key or the full device response.
## Render `DeviceStream`
Fetch the device credentials from your authenticated backend, then pass them
directly to the package component:
```tsx components/device-stream-panel.tsx theme={null}
'use client';
import {
DeviceStream,
NavigationBar,
type RemoteControlHandle,
} from '@mobilerun/react';
import { useCallback, useEffect, useRef, useState } from 'react';
interface StreamCredentials {
streamUrl: string | null;
streamToken: string | null;
state: string;
}
export function DeviceStreamPanel({ deviceId }: { deviceId: string }) {
const controls = useRef(null);
const [credentials, setCredentials] = useState();
const [error, setError] = useState();
const loadCredentials = useCallback(async () => {
try {
const response = await fetch(`/api/device-stream/${deviceId}`, {
cache: 'no-store',
});
if (!response.ok) {
setError(`Unable to load stream (${response.status})`);
return;
}
setCredentials((await response.json()) as StreamCredentials);
setError(undefined);
} catch {
setError('Unable to reach the stream endpoint');
}
}, [deviceId]);
useEffect(() => {
void loadCredentials();
}, [loadCredentials]);
useEffect(() => {
if (!credentials || credentials.streamUrl || error) {
return;
}
const timer = window.setTimeout(() => {
void loadCredentials();
}, 3_000);
return () => window.clearTimeout(timer);
}, [credentials, error, loadCredentials]);
if (error) {
return {error}
;
}
return (
controls.current?.sendSystemKey(action)}
/>
);
}
```
If a device is still starting, `streamUrl` may be empty. The second effect polls the
backend every three seconds until it becomes available. `onStreamHealed` also
retrieves the current credentials after a stable disconnect.
`NavigationBar` stays below the stream and provides Android Back, Home, and Recents.
The `hasControl` prop controls direct interaction with the device screen; it does not
disable `NavigationBar` or provide an authorization boundary. Anyone who receives
stream credentials must be authorized to control that device. If your UI includes a
view-only mode, also guard the `onAction` callback and treat that as a UI affordance,
not a security control.
The stream starts muted so browser autoplay policies do not block it; only unmute
after a user action.
## Customize the appearance
`DeviceStream` does not add Mobilerun logos or application chrome. Style its parent
and override the package CSS variables to match your product:
```css app/globals.css theme={null}
.device-panel {
--background: #09090b;
--foreground: #fafafa;
--muted-foreground: #a1a1aa;
--border: #3f3f46;
--primary: #7c3aed;
display: flex;
width: min(100%, 390px);
aspect-ratio: 9 / 19.5;
flex-direction: column;
overflow: hidden;
border: 1px solid var(--border);
border-radius: 32px;
background: var(--background);
}
.device-screen {
min-height: 0;
flex: 1;
}
```
## Observe the integration safely
Instrument the backend credentials request with your existing OpenTelemetry setup.
Record the device ID, device state, and upstream status, but never credentials.
Use `onConnectionStateChange` for user-visible connection analytics:
```tsx theme={null}
onConnectionStateChange={(connected) => {
posthog.capture(
connected ? 'device_stream_connected' : 'device_stream_disconnected',
{ device_id: deviceId },
);
}}
```
Do not include `streamUrl`, `streamToken`, or `MOBILERUN_CLOUD_API_KEY` in PostHog
events.
## Production checklist
* The account API key is used only by backend code.
* Every credentials request requires an authenticated application session.
* Your backend verifies that the current user may access the requested device.
* Credential responses use `Cache-Control: no-store`.
* Stream credentials are excluded from logs, traces, analytics, and error reports.
* Interactive control is enabled only for authorized users.
For all exported components and the local playground, see the
[`@mobilerun/react` package repository](https://github.com/droidrun/mobilerun-react).
# Physical Phone Setup
Source: https://docs.mobilerun.ai/guides/physical-phone-setup
Best practices for setting up and using your Physical Phone for reliable, stealth automation.
Your Physical Phone is a dedicated, premium real Android device hosted in the Mobilerun data center, optimized for reliable Mobile RPA operations. It provides a persistent environment, genuine hardware, and enhanced isolation with high stealth capabilities that closely mimic real user behavior.
The Physical Phone is a [dedicated physical device](/device-types#physical-phone) in Mobilerun. This guide walks you through the best practices for getting the most out of it.
## 1. Create a Google Account Beforehand
Before your Physical Phone is ready, prepare a Google account that you will use on the device. Having this ready in advance lets you set up the phone quickly once provisioning completes.
* Create a Google account at [accounts.google.com](https://accounts.google.com)
* Keep the credentials accessible. You will need them to sign into Google Play and other Google services on the device
Having credentials pre configured in Mobilerun means the agent can handle Google sign-in automatically when needed.
## 2. Configure a Proxy
A proxy is **mandatory** for every Physical Phone. The device cannot be provisioned without one attached. You need to **bring your own** SOCKS5 proxy. Mobilerun does not provide managed proxies for Physical Phones.
1. Open the [Proxies](/proxies) tab in the dashboard
2. Register your SOCKS5 proxy (host, port, and credentials)
3. Select that proxy when creating the device
See the [Proxies](/proxies) page for details on the supported proxy types and how Smart IP automatically aligns the device's location, timezone, and language with the proxy country.
Always ensure your proxy country matches the persona and locale you want the device to present. A mismatch is a common detection signal.
## 3. Wait for Provisioning
After purchasing your Physical Phone and attaching a proxy, the device needs to be initialized before you can use it. This process sets up the hardware, installs the base system, and prepares the device for your account.
* **Do not assume the phone is broken if it doesn't appear instantly.** Provisioning takes some time.
* You can monitor the status of your device in the [Devices](/devices) tab. Once the device shows as **Ready**, you can start using it.
* If provisioning seems stuck or takes unusually long, reach out on [Discord](https://discord.gg/droidrun) or email us at [contact@mobilerun.ai](mailto:contact@mobilerun.ai) and we'll investigate.
Provisioning typically completes within a few minutes, but can occasionally take longer. Please wait for the device to show as **Ready** before reporting an issue.
## 4. Install Apps via Google Play Store
Always install apps through the **Google Play Store** on the device. Do **not** download APKs from third party sources like APKMirror.
**Why this matters:**
* Third party APKs can be incompatible with the device and cause it to crash or become unresponsive
* Play Store apps are verified, correctly signed, and receive automatic updates
* Using Play Store is the normal behavior of a real user, which supports stealth
**How to install apps:**
1. Open Google Play Store on the device
2. Sign in with your Google account
3. Search for and install the app you need
You can also use the [Apps](/apps) tab in Mobilerun to manage your app library and deploy apps to your device.
## 5. eSIM Provisioning
Physical Phones support eSIM for cellular connectivity. An eSIM gives the device a real phone number and mobile data connection, which increases stealth for apps that check for cellular connectivity.
**Important:** Use an eSIM with roaming that can be activated in Germany. All current Physical Phones are hosted in Germany, so the eSIM must be able to attach to a network there.
**To activate an eSIM:**
1. Open your device's **eSIM** section
2. Choose an activation method:
* **LPA Activation Code**: paste the full code or just the part after `LPA:1$`
* **Form Mode**: enter the SM-DP+ address and activation code separately
3. Submit and wait for the eSIM to activate
**To remove an eSIM:**
* Select the subscription from the list and click remove
You will need to obtain an eSIM plan from a provider of your choice. Mobilerun does not sell eSIM plans directly.
## Quick Setup Checklist
Use this checklist to get your Physical Phone production ready:
* Prepare a Google account before the device is ready
* Register your own SOCKS5 proxy in the [Proxies](/proxies) tab and attach it to the device
* Wait for provisioning to complete
* Sign into Google Play Store on the device
* Install apps only through Google Play Store
* (Optional) Activate an eSIM for cellular connectivity
## Need Help?
If you run into any issues or need further assistance, reach out to us:
* **Discord:** [discord.gg/droidrun](https://discord.gg/droidrun)
* **Email:** [contact@mobilerun.ai](mailto:contact@mobilerun.ai)
# ADB
Source: https://docs.mobilerun.ai/integrations/adb
Run your local adb or Frida client against a Mobilerun cloud device through a WebSocket tunnel.
Mobilerun can expose each device over WebSocket endpoints that speak the `adb` transport protocol and, on supported devices, the Frida transport. Bridge your local client to the tunnel and you can use familiar workflows against a cloud device, without installing anything on the device itself:
* `adb shell`, `adb install`, `adb push`, `adb pull`, `adb forward` over the adb tunnel.
* `frida`, `frida-ps`, `frida-trace`, and any Frida-based tooling over the Frida tunnel.
The adb tunnel is **command-filtered**: an in-line proxy inspects every adb service request and only permits the families needed for ordinary debugging. Full, unfiltered access (including root) can be granted on request. The Frida tunnel is a raw byte pipe that carries the Frida transport verbatim to the device's Frida server.
## Requirements
adb and Frida access are only available on request. Contact Mobilerun support at [contact@mobilerun.ai](mailto:contact@mobilerun.ai) to have them enabled for your account.
* A Mobilerun [API key](/api-keys) (`dr_sk_...`)
* A device in the `ready` state (see [Devices](/devices))
* `adb` installed locally (Android SDK platform-tools) for the adb tunnel
* Frida installed locally (`frida-tools`) for the Frida tunnel, on a device whose `capabilities.frida` is `true`
* A WebSocket-to-TCP bridge such as [`websocat`](https://github.com/vi/websocat)
Check whether a device supports Frida before opening the Frida tunnel:
```bash theme={null}
curl -H "Authorization: Bearer $MOBILERUN_API_KEY" \
https://api.mobilerun.ai/v1/devices/$DEVICE_ID/capabilities
```
`capabilities.frida` is `true` for devices created in a Frida-enabled pool and `false` otherwise. The value is frozen at device creation, so toggling the pool setting later does not change existing devices.
## Endpoints
```
wss://api.mobilerun.ai/v1/devices/{deviceId}/adb
wss://api.mobilerun.ai/v1/devices/{deviceId}/frida
```
| Parameter | Description |
| ---------- | -------------------------------------------------------------------------- |
| `deviceId` | UUID of a device you own. List devices with `GET /v1/devices?state=ready`. |
Authenticate with the same `Authorization: Bearer dr_sk_...` header you use for the REST API. You must own the device and it must be in the `ready` state, or the upgrade is rejected before the WebSocket opens. The Frida endpoint additionally requires `capabilities.frida` to be `true`.
## Connect your local adb client
The adb endpoint speaks the raw adb transport, so your local `adb` daemon needs a TCP socket on the other end. The simplest setup pipes the WebSocket through `websocat` to a local port, then attaches `adb` to that port.
```bash theme={null}
# 1. Bridge the WebSocket to localhost:5037-style TCP
websocat \
--binary tcp-listen:127.0.0.1:7777 \
"wss://api.mobilerun.ai/v1/devices/$DEVICE_ID/adb" \
-H "Authorization: Bearer $MOBILERUN_API_KEY"
# 2. In another terminal, attach adb and use it normally
adb connect 127.0.0.1:7777
adb -s 127.0.0.1:7777 shell getprop ro.product.model
adb -s 127.0.0.1:7777 install ./app-release.apk
adb -s 127.0.0.1:7777 push ./data.bin /sdcard/Download/
```
Each WebSocket connection carries a single adb transport. Open one tunnel per device, and let `adb` multiplex shells, file transfers, and forwards over it.
## Connect your local Frida client
Frida's client speaks its own transport over TCP. Bridge the WebSocket to a local port, then point Frida at that port with `-H`.
```bash theme={null}
# 1. Bridge the WebSocket to a local TCP port
websocat \
--binary tcp-listen:127.0.0.1:27042 \
"wss://api.mobilerun.ai/v1/devices/$DEVICE_ID/frida" \
-H "Authorization: Bearer $MOBILERUN_API_KEY"
# 2. In another terminal, run Frida against the local bridge
frida-ps -H 127.0.0.1:27042
frida -H 127.0.0.1:27042 -n com.example.app
frida-trace -H 127.0.0.1:27042 -i "open" com.example.app
```
Each WebSocket connection carries a single Frida session transport. Open one tunnel per device and let Frida multiplex scripts and RPC calls over it.
## Access policy
By default, the adb tunnel is filtered so that everyday debugging workflows (shells, installs, file transfers, port forwards, app debugging, screenshots) pass through, while requests that would restart `adbd`, take the device offline, or mutate global device state are refused. Blocked requests fail with an in-stream error visible to your `adb` client.
Full, unfiltered access — including `adb root` — can be granted per-account on request. Contact [contact@mobilerun.ai](mailto:contact@mobilerun.ai) if you need it.
## Troubleshooting
* **`400 Bad Request` on the Frida upgrade** — The device does not support Frida (`capabilities.frida` is `false`). Provision a device in a Frida-enabled pool.
* **`401 Unauthorized` on upgrade** — Check your `Authorization` header and that the API key has access to the device.
* **`409` or `412` on upgrade** — The device is not in the `ready` state. List devices with `?state=ready` and pick one that is available.
* **`adb` reports `closed`** — The service you tried to open is denied by the policy. Look at the bridge's stderr for the plaintext reason; switch to an allowed family (e.g. use `adb shell svc` instead of `adb root`).
* **Nested adb is refused** — Forwarding to port `5555` or to the device's own adb port is intentionally blocked. Use the outer tunnel directly.
* **Frida reports the server is unreachable** — The device's Frida server did not start. Confirm `capabilities.frida` is `true` on the device and retry, or contact support.
# MCP Server
Source: https://docs.mobilerun.ai/mcp-server
Connect AI agents and tools to Mobilerun using the Model Context Protocol.
Mobilerun provides a Model Context Protocol (MCP) server that enables AI agents, IDEs, and other MCP-compatible tools to interact with the Mobilerun API directly. All API endpoints are accessible through MCP.
## Overview
The MCP integration allows you to:
* Run mobile automation tasks from AI coding assistants
* Access all Mobilerun API functionality through MCP tools
* Integrate with any MCP-compatible client (Cursor, Claude Desktop, etc.)
## Configuration
There are two ways to connect to the Mobilerun MCP server:
### API Key Authentication
Use your [API key](/api-keys) for direct authentication. This method is ideal for automated workflows and server-side integrations.
```json theme={null}
{
"mcpServers": {
"mobilerun": {
"url": "https://api.mobilerun.ai/v1/mcp",
"transport": "http",
"headers": {
"Authorization": "Bearer dr_sk_YOUR_API_KEY"
}
}
}
}
```
Replace `dr_sk_YOUR_API_KEY` with your actual API key from the [API Keys](/api-keys) tab.
### OAuth Authentication
Use OAuth for interactive authentication through the browser. This method is recommended for personal use in IDEs and desktop applications.
```json theme={null}
{
"mcpServers": {
"mobilerun": {
"url": "https://cloud.mobilerun.ai/api/mcp/sse",
"auth": {
"type": "oauth2",
"authorization_url": "https://cloud.mobilerun.ai/api/auth/oauth2/authorize",
"token_url": "https://cloud.mobilerun.ai/api/auth/oauth2/token"
}
}
}
}
```
With OAuth, you'll be prompted to sign in through your browser when the MCP client first connects.
## Available Tools
Once connected, the MCP server exposes tools for all Mobilerun operations:
| Tool Category | Description |
| --------------- | -------------------------------------------- |
| **Tasks** | Create, run, monitor, and cancel agent tasks |
| **Devices** | List devices and check availability |
| **Apps** | Browse and manage applications |
| **Credentials** | Access stored credentials for tasks |
| **Hooks** | Manage webhook subscriptions |
## Client Setup
### Cursor
Add the MCP configuration to your Cursor settings:
1. Open Cursor Settings
2. Navigate to the MCP section
3. Add the Mobilerun server configuration
4. Restart Cursor to connect
### Claude Desktop
Add the configuration to your Claude Desktop `claude_desktop_config.json` file:
**macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
**Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
## Usage Example
Once configured, you can interact with Mobilerun directly through your AI assistant:
> "Run a task on Mobilerun to open the Settings app and take a screenshot"
The assistant will use the MCP tools to:
1. List available devices
2. Create and run the task
3. Return the results
## Choosing an Authentication Method
Best for automated systems, CI/CD pipelines, and server-side applications where you manage the credentials.
Best for personal use in IDEs where you want to authenticate with your own account interactively.
# n8n
Source: https://docs.mobilerun.ai/n8n
Automate mobile workflows using the official Mobilerun node for n8n.
Mobilerun integrates with [n8n](https://n8n.io), the workflow automation platform, through an official verified node. Connect Mobilerun to 1000+ apps and services without writing code.
## Official Node
The Droidrun Tasks node is available in the n8n integrations library:
View the official integration and installation instructions.
## Available Operations
The n8n node supports the following operations:
| Operation | Description |
| ------------------------ | ---------------------------------- |
| **Run Task** | Execute a new agent task |
| **Get Task** | Retrieve task details |
| **Get Task Status** | Check the current status of a task |
| **List Tasks** | List all tasks in your account |
| **Stop Task** | Cancel a running task |
| **Get Task Screenshot** | Get a screenshot from a task |
| **Get Task Screenshots** | Get all screenshots from a task |
## Setup
1. Install the Droidrun Tasks node in your n8n instance
2. Create an [API key](/api-keys) in your Mobilerun dashboard
3. Add your credentials in n8n
4. Start building workflows
## Use Cases
* Trigger mobile automation from webhooks or schedules
* Chain Mobilerun tasks with other services
* Process task results and send notifications
* Build end-to-end testing pipelines
# OpenClaw
Source: https://docs.mobilerun.ai/openclaw
Set up the Mobilerun skill in OpenClaw to give your AI agent full Android phone control.
[OpenClaw](https://openclaw.ai) is an agent CLI that loads skills — structured knowledge packs — to give AI agents access to external APIs and services. Installing the Mobilerun skill in OpenClaw lets you tell the agent things like "open Instagram and like the latest post" or "go to Amazon and add the cheapest USB-C cable to my cart," and it executes those tasks on a real Android phone.
## Prerequisites
* [OpenClaw CLI](https://openclaw.ai/docs/installation) installed
* A Mobilerun account at [cloud.mobilerun.ai](https://cloud.mobilerun.ai)
* An Android device connected via the [Mobilerun Portal](/guides/connect-android) app, or a cloud-hosted device ([Cloud Phone](/device-types#cloud-phone) or [Physical Phone](/device-types#physical-phone))
## Step 1 — Install the Plugin
```bash theme={null}
openclaw plugins install @mobilerun/openclaw-mobilerun
```
This downloads the Mobilerun skill from the npm registry and registers it with your local OpenClaw installation.
Verify it installed correctly:
```bash theme={null}
openclaw plugins list
```
You should see `@mobilerun/openclaw-mobilerun` in the output.
## Step 2 — Get Your API Key
1. Go to [cloud.mobilerun.ai/api-keys](https://cloud.mobilerun.ai/api-keys) (sign in with Google, GitHub, or Discord if prompted)
2. Click **New Key**, give it a name, and copy the key — it starts with `dr_sk_` and is shown only once
## Step 3 — Configure the API Key
You have two options. Choose whichever fits your workflow:
### Option A — Environment variable (quickest)
```bash theme={null}
export MOBILERUN_API_KEY=dr_sk_your_key_here
```
Add this to your shell profile (`~/.zshrc`, `~/.bashrc`, etc.) to make it permanent.
### Option B — OpenClaw config file
OpenClaw stores skill configuration in `~/.openclaw/openclaw.json`. Add your key under `skills.entries.mobilerun.apiKey`:
```json theme={null}
{
"skills": {
"entries": {
"mobilerun": {
"apiKey": "dr_sk_your_key_here"
}
}
}
}
```
If the file already exists, merge this key in — do not replace the entire file.
The config file method is preferred when you use multiple skills with different credentials, since each skill's key is namespaced separately.
## Step 4 — Connect a Device
If you don't have a device connected yet, install the Mobilerun Portal app on your Android phone:
1. On your Android phone, open Chrome and go to **[droidrun.ai/portal](https://droidrun.ai/portal)**
2. Download and install the APK (tap "Install anyway" if Android warns about unknown sources — this is expected)
3. Open the Portal app and tap **Enable Now** to grant the Accessibility permission
4. **Long-press** "Connect to Mobilerun" to open the API key dialog
5. Paste your `dr_sk_...` key and tap **Connect**
Once connected, the device appears as `ready` in the Mobilerun dashboard and the agent can use it immediately.
Cloud-hosted devices ([Cloud Phones](/device-types#cloud-phone) and [Physical Phones](/device-types#physical-phone)) are also available if you don't want to connect a personal device.
## Step 5 — Start the Agent with the Terminal User Interface (TUI)
```bash theme={null}
openclaw tui
```
OpenClaw loads the Mobilerun skill and the agent is ready. Try a prompt:
```
Open the Settings app and tell me the Android version on my phone.
```
The agent will:
1. Confirm the API key and find your connected device
2. Take a screenshot of the current screen
3. Navigate to Settings and read the relevant UI elements
4. Report the Android version back to you
## Configuration Reference
| Method | Location | Key path |
| -------------------- | --------------------------- | --------------------------------- |
| Environment variable | Shell environment | `MOBILERUN_API_KEY` |
| Config file | `~/.openclaw/openclaw.json` | `skills.entries.mobilerun.apiKey` |
The skill checks `MOBILERUN_API_KEY` first. If that is not set, it falls back to the config file. If neither is present, the agent will ask you for the key during the session and offer to save it.
## Troubleshooting
Keys always start with `dr_sk_`. If yours looks different, copy it again from [cloud.mobilerun.ai/api-keys](https://cloud.mobilerun.ai/api-keys). Keys can be revoked — create a new one if needed.
The agent will tell you the device status. If no device appears:
* Make sure the Mobilerun Portal app is open on your phone
* Check that the Accessibility permission is still enabled (some phones disable it after reboot)
* Confirm the phone has a stable internet connection
The Portal app lost its connection. Open the app on your phone — it usually reconnects automatically. If not, tap **Connect** again.
Autonomous agent tasks (`POST /tasks`) require credits. Check your credit balance at [cloud.mobilerun.ai/billing](https://cloud.mobilerun.ai/billing).
## Next Steps
Learn what the Mobilerun skill includes and how the agent uses it.
Manage your Mobilerun API keys.
Set up your personal Android device.
Understand plans, credits, and device types.
# Playground
Source: https://docs.mobilerun.ai/playground
Test and optimize your agent configurations interactively before deploying to production.
The Playground is your interactive environment for testing agent prompts and configurations on real devices. Use it to iterate quickly on your automation logic without writing code.
## Overview
The Playground allows you to:
* Test agent prompts in real-time
* Observe how your agent interacts with the device
* Iterate on prompt wording and task instructions
* Validate automation flows before production deployment
## Selecting a Device
Before running a task, select a device from your account — a Personal Phone, Cloud Phone, or Physical Phone.
Not sure which device type to use? See the [Device Types](/device-types) documentation to understand the differences between Personal Phones, Cloud Phones, and Physical Phones.
## Running a Task
1. **Select your device** from the device dropdown
2. **Enter your prompt** describing what the agent should do
3. **Click Run** to start the task
4. **Observe** the agent's actions in the device stream
The agent will interpret your prompt and execute the corresponding actions on the device. You can watch the device screen update in real-time as the agent performs each step.
## Prompt Optimization
The Playground is designed for rapid prompt iteration. Use it to:
| Technique | Description |
| ----------------------- | -------------------------------------------------------------- |
| **Refine instructions** | Adjust your prompt wording to get more accurate agent behavior |
| **Test edge cases** | Verify how your agent handles unexpected UI states |
| **Compare approaches** | Try different prompt styles to find what works best |
| **Debug failures** | Identify why a task failed and adjust your instructions |
### Writing Effective Prompts
Good prompts are specific and action-oriented. Here are some guidelines:
**Be explicit about the goal**
```
Open the Settings app, navigate to Display, and set brightness to 50%
```
**Include context when needed**
```
In the Gmail app, compose a new email to test@example.com with subject "Test" and body "Hello"
```
**Break down complex tasks**
```
1. Open Chrome
2. Navigate to github.com
3. Click the Sign In button
4. Enter username "testuser"
```
## Session Management
Each Playground session maintains state until you end it or switch devices:
* App installations persist within the session
* Login states are maintained
* Files created during the session remain accessible
# Port Forwarding
Source: https://docs.mobilerun.ai/port-forwarding
Reach arbitrary TCP services on a Mobilerun device through a WebSocket tunnel or an HTTP reverse proxy.
Mobilerun exposes a per-device port-forwarding endpoint that bridges to any TCP service listening on the device. You can connect with either a raw WebSocket tunnel (for arbitrary TCP traffic) or a transparent HTTP reverse proxy (for HTTP services), without managing your own ADB forwards.
Use this when you want to talk to an app, web server, debugger, or other service running on the device — for example, a local HTTP server inside a test build, an Appium endpoint, or a custom inspector.
## Endpoints
Both endpoints are scoped to a device and a target port on the device.
**WebSocket tunnel** — binary frames are forwarded to and from the device TCP port:
```text theme={null}
wss://api.mobilerun.ai/v1/devices/{deviceId}/ports/{port}
```
**HTTP reverse proxy** — preserves method, path, and body and streams the response back:
```text theme={null}
https://api.mobilerun.ai/v1/devices/{deviceId}/ports/{port}/{path}
```
### Authentication
Pass your Mobilerun API key as a request header:
```text theme={null}
Authorization: Bearer dr_sk_YOUR_API_KEY
```
Always send credentials as headers. The proxy strips `Authorization` and `X-Device-Token` from forwarded HTTP requests so your key is never sent to the on-device service.
### Requirements
* The device must be in the `ready` state. See [Devices](/devices) for state details.
* The target service must be listening on the chosen TCP port on the device.
* You need a Mobilerun [API key](/api-keys).
## HTTP reverse proxy
Send any HTTP request to the device. Method, path, query string, headers (minus auth), and body are preserved. Redirects are not followed — you receive the device's response as-is.
```bash theme={null}
curl -H "Authorization: Bearer $MOBILERUN_API_KEY" \
https://api.mobilerun.ai/v1/devices/$DEVICE_ID/ports/8080/api/status
```
The device sees a request to `GET /api/status` on its local port `8080`.
Request bodies are capped at 10 MB. Larger payloads are rejected before they reach the device.
## WebSocket tunnel
Open a WebSocket and exchange binary frames with the device port. Each frame is written to the underlying TCP connection in order; bytes coming back from the device are delivered as binary frames.
```js theme={null}
import WebSocket from 'ws';
const ws = new WebSocket(
`wss://api.mobilerun.ai/v1/devices/${deviceId}/ports/8080`,
{
headers: {
Authorization: `Bearer ${process.env.MOBILERUN_API_KEY}`,
},
}
);
ws.on('open', () => ws.send(Buffer.from('hello\n')));
ws.on('message', (data) => console.log('from device:', data));
```
Individual WebSocket frames are capped at 10 MB. Chunk larger payloads across multiple frames.
## Blocked ports
Forwarding to ports used by Mobilerun's own on-device agents is denied with `403 Forbidden`. The default blocklist covers adb, the device streaming bridge, the agent runtime, and the Frida server. Requests to other ports are passed through.
## Limitations
* **TCP only.** UDP services are not exposed.
* **Ready devices only.** Requests against devices in other states return an error before the tunnel is opened.
* **No redirect following.** The HTTP proxy returns the device's `3xx` response so your client can decide what to do.
# Proxies
Source: https://docs.mobilerun.ai/proxies
Configure proxies to route device traffic and unlock stealth-level automations.
The Proxies tab lets you register the proxy endpoints your devices will route traffic through. Every device runs behind a proxy, so you need at least one configured before you can spin up a device.
Setting up a proxy is **mandatory** before creating a device. Devices cannot be provisioned without an attached proxy.
## Choosing a Proxy Type
Different automations need different levels of stealth. Pick the proxy type that matches your use case:
| Proxy Type | Stealth Level | Best For |
| --------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| **Static ISP** | Basic | The cheapest option. Great for any automation that doesn't require stealth — general scraping, internal tooling, low-risk flows. |
| **Residential** | Good | Solid stealth and reliable IP reputation. Recommended for collecting high-quality data or running automations where detection matters. |
| **Mobile** | Highest | The most stealthy option. Use this for apps that are hard to get into or aggressively block non-mobile traffic. |
When in doubt, start with Static ISP and only upgrade to Residential or Mobile if you hit detection or blocking issues.
## Protocol Requirement
Mobilerun only supports the **SOCKS5** protocol. Make sure the proxy you purchase from your provider is offered as SOCKS5 before adding it.
## Creating a Proxy
Open the Proxies tab in the dashboard and click **new Proxy** to register a new endpoint. Fill in the form with the credentials you received from your proxy provider.
Once saved, the proxy becomes selectable when creating a new device.
## Smart IP
Smart IP is **enabled by default** on every proxy. When active, Mobilerun automatically aligns the device's environment with the target country of your proxy so the device looks consistent with its outbound IP.
Smart IP configures parameters such as:
* **Geolocation** — GPS coordinates matched to the proxy's country
* **System time** — Timezone aligned with the proxy's region
* **System language** — Default locale set to the country's primary language
This drastically reduces fingerprint mismatches that flag automated devices. You can disable Smart IP per proxy if you need to keep the device's local settings untouched.
## Proxy Partners
Need a proxy provider? Here are partners we recommend:
# Quickstart
Source: https://docs.mobilerun.ai/quickstart
Run your first AI agent on a real mobile device, in the cloud or on your own hardware.
Mobilerun runs AI agents that control real Android and iOS devices. Give the agent a natural language goal such as *"open Settings and turn on dark mode"* and it taps, swipes, and types its way to the result.
There are two ways to use Mobilerun. Pick the path that matches your project and then follow its quickstart.
Hosted devices with zero local setup. Authenticate with a `dr_sk_` key and call the REST API, the TypeScript or Python SDK, or the MCP server. **Start here for most integrations.**
Run the open source `mobilerun` Python package on your own machine against your own Android device over adb. Bring your own LLM provider key.
Prefer starting from working code? The [mobilerun-examples repository](https://github.com/droidrun/mobilerun-examples) has production-minded, runnable integrations, including a TypeScript task runner with a live device stream and a Python signed-webhook receiver.
## Which path is right for me?
| | Cloud | Framework |
| ------------- | ---------------------------------------------------- | --------------------------------------------- |
| Where it runs | Mobilerun's hosted devices | Your machine and your device |
| Setup | None, you provision a device in the dashboard | Install Python, adb, and the Portal APK |
| Auth | Mobilerun key (`dr_sk_...`) plus [credits](/credits) | Your own LLM provider key |
| Languages | TypeScript, Python, REST, MCP | Python |
| Best for | Production automation, CI, scaling | Local development, full control, self hosting |
## Two kinds of API keys
Mobilerun involves two **separate** credentials. Knowing which one you need avoids the most common setup mistake.
* **Mobilerun cloud key (`dr_sk_...`)** authenticates you to the Cloud API, SDK, and MCP server. The agent's LLM calls are billed from your [credit](/credits) balance, so you do **not** need your own model key. Create one on the [API Keys](/api-keys) page.
* **LLM provider key (`GOOGLE_API_KEY`, `OPENAI_API_KEY`, and similar)** is used only by the open source [Framework](/framework/quickstart) running on your own machine. The Framework calls the model provider directly, so there are no Mobilerun credits and no `dr_sk_` key involved.
If you use the Cloud you only need a `dr_sk_` key. If you run the Framework locally you only need an LLM provider key. A single path never needs both.
## Next steps
Run your first cloud task in TypeScript, Python, or cURL.
Install the open source package and run a local agent.
Create and manage your `dr_sk_` cloud keys.
Every task parameter, including models, vision, reasoning, stealth, memory, and structured output.
# Agent Skills
Source: https://docs.mobilerun.ai/skills
Give any AI agent the ability to control Android phones through the Mobilerun skill.
Mobilerun publishes an **agent skill** — a self-contained knowledge pack that tells an AI agent everything it needs to know to use the Mobilerun API. Once installed, the agent can take screenshots, tap and swipe, type, manage apps, and run autonomous tasks on a real Android device, all without any manual API wiring on your part.
## What Is a Skill?
A skill is a bundle of markdown reference files with a `SKILL.md` entry point. When loaded by a compatible agent runtime (such as [OpenClaw](/openclaw)), the agent reads these files at the start of a session and gains structured knowledge about an API — its endpoints, authentication, error handling, and usage patterns.
The Mobilerun skill includes:
| File | Purpose |
| ----------------- | --------------------------------------------------------------------------------------- |
| `SKILL.md` | Entry point — authentication flow, device setup, quick-reference table, common patterns |
| `api.md` | Platform API — device provisioning, AI agent tasks, webhooks, app library |
| `phone-api.md` | Phone control API — screenshot, UI state, tap, swipe, type, app management |
| `setup.md` | Authentication and device connectivity guide |
| `subscription.md` | Plans, pricing, credits, and device types |
## Requirements
* A Mobilerun account at [cloud.mobilerun.ai](https://cloud.mobilerun.ai)
* An API key from the [API Keys](/api-keys) page (`dr_sk_...`)
* An Android device connected via the [Mobilerun Portal](/guides/connect-android) app, or a cloud-hosted device ([Cloud Phone](/device-types#cloud-phone) or [Physical Phone](/device-types#physical-phone))
## Installation
### Via OpenClaw (recommended)
If you are using the OpenClaw agent CLI, install the skill as a plugin:
```bash theme={null}
openclaw plugins install @mobilerun/openclaw-mobilerun
```
See the [OpenClaw setup guide](/openclaw) for full configuration instructions.
### Direct Download (other runtimes)
Download the pre-packaged `.skill` file and load it into any compatible agent runtime:
Download the latest release from GitHub
The `.skill` file is a zip archive containing the full set of reference documents. Refer to your agent runtime's documentation for how to load a `.skill` file.
## What the Skill Enables
Once installed and given an API key, the agent gains two ways to control a device:
Step-by-step device interaction: screenshot, tap, swipe, type, press keys, open apps. Best for quick, precise actions.
Submit a natural language goal (`"Book a table for 2 at 7pm"`) and the Mobilerun AI agent executes it on the device end-to-end. Requires credits.
## How the Agent Uses the Skill
The agent follows a structured initialization flow on every session:
1. **Checks for an API key** — looks in `MOBILERUN_API_KEY` env var or the OpenClaw config file. If missing, it asks the user once.
2. **Verifies the key and discovers devices** — calls `GET /devices`. A `200` response with a `ready` device means the agent proceeds immediately.
3. **Guides setup only when needed** — if no device is found, the agent walks the user through installing the Mobilerun Portal app and connecting.
4. **Executes the request** — once a ready device is confirmed, the agent completes the user's task without additional prompts.
## Available LLM Models for Agent Tasks
When submitting autonomous tasks, you can specify which model drives the agent. The
[Agent](/agent#model-selection) page holds the canonical catalog of `llmModel` values. To see the
live list your account can use, call `GET /v1/models`, or use `client.models.list()` in the SDK.
## Source
The skill is open source and maintained in the [droidrun/skills](https://github.com/droidrun/skills) repository. New releases are built automatically on every push to `master`.
# Tasks
Source: https://docs.mobilerun.ai/tasks
Monitor all your agent tasks, track execution history, and view live progress of running tasks.
The Tasks tab provides a comprehensive view of all agent tasks associated with your account. Monitor active executions in real-time and review historical task data.
## Overview
The Tasks tab displays:
* All tasks you have executed
* Currently running tasks with live status updates
* Task history with execution details and results
## Task List
Your task list shows every task in reverse chronological order, with the most recent tasks at the top. Each task entry displays:
| Field | Description |
| ------------- | ---------------------------------- |
| **Task ID** | Unique identifier for the task |
| **Prompt** | The instruction given to the agent |
| **Device** | The device the task ran on |
| **Status** | Current task state |
| **Duration** | Total execution time |
| **Timestamp** | When the task was created |
## Task Status
Tasks progress through several states during execution:
| Status, with API value | Description |
| ---------------------- | ---------------------------------------------- |
| **`queued`** | Task is accepted and waiting for a free device |
| **`created`** | Task record created, about to start |
| **`running`** | Agent is actively executing the task |
| **`paused`** | Execution is temporarily suspended |
| **`cancelling`** | A cancel request is being applied |
| **`completed`** | Task finished successfully |
| **`failed`** | Task encountered an error and stopped |
| **`cancelled`** | Task was stopped before completion |
When you poll a task, treat `completed`, `failed`, and `cancelled` as the terminal states. Stop
polling once the status is one of these three values.
## Live Updates
The Tasks tab updates in real-time. When a task is running:
* Status changes appear instantly
* Execution progress is visible without refreshing
* New tasks appear at the top of the list automatically
You do not need to refresh the page to see task updates. The interface synchronizes automatically with the server.
## Task Details
Click on any task to view its full details:
### Execution Trajectory
The trajectory shows every action the agent took during execution:
* Screenshots at each step
* UI elements the agent interacted with
* Decisions and reasoning (when available)
* Timestamps for each action
### Task Output
For tasks with structured output schemas, view the extracted data in a formatted display. This is useful for:
* Data extraction tasks
* Verification workflows
* Automated testing results
### Error Information
For failed tasks, the details view shows:
* Error messages and stack traces
* The last successful step before failure
* Screenshots capturing the failure state
## Filtering and Search
Find specific tasks using the available filters:
| Filter | Description |
| -------------- | ---------------------------------------- |
| **Status** | Show only tasks with a specific status |
| **Date Range** | Filter by execution date |
| **Device** | Show tasks that ran on a specific device |
| **Search** | Find tasks by prompt text or task ID |
## Task Actions
From the task list, you can:
### View Task Stream
For running tasks, click to open the live device stream and watch the agent execute in real-time.
### Cancel Running Tasks
Stop a task that is currently in progress. The agent will halt execution and the task will be marked as cancelled.
The REST API endpoint for cancellation is `POST /tasks/{id}/cancel`. The TypeScript and Python SDKs name this method `client.tasks.stop(taskId)` it hits the same `/cancel` endpoint.
### Retry Failed Tasks
Re-run a failed task with the same configuration. This creates a new task with identical parameters.
# Webhooks
Source: https://docs.mobilerun.ai/webhooks
Receive real-time notifications when task events occur in your Mobilerun account.
The Webhooks tab allows you to configure HTTP callbacks that notify your systems when events occur. Use webhooks to integrate Mobilerun with your existing infrastructure and automation pipelines.
## What is a Webhook?
A webhook is an HTTP request that Mobilerun sends to your server when a specific event happens. Instead of polling the API to check for updates, your server receives notifications automatically in real-time.
When an event occurs:
1. Mobilerun detects the event (e.g., a task completes)
2. Mobilerun sends an HTTP POST request to your configured URL
3. Your server receives the payload and processes it
This enables real-time integrations without constant API polling.
## Event Triggers
Configure webhooks to fire on any of these task events:
| Event | Description |
| ------------- | ---------------------------------------- |
| **created** | A new task has been submitted |
| **running** | A task has started executing on a device |
| **paused** | A task execution has been paused |
| **completed** | A task finished successfully |
| **failed** | A task encountered an error and stopped |
| **cancelled** | A task was manually cancelled |
You can subscribe to multiple events with a single webhook, or create separate webhooks for different event types.
## Creating a Webhook
To add a new webhook:
1. Click **Add Webhook**
2. Enter your endpoint URL (must be HTTPS)
3. Select which events should trigger the webhook
4. Save the webhook
Your endpoint will start receiving events immediately.
## Webhook Payload
When an event fires, Mobilerun sends a POST request with a JSON payload containing:
```json theme={null}
{
"event": "completed",
"task": {
"id": "task_abc123",
"task": "Open Settings and enable dark mode",
"status": "completed",
"deviceId": "device_xyz",
"createdAt": "2025-01-08T10:30:00Z",
"finishedAt": "2025-01-08T10:32:45Z",
"succeeded": true,
"output": {}
}
}
```
## Managing Webhooks
Your webhook list displays:
| Field | Description |
| ----------- | --------------------------------- |
| **URL** | The endpoint receiving events |
| **Events** | Which events trigger this webhook |
| **State** | Active, disabled, or deleted |
| **Created** | When the webhook was configured |
### Webhook States
| State | Description |
| ------------ | ------------------------------------------ |
| **Active** | Webhook is receiving events normally |
| **Disabled** | Webhook is paused and not receiving events |
| **Deleted** | Webhook has been removed |
### Edit Webhooks
Update a webhook to change:
* The events it subscribes to
* Its active/disabled state
### Delete Webhooks
Remove webhooks that are no longer needed. Deleted webhooks stop receiving events immediately.
## Use Cases
Trigger deployment pipelines when test tasks complete successfully.
Send notifications to Slack or email when tasks fail.
Store task results in your database automatically.
Chain tasks together by starting new tasks when others complete.
## Integration Examples
### Zapier
Connect Mobilerun to thousands of apps using Zapier. Trigger Zaps when tasks complete and automate downstream workflows.
### n8n
Use webhooks to integrate with n8n workflows for custom automation logic.
### Custom Endpoints
Build your own webhook receiver to process events however you need:
```python theme={null}
from flask import Flask, request
app = Flask(__name__)
@app.route("/webhook", methods=["POST"])
def handle_webhook():
payload = request.json
event = payload["event"]
task = payload["task"]
if event == "completed" and task["succeeded"]:
# Handle successful task
pass
elif event == "failed":
# Handle failed task
pass
return "", 200
```
For a complete, runnable receiver with HMAC verification, replay protection, and secure file
downloads, see the
[Python webhook receiver example](https://github.com/droidrun/mobilerun-examples/tree/main/examples/python/webhook-file-receiver).
## Best Practices
| Practice | Description |
| --------------------- | -------------------------------------------------------- |
| **Use HTTPS** | Always use secure endpoints for webhook URLs |
| **Respond quickly** | Return a 2xx status code promptly to acknowledge receipt |
| **Handle duplicates** | Design your handler to be idempotent in case of retries |
| **Verify payloads** | Validate incoming data before processing |