# 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.
This model list applies to Mobilerun Cloud. For the open source Framework, choose a provider and
model in the [Framework configuration](/framework/sdk/configuration#llm-configuration).
### 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"
```
For `mobilerun` CLI commands that use Cloud devices, sign in with `mobilerun login` or set `MOBILERUN_CLOUD_API_KEY`. The SDK and HTTP examples on this page use `MOBILERUN_API_KEY`.
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, and the identifier must match the device's platform — a packageName on an iOS device or a bundleId on an Android device is rejected with 400. 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.
# Abort the named session's in-flight chat turn
Source: https://docs.mobilerun.ai/api-reference/chat/abort-the-named-sessions-in-flight-chat-turn
/api-reference/mobilerun-va-chat.yaml post /assistant/chat/abort
Abort the in-flight chat turn owned by `sessionId`. Idempotent. A turn owned by a different session is left untouched (204).
# Create a named chat session
Source: https://docs.mobilerun.ai/api-reference/chat/create-a-named-chat-session
/api-reference/mobilerun-va-chat.yaml post /assistant/chat/sessions
Creates a titled agent session. Setup may occur on the first prompt. Idempotent via the `Idempotency-Key` header — a duplicate submit by the same authenticated caller within the 24-hour idempotency window returns the already-created session instead of a second one.
# Deliver a HITL approval/rejection to the user's active chat session
Source: https://docs.mobilerun.ai/api-reference/chat/deliver-a-hitl-approvalrejection-to-the-users-active-chat-session
/api-reference/mobilerun-va-chat.yaml post /assistant/chat/permission
Deliver a HITL approval/rejection for an in-flight turn.
# Deliver a question answer to the user's active chat session
Source: https://docs.mobilerun.ai/api-reference/chat/deliver-a-question-answer-to-the-users-active-chat-session
/api-reference/mobilerun-va-chat.yaml post /assistant/chat/question
Deliver the user's answers to the agent's pending question for an in-flight turn. Idempotent via the `idempotency-key` header.
# Dismiss an outstanding question without answering
Source: https://docs.mobilerun.ai/api-reference/chat/dismiss-an-outstanding-question-without-answering
/api-reference/mobilerun-va-chat.yaml post /assistant/chat/question/reject
Dismiss the agent's pending question. Already-resolved questions return 200 (no-op) so multi-tab dismiss stays idempotent.
# List messages for a chat session
Source: https://docs.mobilerun.ai/api-reference/chat/list-messages-for-a-chat-session
/api-reference/mobilerun-va-chat.yaml get /assistant/chat/messages
Return the user's chat history for the given session. History remains readable after the session is no longer active.
# List the user's named chat or agent-workflow sessions
Source: https://docs.mobilerun.ai/api-reference/chat/list-the-users-named-chat-or-agent-workflow-sessions
/api-reference/mobilerun-va-chat.yaml get /assistant/chat/sessions
Default (`kind` absent or `chat`): active named chat sessions, pinned first then most recent activity — `workflowId` must be absent, or the request 400s. `mine=true` (only valid with `kind=chat`) narrows to sessions the caller created. `kind=agent_workflow`: workflow-linked sessions for one workflow (`workflowId` required, or the request 400s), no status filter, newest episode first.
# Re-attach to the active turn stream
Source: https://docs.mobilerun.ai/api-reference/chat/re-attach-to-the-active-turn-stream
/api-reference/mobilerun-va-chat.yaml get /assistant/chat/stream
Reconnect to the in-flight turn stream. Replays buffered events from the start of the active turn, then continues live until the turn finishes. Responds 204 when no active turn exists for the requested session. Upstream streaming failures return a retryable 503 with `Retry-After`. Resume is best-effort. Does not start an inactive session.
# Rename, archive, or pin a named chat session
Source: https://docs.mobilerun.ai/api-reference/chat/rename-archive-or-pin-a-named-chat-session
/api-reference/mobilerun-va-chat.yaml patch /assistant/chat/sessions/{id}
Rename, change status, and/or pin. Title updates apply best-effort. Archiving always clears the pinned flag. `title` is rejected with 409 `code: "session_title_managed_by_workflow"` when this chat is bound to a workflow — a bound chat's title is managed by the workflow. Other fields (description, status, pinned) remain updateable; archiving a bound chat stays allowed.
# Send a single user message
Source: https://docs.mobilerun.ai/api-reference/chat/send-a-single-user-message
/api-reference/mobilerun-va-chat.yaml post /assistant/chat/message
Send a single user message. The response format follows the Accept header: `text/event-stream` for SSE, `application/json` for a buffered assistant reply. `sessionId` targets a concrete active chat. The resolved chat session ID is returned as `chatSessionId` in the JSON body and as the `X-Chat-Session-Id` response header on the SSE response.
# 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, platform (android or ios), 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.
# Resume (unpark) a stopped device
Source: https://docs.mobilerun.ai/api-reference/devices/resume-unpark-a-stopped-device
/api-reference/phones.yaml post /devices/{deviceId}/resume
Wakes a parked device: capacity is preflighted (the device's data may be replicated to another node if its home is full), the device starts running again, and per-minute billing resumes. On a device that is not parked this is a no-op ready transition.
# Stop (park) a device
Source: https://docs.mobilerun.ai/api-reference/devices/stop-park-a-device
/api-reference/phones.yaml post /devices/{deviceId}/stop
Parks the device: its data, apps and identity are kept, but nothing runs and nothing is billed until it is resumed. Only devices whose capabilities report stop=true support this; others return 404.
# 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.
# Compact owner-scoped eSIM list for a filter/selector UI
Source: https://docs.mobilerun.ai/api-reference/esims/compact-owner-scoped-esim-list-for-a-filterselector-ui
/api-reference/mobilerun-numbers.yaml get /numbers/esims/selector
Returns a lightweight list (id, msisdn, carrierName, status, masked iccid) for use in a message filter dropdown. Unlike `GET /esims`, this includes all statuses, including retired eSIMs.
# FE-return payment confirmation poll (BYO rent, pay-first)
Source: https://docs.mobilerun.ai/api-reference/esims/fe-return-payment-confirmation-poll-byo-rent-pay-first
/api-reference/mobilerun-numbers.yaml post /numbers/esims/{id}/confirm-payment
Checks for proof of payment for this eSIM's current rent and confirms it if found. If no proof is available yet, returns 200 with the eSIM unchanged rather than an error. Always returns the current eSIM state.
# Get physical eSIM by id
Source: https://docs.mobilerun.ai/api-reference/esims/get-physical-esim-by-id
/api-reference/mobilerun-numbers.yaml get /numbers/esims/{id}
Retrieves a single physical eSIM.
# Import a BYO (tenant-supplied) physical eSIM activation code
Source: https://docs.mobilerun.ai/api-reference/esims/import-a-byo-tenant-supplied-physical-esim-activation-code
/api-reference/mobilerun-numbers.yaml post /numbers/esims/import
Registers a bring-your-own (BYO) eSIM activation code as owned inventory. Provide either `{ smdpAddress, matchingId?, confirmationCode? }` or `{ lpaCode }` — supplying both, or neither, returns 400. An optional `name` sets a display label on the created eSIM (up to 15 characters).
Subject to per-owner and daily import limits, and disabled entirely unless BYO imports are enabled for this deployment (409 `byo_disabled`). Idempotent via `idempotencyKey`: replaying the same key with an identical request returns the original response; the same key with a different request returns 409 `idempotency_conflict`.
When rent-first billing is off (default), the import is free — 201 with the eSIM. Setting `autoInstall: true` additionally dispatches an install immediately after import (`deviceId` may only be set together with `autoInstall`): this returns 202 with `{esim, operationId, statusUrl}` when the install claim succeeds (poll `GET /esims/{id}/install-status`), or 201 with the eSIM plus `installDispatch: {ok: false, reason}` when the install could not be dispatched — the import itself still succeeds either way.
When rent-first billing is on, import additionally requires available device capacity (409 `device_pool_empty`) and is subject to a per-owner awaiting-payment cap (409 `byo_awaiting_payment_cap`). On success the eSIM is created `awaiting_payment` and a checkout is started: 201 with `{esim, rentStatus, checkoutUrl}` when the checkout URL is ready immediately, or 202 with `checkoutUrl: null` otherwise — poll `GET /esims/{id}` until it's populated. Once payment is confirmed, install is triggered automatically.
# Install a physical eSIM on a device
Source: https://docs.mobilerun.ai/api-reference/esims/install-a-physical-esim-on-a-device
/api-reference/mobilerun-numbers.yaml post /numbers/esims/{id}/install
Installs the eSIM's activation code onto a device. `deviceId` is optional — omit it to use an available device from the pool. This call is asynchronous: it returns 202 with `{esim, operationId, statusUrl}` immediately, and the result is available by polling `GET /esims/{id}/install-status`. Retrying with the same request is safe if a response is lost.
Returns 409 when the eSIM is not in the `owned` state, or when no device is currently available (see `reason`). When rent-first billing is enabled, a BYO eSIM whose rent isn't active returns 402 with `{esim, rentStatus, checkoutUrl}` instead.
# List physical eSIMs
Source: https://docs.mobilerun.ai/api-reference/esims/list-physical-esims
/api-reference/mobilerun-numbers.yaml get /numbers/esims
Lists physical eSIMs owned by the authenticated owner.
# Poll a physical eSIM's install status
Source: https://docs.mobilerun.ai/api-reference/esims/poll-a-physical-esims-install-status
/api-reference/mobilerun-numbers.yaml get /numbers/esims/{id}/install-status
Returns the eSIM's current install status, checking for a terminal outcome if an install is still in progress.
# Pool-device capacity hint for the BYO upload flow
Source: https://docs.mobilerun.ai/api-reference/esims/pool-device-capacity-hint-for-the-byo-upload-flow
/api-reference/mobilerun-numbers.yaml get /numbers/esims/capacity
Reports whether a free device is currently available, for pre-checking the import flow before upload. This is a hint only, not a reservation — `POST /esims/import` re-checks availability at submit time.
# Purchase a physical eSIM
Source: https://docs.mobilerun.ai/api-reference/esims/purchase-a-physical-esim
/api-reference/mobilerun-numbers.yaml post /numbers/esims
Purchases a physical eSIM from available inventory for the authenticated owner. Returns 409 when no stock is available, or 402 with a billing checkout URL when billing capacity is exhausted.
# Remove a physical eSIM (owner-facing, both sources)
Source: https://docs.mobilerun.ai/api-reference/esims/remove-a-physical-esim-owner-facing-both-sources
/api-reference/mobilerun-numbers.yaml delete /numbers/esims/{id}
Removes a physical eSIM. Idempotent — returns 204 for a fresh removal or a replay of an already-removed eSIM. An eSIM currently installed on a device is uninstalled first, then removed. An eSIM in an intermediate install state returns 409 `operator_resolution_required` and requires manual resolution. Returns 404 if the eSIM doesn't exist or isn't owned by the caller.
# Update a physical eSIM's self-reported MSISDN and/or display name
Source: https://docs.mobilerun.ai/api-reference/esims/update-a-physical-esims-self-reported-msisdn-andor-display-name
/api-reference/mobilerun-numbers.yaml patch /numbers/esims/{id}
Updates the eSIM's self-reported msisdn and/or display name. Both fields are optional, but the request body itself is required. Omitting a field leaves it unchanged; setting it to null or an empty string clears it. `name` is capped at 15 characters. Available regardless of the eSIM's current status.
# 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.
# Cancel a pending upload (transitions to expired, deletes the underlying object)
Source: https://docs.mobilerun.ai/api-reference/files/cancel-a-pending-upload-transitions-to-expired-deletes-the-underlying-object
/api-reference/mobilerun-va-chat.yaml delete /agents/files/{fileId}/pending
Soft-cancels an in-flight upload before confirm. Only acts on `pending` rows — refuses to touch `ready` to avoid wiping confirmed files. Idempotent: `{ cancelled: false }` if the row exists but is no longer pending.
# Confirm a file upload by server-side HEAD validation
Source: https://docs.mobilerun.ai/api-reference/files/confirm-a-file-upload-by-server-side-head-validation
/api-reference/mobilerun-va-chat.yaml post /agents/files/{fileId}/confirm
# 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 an owned file
Source: https://docs.mobilerun.ai/api-reference/files/download-an-owned-file
/api-reference/mobilerun-va-chat.yaml get /agents/files/{fileId}/download
Redirects to a short-lived presigned download URL. Missing, non-ready, and files owned by another tenant all return the same 404 response.
# 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.
# Hard-delete a file
Source: https://docs.mobilerun.ai/api-reference/files/hard-delete-a-file
/api-reference/mobilerun-va-chat.yaml delete /agents/files/{fileId}
# 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.
# List the user's ready files, optionally filtered by zone
Source: https://docs.mobilerun.ai/api-reference/files/list-the-users-ready-files-optionally-filtered-by-zone
/api-reference/mobilerun-va-chat.yaml get /agents/files
# Mint a presigned PUT URL for a user file upload
Source: https://docs.mobilerun.ai/api-reference/files/mint-a-presigned-put-url-for-a-user-file-upload
/api-reference/mobilerun-va-chat.yaml post /agents/files/upload-url
# Update file metadata (skills zone only)
Source: https://docs.mobilerun.ai/api-reference/files/update-file-metadata-skills-zone-only
/api-reference/mobilerun-va-chat.yaml patch /agents/files/{fileId}
Partial update of `displayName` and/or `enabled`. Only files with `zone=skills` are mutable; other zones return 422 `unsupported_zone`.
# 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.
# Get a mailbox message
Source: https://docs.mobilerun.ai/api-reference/mailbox-messages/get-a-mailbox-message
/api-reference/mobilerun-mailbox.yaml get /mailboxes/{mailboxId}/messages/{messageId}
# List mailbox messages
Source: https://docs.mobilerun.ai/api-reference/mailbox-messages/list-mailbox-messages
/api-reference/mobilerun-mailbox.yaml get /mailboxes/{mailboxId}/messages
Lists messages for a mailbox with keyset pagination and time/sender/hasOtp filters for polling.
# Cancel or archive a mailbox
Source: https://docs.mobilerun.ai/api-reference/mailboxes/cancel-or-archive-a-mailbox
/api-reference/mobilerun-mailbox.yaml delete /mailboxes/{mailboxId}
For paid rent, schedules end-of-cycle cancellation. For an included generation, archives immediately and releases its package seat. This never deletes the mailbox, its address, or its messages — the address is permanently reserved. Idempotent.
# Claim or rent a mailbox
Source: https://docs.mobilerun.ai/api-reference/mailboxes/claim-or-rent-a-mailbox
/api-reference/mobilerun-mailbox.yaml post /mailboxes
Reserves a permanently-allocated, individually-rented mailbox and starts an Autumn rental checkout. An optional localPart selects the full address local part; omitting it keeps the default random, non-guessable mx_-prefixed address. The address is withheld until the first payment is confirmed. Idempotent on (owner, clientRequestId): same key + payload replays (200); a conflicting or already-held local part returns 409. 201 when the checkout URL is already persisted, otherwise 202 (poll GET for the URL).
# Get a mailbox
Source: https://docs.mobilerun.ai/api-reference/mailboxes/get-a-mailbox
/api-reference/mobilerun-mailbox.yaml get /mailboxes/{mailboxId}
# Get included mailbox capacity
Source: https://docs.mobilerun.ai/api-reference/mailboxes/get-included-mailbox-capacity
/api-reference/mobilerun-mailbox.yaml get /mailboxes/capacity
Returns the authoritative number of package-funded mailbox claims currently available after local reservations.
# Get the best OTP candidate
Source: https://docs.mobilerun.ai/api-reference/mailboxes/get-the-best-otp-candidate
/api-reference/mobilerun-mailbox.yaml get /mailboxes/{mailboxId}/otp
Returns the highest-confidence, most recent OTP for the mailbox, restricted to messages of completed/active paid intervals. Does not wait server-side (SDKs poll). 200 with the best code, 204 when none matches.
# List mailboxes
Source: https://docs.mobilerun.ai/api-reference/mailboxes/list-mailboxes
/api-reference/mobilerun-mailbox.yaml get /mailboxes
Lists the caller-owned mailboxes with page-based pagination.
# Restart rent on an archived mailbox
Source: https://docs.mobilerun.ai/api-reference/mailboxes/restart-rent-on-an-archived-mailbox
/api-reference/mobilerun-mailbox.yaml post /mailboxes/{mailboxId}/restart
Starts a new generation on an archived mailbox, reusing the same permanent address. Uses included capacity first unless paid rent is requested.
# Undo a scheduled cancellation
Source: https://docs.mobilerun.ai/api-reference/mailboxes/undo-a-scheduled-cancellation
/api-reference/mobilerun-mailbox.yaml post /mailboxes/{mailboxId}/uncancel
Retracts a scheduled end-of-cycle cancellation for the current generation. Only valid while cancellation is pending.
# Update a mailbox label
Source: https://docs.mobilerun.ai/api-reference/mailboxes/update-a-mailbox-label
/api-reference/mobilerun-mailbox.yaml patch /mailboxes/{mailboxId}
Updates the label of a mailbox.
# List messages for one eSIM
Source: https://docs.mobilerun.ai/api-reference/messages/list-messages-for-one-esim
/api-reference/mobilerun-numbers.yaml get /numbers/esims/{id}/messages
# List the caller's own SMS conversations, one row per thread
Source: https://docs.mobilerun.ai/api-reference/messages/list-the-callers-own-sms-conversations-one-row-per-thread
/api-reference/mobilerun-numbers.yaml get /numbers/messages/conversations
Lists the caller's own SMS conversations, one row per thread. Each row includes the most recent message in the thread, its unread inbound count, and the eSIMs it was seen through. Optional `esimId` or `numberId` narrows to threads on one eSIM or number.
Cursor-paginated via `limit` (default 20, max 100) and `cursorLastOccurredAt`/`cursorLastMessageId` (both required together, taken from a previous page's `nextCursor`). Pagination follows each thread's most recent activity rather than a fixed snapshot, so a thread with new activity can move ahead of an in-progress page fetch. Clients that need a stable ordering should snapshot their own view.
# List the caller's own SMS inbox/outbox messages
Source: https://docs.mobilerun.ai/api-reference/messages/list-the-callers-own-sms-inboxoutbox-messages
/api-reference/mobilerun-numbers.yaml get /numbers/messages
Lists the caller's own SMS messages, newest first. Supports filtering by direction, esimId, numberId, status, peerNumber (substring search, min 3 characters), and peerKey (exact thread match). Each row includes its canonical thread key (`peerKey`).
# Mark a conversation's inbound messages read up to a cursor
Source: https://docs.mobilerun.ai/api-reference/messages/mark-a-conversations-inbound-messages-read-up-to-a-cursor
/api-reference/mobilerun-numbers.yaml post /numbers/messages/conversations/read
Marks the caller's own inbound messages in a conversation thread as read, up to and including the given `(upToOccurredAt, upToMessageId)` cursor — typically a conversation row's `lastMessage`. Idempotent: repeating the call with the same cursor updates 0 rows. Returns the number of rows updated.
# Send an SMS through one eSIM
Source: https://docs.mobilerun.ai/api-reference/messages/send-an-sms-through-one-esim
/api-reference/mobilerun-numbers.yaml post /numbers/esims/{id}/messages
# 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 clipboard
Source: https://docs.mobilerun.ai/api-reference/misc/get-device-clipboard
/api-reference/phones/tools.yaml get /devices/{deviceId}/clipboard
Returns the current text content of the device's clipboard. Devices without clipboard support return an unsupported-feature error.
# 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 clipboard
Source: https://docs.mobilerun.ai/api-reference/misc/set-device-clipboard
/api-reference/phones/tools.yaml post /devices/{deviceId}/clipboard
Replaces the device's clipboard content with the text in the request body; an empty text clears the clipboard. Devices without clipboard 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.
# List Models
Source: https://docs.mobilerun.ai/api-reference/models/list-models
/api-reference/tasks.yaml get /models
List available 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.
# Update a phone number's display label
Source: https://docs.mobilerun.ai/api-reference/numbers/update-a-phone-numbers-display-label
/api-reference/mobilerun-numbers.yaml patch /numbers/phones/{id}
Updates the phone number's user-defined display label. Omitting `label` leaves it unchanged; setting it to null or an empty string clears it. The label is capped at 100 characters, is display-only, and never affects routing. It also seeds the billing entity name when set at purchase time; a later change here does not rename the already-created billing entity.
# 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.
### Reset a Device
Resetting returns a device to a clean state, clearing installed apps and user data accumulated during sessions. Trigger a reset through the API or SDK:
```typescript TypeScript theme={null}
import Mobilerun from '@mobilerun/sdk';
const client = new Mobilerun({ apiKey: process.env.MOBILERUN_CLOUD_API_KEY });
await client.devices.reset('deviceId');
```
```python Python theme={null}
import os
from mobilerun_sdk import Mobilerun
client = Mobilerun(api_key=os.environ.get("MOBILERUN_CLOUD_API_KEY"))
client.devices.reset("deviceId")
```
The device reports the `resetting` state for the entire reset, including the reconnection and setup steps that follow the wipe. It returns to `ready` once the reset completes.
Resetting a Personal Phone factory-resets the handset. The phone goes offline for several minutes while it wipes and reboots. Mobilerun keeps the device in `resetting` for up to 5 minutes while it waits for the phone to reconnect. When the phone comes back online, the device returns to `ready` within seconds. If the phone does not reconnect within that window, Mobilerun terminates the device.
### Monitor Usage
Track your device utilization:
* Active session time
* Task execution history
## Subscription Errors
When you create a device or connect your own phone, Mobilerun checks that your account has a matching subscription with a free device slot. If the check fails, the API returns one of three errors:
| Status | Code | Meaning | What to do |
| ------ | ----------------------- | --------------------------------------------------------------- | ------------------------------------------------------------ |
| `402` | `SUBSCRIPTION_REQUIRED` | Your account has no active subscription for this device type. | Subscribe or upgrade your plan, then try again. |
| `429` | `RESOURCE_EXHAUSTED` | All device slots in your plan are in use. | Terminate a device or upgrade your plan to add another. |
| `402` | `PAYMENT_REQUIRED` | The billing check could not be completed and access was denied. | Try again shortly, or contact support if the error persists. |
Example error body from a device creation request:
```json theme={null}
{
"code": "SUBSCRIPTION_REQUIRED",
"message": "You need an active subscription to create this device. Subscribe or upgrade your plan, then try again."
}
```
Both `SUBSCRIPTION_REQUIRED` and `PAYMENT_REQUIRED` return HTTP 402. Match on the `code` field to tell them apart. If your own phone cannot connect through the Portal app, verify that your [Personal Phone](/device-types#personal-phone) subscription is active.
# 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`
**Common actions include:**
```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=None, clear=False), type_text(text, clear=False),
type_secret(secret_id, index),
swipe(coordinate, coordinate2, duration=1.0), system_button(button),
wait(duration=1.0), open_app(text | bundle_id | app_id),
complete(success, message)
```
`type_secret` is available when credentials are configured. For `open_app`, use `text` for an Android app name, `bundle_id` for an iOS app, or `app_id` for an exact app ID.
## 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.5-flash-lite
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.
## Choosing the Manager Prompt
The Manager chooses its prompt in this order:
1. `manager_system` passed to `MobileAgent`
2. The file set in `config.agent.manager.system_prompt`
3. The built-in prompt for the selected stateful or stateless mode
## 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 when reasoning mode is disabled |
| `fast_agent_user` | FastAgent | Initial task input when reasoning mode is disabled |
## Context Variables
Each agent has access to different variables in its templates:
### Manager (stateful)
* `instruction` - User's goal
* `platform` - Active device platform
* `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)
### Manager (stateless)
* `instruction` - User's goal
* `platform` - Active device platform
* `device_date` - Current device date/time
* `previous_plan` - Plan returned on the previous turn
* `previous_state` - Device state from the previous turn
* `memory` - Facts saved by earlier Manager responses
* `last_thought` - Previous manager rationale
* `progress_summary` - Previous cumulative progress summary
* `action_history` - Actions and outcomes available for the current turn
* `current_state` - Current formatted device state
If you provide `manager_system`, use the variables and final response format for the selected Manager mode.
### Executor
* `instruction` - User's goal
* `app_card` - App-specific guidance
* `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
* `platform` - Active device platform
### FastAgent
**System prompt:**
* `tool_descriptions` - Available tool signatures
* `available_secrets` - Credential IDs
* `available_tools` - Tools available to the agent
* `variables` - Custom variables
* `output_schema` - Output model schema (if provided)
* `parallel_tools` - Whether the agent can call multiple tools at once
* `vision` - Whether screenshots are included
* `platform` - Active device platform
* `screenshot_only` - Whether screenshots are used without UI element data
**User prompt:**
* `goal` - Task description
* `variables` - Custom variables
## Manager Response Format
Custom Manager prompts must return one result block:
* Use `...` while work remains.
* In stateful mode, finish with `...`, or set `success="false"` if blocked.
* In stateless mode, finish with `...`, or set `success="false"` if blocked.
``, ``, and `` blocks may appear before the result.
Valid unfinished response:
```xml theme={null}
The next screen still needs input.
1. Enter the shipping address
2. Review the order
```
Valid stateful final response:
```xml theme={null}
The order was submitted and confirmation 1234 was shown.
```
Valid stateless final response:
```xml theme={null}
The requested setting was enabled.
The setting is enabled.
```
## Example: Custom Manager Prompt
```python theme={null}
from mobilerun import AgentConfig, MobileAgent, MobileConfig
config = MobileConfig(agent=AgentConfig(reasoning=True))
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 %}
Return one result:
- ... while work remains.
- ... when complete.
- ... when blocked.
Optional and blocks may appear before the result.
"""
}
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}
from mobilerun import AgentConfig, MobileAgent, MobileConfig
config = MobileConfig(agent=AgentConfig(reasoning=True))
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 %}
Return one result: ... while work remains,
... when complete,
or the same tag with success="false" when blocked.
"""
}
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 AgentConfig, MobileAgent, 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
Return one result: ... while work remains,
... when complete,
or the same tag with success="false" when blocked. Optional and
blocks may appear before the result.
"""
}
config = MobileConfig(agent=AgentConfig(reasoning=True))
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
State available to custom tools during a Mobilerun task.
## Shared State
`MobileAgentState` stores information about the current run. Custom tools can read it through `ctx.shared_state`.
| Field | Description |
| ------------------------ | ------------------------------------------------------- |
| `step_number` | Current execution step |
| `platform` | Active device platform |
| `formatted_device_state` | Current screen and UI elements |
| `current_package_name` | Current app package |
| `action_history` | Actions already performed |
| `action_outcomes` | Success or failure of each action |
| `agent_memory` | Facts saved by the Manager or FastAgent during this run |
| `custom_variables` | Values passed through `MobileAgent(variables=...)` |
| `finished` | Whether the run has finished |
| `success` | Whether the run succeeded |
# 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) -> str:
"""
Args:
arg1: Your parameter
arg2: Another parameter
ctx: Access to the device and current run
"""
# Implementation
return "result"
```
**Key points:**
* List only user parameters in `"parameters"` (not `ctx`)
* Mobilerun passes `ctx` automatically
* Access device via `ctx.driver`, shared state via `ctx.shared_state`, credentials via `ctx.credential_manager`
* Return type should be `str`
***
## Using ActionContext
Use `ctx` to access the device and current run:
```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"
# Read information saved during this run
if "skip_validation" in shared_state.agent_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
* `agent_memory` - Information saved by the Manager or FastAgent during the run
* `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.5-flash-lite
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.5-flash-lite"),
"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
Disable telemetry for an agent in Python:
```python theme={null}
from mobilerun import MobileConfig, TelemetryConfig
config = MobileConfig(telemetry=TelemetryConfig(enabled=False))
```
Or set the equivalent value in your YAML configuration:
```yaml theme={null}
telemetry:
enabled: false
```
You can also disable telemetry for every Mobilerun process in your environment:
```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 sent only when both `telemetry.enabled` and the environment setting allow it. An explicit `false` in either place always disables telemetry, so an environment value of `true` cannot override `telemetry.enabled: false`.
***
# 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, xAI, 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"
```
Mobilerun creates `config.yaml` automatically in `~/.config/droidrun` on Linux, `~/Library/Application Support/droidrun` on macOS, or `%LOCALAPPDATA%\droidrun\droidrun` on Windows. Set `MOBILERUN_CONFIG` or pass `--config` to use another file.
***
## Commands
Run `mobilerun --version` to print the installed package version.
Execute natural language commands on your device.
### Usage
```bash theme={null}
mobilerun run "" [OPTIONS]
```
### Flags
| Flag | Description | Default |
| ------------------------------------ | ---------------------------------------------------------------------- | -------------------------------------------- |
| `--config`, `-c` | Explicit config file | `MOBILERUN_CONFIG`, then the platform config |
| `--device`, `-d` | Device serial/IP, iOS local-server URL, or visual-remote URL | From config / auto-detect |
| `--device-id` | Device to use when a server provides more than one | From config (`auto` initially) |
| `--agent`, `-a` | Android external agent name | From config (`mobilerun` initially) |
| `--provider`, `-p` | LLM provider override | From config |
| `--model`, `-m` | LLM model override | From config |
| `--temperature` | LLM temperature override | From the selected profile |
| `--steps` | Maximum execution steps | From config (`15` initially) |
| `--base_url`, `-u` | Base URL for providers such as Ollama or OpenRouter | Provider profile |
| `--api_base` | API base for OpenAI-compatible providers | Provider profile |
| `--vision` / `--no-vision` | Enable or disable screenshots for all agents | From config |
| `--vision-only` / `--no-vision-only` | Use screenshots without an accessibility tree | From config |
| `--reasoning` / `--no-reasoning` | Enable or disable Manager-Executor planning | From config |
| `--stream` / `--no-stream` | Stream LLM responses | From config |
| `--tracing` / `--no-tracing` | Enable or disable the configured tracing provider (Phoenix by default) | From config |
| `--debug` / `--no-debug` | Enable verbose logging | From config |
| `--tcp` / `--no-tcp` | Override Android TCP communication | From config |
| `--control-backend visual-remote` | Connect to a Visual Remote server | From config |
| `--save-trajectory` | Save execution at `none`, `step`, or `action` level | From config (`none` initially) |
| `--ios` | Use an iOS device | From config (`android` initially) |
Pass `--provider` and `--model` together. Supplying only one causes the run to fail before the agent starts.
### 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.7-flash
# OpenAI
export OPENAI_API_KEY=your-key
mobilerun run "Create shopping list" \
--provider OpenAI \
--model gpt-5.5
# xAI Grok
export XAI_API_KEY=your-key
mobilerun run "Open Settings" \
--provider XAI \
--model grok-4.6
# Anthropic Claude
export ANTHROPIC_API_KEY=your-key
mobilerun run "Reply to latest email" \
--provider Anthropic \
--model claude-sonnet-4-6
# Local Ollama (free)
mobilerun run "Turn on dark mode" \
--provider Ollama \
--model llama3.3:70b \
--base_url http://localhost:11434
```
```bash theme={null}
# Use screenshots without an accessibility tree
mobilerun run "Check Wi-Fi" --vision-only
# Use a compatible visual-remote server
mobilerun run "Check Wi-Fi" \
--vision-only \
--control-backend visual-remote \
--device http://localhost:8090
```
Visual Remote controls the device from screenshots and coordinate-based actions. Some Visual Remote servers may not support opening apps.
```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` |
| XAI | Included by default | `XAI_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 | Included by default | `DEEPSEEK_API_KEY` |
| MiniMax | Included by default | `MINIMAX_API_KEY` |
| ZAI | Included by default | `ZAI_API_KEY` |
The wizard supports API-key and OAuth sign-in. MiniMax uses separate global and Mainland China endpoints, and ZAI also offers Coding API authentication.
### `mobilerun configure`
Run the wizard to choose a provider, sign-in method, and model.
```bash theme={null}
# Interactive setup
mobilerun configure
# Skip the wizard questions (OAuth still requires approval if you are not signed in)
mobilerun configure \
--provider openai \
--auth-mode oauth \
--model gpt-5.5
```
| Flag | Description | Default |
| ------------- | ------------------------------------------------------------------------------------ | ---------------------------- |
| `--provider` | `gemini`, `openai`, `anthropic`, `xai`, `ollama`, `openai_like`, `minimax`, or `zai` | Prompted |
| `--auth-mode` | How to sign in to the provider | Prompted |
| `--model` | Model to save in every built-in agent profile | Prompted |
| `--api-key` | API key for an API-key provider | Existing saved key or prompt |
| `--base-url` | Compatible-provider base URL | Provider default or prompt |
### Provider OAuth login commands
Use these commands to sign in with OAuth. Run `mobilerun configure` to change the saved provider or model.
```bash theme={null}
mobilerun configure openai
mobilerun configure anthropic
mobilerun configure gemini
mobilerun configure xai
```
#### `mobilerun configure openai`
Authenticate a ChatGPT/OpenAI OAuth profile. The command uses a browser callback on desktop and can use a device-code flow in a headless environment.
| Flag | Description | Default |
| --------------------------------- | ------------------------------------------- | ----------------------- |
| `--credential-path` | OAuth credential file | Default credential file |
| `--model` | Optional model override for later API calls | Provider default |
| `--timeout` | Seconds to wait for authorization | `300` |
| `--callback-host` | Local callback host | `localhost` |
| `--callback-port` | Local callback port | `1455` |
| `--callback-path` | Local callback path | `/auth/callback` |
| `--open-browser` / `--no-browser` | Open the authorization URL automatically | `--open-browser` |
#### `mobilerun configure anthropic`
Authenticate with Anthropic, or save an existing setup token without starting the browser flow.
```bash theme={null}
mobilerun configure anthropic
mobilerun configure anthropic --token
```
| Flag | Description | Default |
| --------------------------------- | -------------------------------------------- | ----------------------- |
| `--credential-path` | Credential file | Default credential file |
| `--token` | Setup token to save instead of running OAuth | None |
| `--timeout` | Seconds to wait for authorization | `300` |
| `--open-browser` / `--no-browser` | Open the authorization URL automatically | `--open-browser` |
#### `mobilerun configure gemini`
Authenticate a Gemini Code Assist OAuth profile.
| Flag | Description | Default |
| --------------------------------- | -------------------------------------------------- | ----------------------- |
| `--credential-path` | OAuth credential file | Default credential file |
| `--model` | Optional model override for later API calls | Provider default |
| `--timeout` | Seconds to wait for authorization | `300` |
| `--callback-host` | Local callback host | `127.0.0.1` |
| `--callback-port` | Local callback port; `0` selects one automatically | `0` |
| `--callback-path` | Local callback path | `/oauth2callback` |
| `--open-browser` / `--no-browser` | Open the authorization URL automatically | `--open-browser` |
#### `mobilerun configure xai`
Sign in to xAI with OAuth and save your login. Then run `mobilerun configure` and select xAI OAuth for your agent.
```bash theme={null}
mobilerun configure xai
# SSH or another headless machine
mobilerun configure xai --device-code --no-browser
```
For device-code login, open the printed URL, enter the code, and keep the code private.
| Flag | Description | Default |
| --------------------------------- | ---------------------------------------- | ----------------------- |
| `--credential-path` | OAuth credential file | Default credential file |
| `--model` | xAI model (`grok-4.6` or `grok-4.5`) | `grok-4.6` |
| `--timeout` | Seconds to wait for authorization | `300` |
| `--open-browser` / `--no-browser` | Open the authorization URL automatically | `--open-browser` |
| `--device-code` | Use device-code login | `false` |
When you pass `--credential-path`, use the same path in the corresponding LLM profile.
Use these commands to sign in to Mobilerun Cloud for Cloud device listing and direct actions. LLM provider sign-in uses `mobilerun configure`.
### `mobilerun login`
Run `mobilerun login`, open the verification link, enter the displayed code, and approve the sign-in. Mobilerun saves the session locally.
```bash theme={null}
mobilerun login
# Advanced overrides
mobilerun login \
--auth-url https://cloud.mobilerun.ai/api/auth \
--client-id mobilerun-cli
```
| Flag | Description | Default |
| ------------- | -------------------------------------------------------------------------- | ------------------------------------- |
| `--auth-url` | Mobilerun Cloud authentication URL; `MOBILERUN_AUTH_URL` also overrides it | `https://cloud.mobilerun.ai/api/auth` |
| `--client-id` | OAuth device client id | `mobilerun-cli` |
### `mobilerun whoami`
Show the email and user ID for the current Cloud session.
```bash theme={null}
mobilerun whoami
```
### `mobilerun logout`
Sign out and remove the saved Cloud credential.
```bash theme={null}
mobilerun logout
```
`mobilerun devices` and Cloud direct actions use `MOBILERUN_CLOUD_API_KEY` when it is set. Otherwise, they use the credential saved by `mobilerun login`. `mobilerun whoami` and `mobilerun logout` operate on the saved login. These commands do not use `MOBILERUN_API_KEY`.
### `mobilerun devices`
List local Android devices and, when a Cloud credential is available, Mobilerun Cloud devices.
```bash theme={null}
# Local devices, plus Cloud devices when a credential is available
mobilerun devices
# Only Mobilerun Cloud devices
mobilerun devices --cloud
```
| Flag | Description | Default |
| ------------ | ------------------------------------ | ----------------------------- |
| `--cloud` | Skip ADB and list only cloud devices | `false` |
| `--base-url` | Cloud API base URL | `https://api.mobilerun.ai/v1` |
Use `--cloud` to list only Cloud devices. It returns an error if authentication is missing or Cloud cannot be reached.
***
### `mobilerun setup`
Install the Portal APK on an Android device. Without a version flag, Mobilerun selects the Portal release compatible with the installed Mobilerun version.
```bash theme={null}
# Auto-detect device
mobilerun setup
# Specific device
mobilerun setup --device emulator-5554
# Custom APK
mobilerun setup --path /path/to/portal.apk
# Newest available Portal release
mobilerun setup --latest
# Pin a release
mobilerun setup --portal-version 0.7.1
```
| Flag | Description | Default |
| ------------------------- | ------------------------------------------------------------ | ------------------- |
| `--device`, `-d` | Android serial or IP | Auto-detect |
| `--path` | Install an APK from a local path | Compatible download |
| `--portal-version`, `-pv` | Download a specific Portal version | Compatible version |
| `--latest` | Download the latest Portal instead of the compatible version | `false` |
| `--debug` | Enable verbose setup logging | `false` |
**What it does:**
1. Downloads the compatible Portal APK
2. Installs it and grants the Android permissions allowed during installation
3. Attempts to enable the accessibility service
4. Opens settings if manual enable needed
To use Android through ADB without Portal, set `device.portal_mode: disabled`.
***
### `mobilerun ping`
Test Portal with `mobilerun ping`. For an ADB-only setup, use `adb devices -l`.
```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!`
| Flag | Description | Default |
| ------------------------ | --------------------------------- | ----------- |
| `--device`, `-d` | Android serial or IP | Auto-detect |
| `--tcp` / `--no-tcp` | Select Portal TCP communication | `false` |
| `--debug` / `--no-debug` | Enable verbose connection logging | `false` |
***
### `mobilerun doctor`
Run system and device diagnostics.
```bash theme={null}
mobilerun doctor
mobilerun doctor --device emulator-5554 --debug
```
| Flag | Description | Default |
| ------------------------ | -------------------------------- | ----------- |
| `--device`, `-d` | Device serial or IP | Auto-detect |
| `--debug` / `--no-debug` | Enable verbose diagnostic output | `false` |
***
### `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 direct-action subcommands accept these common flags:
| Flag | Description | Default |
| -------------------- | ------------------------------------------- | ---------------------------------------- |
| `--device`, `-d` | Local device serial/IP or Cloud device UUID | From config / auto-detect |
| `--config`, `-c` | Explicit Framework config file | `MOBILERUN_CONFIG`, then platform config |
| `--tcp` / `--no-tcp` | Override local Android TCP communication | From config |
| `--ios` | Target a local iOS device | `false` |
| `--cloud` | Target a Mobilerun Cloud device | `false` |
| `--device-id` | Cloud device id; `-d` also accepts it | None |
| `--base-url` | Cloud API base URL | `https://api.mobilerun.ai/v1` |
Action-specific options are `swipe --duration ` (default `1.0`), `type --clear`, and `apps --system` / `--no-system`.
`mobilerun device long-press` is not available for local iOS devices.
### Target a cloud device
Pass a Cloud device UUID with `--cloud --device-id`. When a Cloud credential is available, you can also pass the UUID with `-d`.
```bash theme={null}
# Use --cloud explicitly
mobilerun device screenshot \
--cloud \
--device-id 7f1c1d2e-4b8a-4a3a-9a4d-1234567890ab
# Use an available Cloud credential
mobilerun device tap 500 500 \
-d 7f1c1d2e-4b8a-4a3a-9a4d-1234567890ab
```
ADB serials target local devices.
Cloud direct actions support Android only.
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
```
Pass `--debug` to include load errors and other diagnostic output.
**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
# Continue with the agent if the replay no longer matches the screen
mobilerun macro replay trajectories/checkout \
--on-mismatch agent \
--config /path/to/config.yaml
```
**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` |
| `--debug` | Enable replay debug logging | `false` |
| `--on-mismatch` | `stop`, or `agent` to continue when the screen no longer matches the recording | `stop` |
| `--state-timeout` | Seconds to wait for the recorded screen state before each action | `5.0` |
| `--state-threshold` | Minimum similarity score (0–1) between the current and recorded UI state | `0.85` |
| `--config` | Config used by `--on-mismatch agent` | Default config |
| `--provider` | LLM provider used by `--on-mismatch agent` | From config |
| `--model` | LLM model used by `--on-mismatch agent` | From config |
With `--on-mismatch agent`, Mobilerun uses your default configuration unless you pass `--config` or both `--provider` and `--model`.
***
### 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. Explicit `--config` file
3. `MOBILERUN_CONFIG` environment variable
4. Platform config file (for example, `~/.config/droidrun/config.yaml` on Linux)
5. Built-in defaults (lowest)
### Common Patterns
```bash Quick Test theme={null}
mobilerun run "Turn on dark mode" \
--provider GoogleGenAI \
--model gemini-3.5-flash-lite
```
```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.5-flash-lite \
--no-vision
```
```bash Screenshot Only theme={null}
mobilerun run "Check Wi-Fi" \
--vision-only \
--control-backend visual-remote \
--device http://localhost:8090
```
```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 |
| `XAI_API_KEY` | xAI API key for Grok | None |
| `ANTHROPIC_API_KEY` | Anthropic API key | None |
| `DEEPSEEK_API_KEY` | DeepSeek API key | None |
| `OPENROUTER_API_KEY` | OpenRouter API key | None |
| `MINIMAX_API_KEY` | MiniMax API key | None |
| `ZAI_API_KEY` | ZAI API key | None |
| `MOBILERUN_CLOUD_API_KEY` | Cloud key for `devices --cloud` and cloud direct actions | None |
| `MOBILERUN_AUTH_URL` | Auth server used by `mobilerun login` | `https://cloud.mobilerun.ai/api/auth` |
| `MOBILERUN_CONFIG` | Override the Framework config path | Unset (platform config is used) |
| `MOBILERUN_DEVICE_TOKEN` | Bearer token for an authenticated `mobilerun-ios --local` server | None |
Cloud device commands use `MOBILERUN_CLOUD_API_KEY`, not `MOBILERUN_API_KEY`.
**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
Portal is recommended for local Android control; ADB-only control is also supported. Local iOS uses `mobilerun-ios` with WebDriverAgent.
## Prerequisites
Portal requires Android 8.0 (API 26) or newer. ADB-only setup requires ADB and UIAutomator.
**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 command:
* Downloads the compatible Portal APK
* Installs or upgrades the APK and grants the Android permissions allowed during installation
* Enables its accessibility service or opens Android Settings for approval
Open Portal once and grant **Display over other apps**. Grant notification permission, Notification Access, screen-capture consent, and **Install unknown apps** only when your tasks need those features.
To run without Portal, skip this step and set `device.portal_mode: disabled` in the framework configuration.
```bash theme={null}
mobilerun ping
# Output: Portal is installed and accessible. You're good to go!
```
`mobilerun ping` checks Portal. For ADB-only setup, use `adb devices -l` or `adb -s SERIAL get-state`.
***
## 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
* **Local Control** - HTTP, WebSocket, and ADB interfaces
* **Cloud Connection** - Optional remote control and screen streaming
* **Events and Triggers** - Optional device events and trigger rules
Keep **Cloud Connection** off for local Framework control. When enabled, Portal connects to the configured server and can send device state, actions, events, and screen-sharing data.
For advanced APIs, see the Portal documentation: [Local API](https://github.com/droidrun/mobilerun-portal/blob/main/docs/local-api.md), [Reverse Connection](https://github.com/droidrun/mobilerun-portal/blob/main/docs/reverse-connection.md), [WebSocket Events](https://github.com/droidrun/mobilerun-portal/blob/main/docs/websocket-events.md), and [Triggers and Events](https://github.com/droidrun/mobilerun-portal/blob/main/docs/triggers.md).
***
## Communication Modes
**How it works:**
* Tap, swipe, buttons, app lifecycle, packages, install/uninstall, screenshots, and date use ADB.
* UI state comes from UIAutomator.
* Text uses Android's ADB input command and is limited to printable ASCII; Portal is needed for reliable Unicode input.
**Framework configuration:**
```yaml theme={null}
device:
platform: android
portal_mode: disabled
```
`portal_mode: disabled` also bypasses the framework's automatic Portal setup.
**How it works:**
* Portal runs HTTP server on device port 8080
* ADB forwards local port → device port 8080
* Mobilerun sends authenticated HTTP requests to `localhost:PORT`
* In `auto` mode, Mobilerun uses the ContentProvider or ADB if HTTP is unavailable
**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
* Authentication is handled through ADB
**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 include: Row: 0 result=... and a successful JSON status
```
### Portal policy (`portal_mode`)
```python theme={null}
from mobilerun import DeviceConfig, MobileConfig
config = MobileConfig(
device=DeviceConfig(
portal_mode="auto", # auto | required | disabled
auto_setup=True,
use_tcp=False,
)
)
```
| Setting | Meaning |
| ---------- | ------------------------------------------------------------ |
| `auto` | Use Portal when available; otherwise use ADB. |
| `required` | Stop with an error if Portal or its keyboard is unavailable. |
| `disabled` | Use ADB only and skip Portal setup. |
`auto_setup=True` installs or repairs Portal before connecting. Set it to `False` to skip that step.
### Local HTTP and WebSocket authentication
Portal generates a bearer token for direct local HTTP and WebSocket clients. Copy it from the Portal main screen or retrieve it over ADB:
```bash theme={null}
adb shell content query --uri content://com.mobilerun.portal/auth_token
```
Mobilerun handles Portal authentication automatically. Direct WebSocket clients and HTTP calls other than `GET /ping` must use the token shown in Portal or retrieved through ADB.
***
## Advanced Setup
### Setup
1. **Settings** > **Developer options** > **Wireless debugging**
2. Note the IP address and debugging port shown on the main Wireless debugging screen (for example, `192.168.1.100:37757`)
**Pairing Code Method:**
1. Tap **Pair device with pairing code**
2. Note the pairing code and the pairing-specific IP:port shown in that dialog
3. Run `adb pair IP:PAIRING_PORT`
4. Enter pairing code
Android Studio can alternatively pair by scanning the device's QR code. The `adb pair` CLI expects an address/port and pairing code, not the QR payload.
```bash theme={null}
adb connect IP:PORT
adb -s IP:PORT get-state
# Optional: validate Portal as well
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
adb -s DEVICE_IP:5555 get-state
# Optional: validate Portal as well
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)
4. If Portal is intentionally not used, set `device.portal_mode: disabled` and validate ADB with `adb devices -l` instead of `mobilerun ping`
**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. When Portal is available, Mobilerun tries to enable the Portal keyboard when it connects:
```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
4. In ADB-only mode, use printable ASCII; Unicode, control characters, and literal `%s` are rejected
**Symptoms:** `get_ui_tree()` or the formatted agent state is empty or incomplete
**Solutions:**
1. With Portal enabled, verify its accessibility path with `mobilerun ping`
2. In ADB-only mode, inspect `adb shell uiautomator dump /dev/tty`
3. Some apps expose incomplete trees (WebViews, games, and custom-rendered UI)
4. Wait for the UI to settle after a tap or swipe, then request fresh state
5. With Portal enabled and overlay permission granted, use its overlay for visual debugging
This guide describes `mobilerun-ios --local` on ports `8080`–`8089`. See [iOS Driver](/framework/sdk/ios-tools) for the driver API.
***
## 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.
***
## How iOS Connects
The iOS local driver connects differently from Android:
| Feature | Android | iOS |
| ------------- | ----------------------------------------------- | ---------------------------------------- |
| Communication | ADB alone, or ADB + Portal HTTP/ContentProvider | Local HTTP API (port 8080 by default) |
| Device bridge | ADB; optional Portal APK | `mobilerun-ios` backed by WebDriverAgent |
| Setup tool | Optional `mobilerun setup` | `mobilerun-ios --local ` |
| Accessibility | Portal Accessibility API or ADB UIAutomator | WebDriverAgent / XCUITest |
| Text Input | Portal keyboard or printable-ASCII ADB input | Direct XCUITest text input |
| Connection | ADB over USB/TCP | USB through `mobilerun-ios` |
`mobilerun-ios` uses WebDriverAgent to read the screen, perform actions, manage apps, and take screenshots. The Framework connects to its local server automatically.
***
## 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_ui_tree()` | ✅ | Accessibility tree and device state |
| `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 on the Mac or a URL |
| `uninstall_app()` | ❌ | Not supported |
***
## 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. Authenticated servers are not auto-discovered. Set `MOBILERUN_DEVICE_TOKEN` and pass the printed URL with `--device`.
***
## 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
* Natural-language runs and LLM setup (`run`, `configure`)
* Cloud authentication (`login`, `whoami`, `logout`)
* Device setup and control (`devices`, `setup`, `connect`, `disconnect`, `ping`, `doctor`, `device`)
* Recorded automation replay (`macro`)
* Configuration overrides and flags
* Environment variables and API keys
* Common workflows and troubleshooting
**[Device Setup](./device-setup)** - Set up Android and iOS devices
* Install Portal or use Android through ADB
* Connect devices over USB or Wi-Fi
* Select and troubleshoot multiple devices
**[Configuration System](/framework/sdk/configuration)** - Configure agents, models, devices, and tools
* Use Python or YAML configuration
* Select LLM providers and models
* Customize 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.
***
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
```
Google Gemini, OpenAI, xAI, DeepSeek, MiniMax, ZAI, Ollama, and OpenRouter support is included by default. Anthropic requires the optional extra: use `uv tool install 'mobilerun[anthropic]'` for CLI-only use or `uv pip install 'mobilerun[anthropic]'` in a Python project.
### Set Up the Portal APK (Recommended)
Portal is the recommended way to control Android devices. It improves text entry and helps Mobilerun read the screen reliably. To use Android through ADB without Portal, see [Device Setup](/framework/guides/device-setup).
```bash theme={null}
mobilerun setup
```
This command automatically:
1. Downloads the Portal APK compatible with this Mobilerun version
2. Installs it on your connected device and grants the Android permissions allowed during installation
3. Attempts to enable the accessibility service, opening Settings if manual action is required
Use `mobilerun setup --latest` to opt into the newest Portal release, or `mobilerun setup --portal-version ` to pin a specific release.
### Test Connection
Verify that Portal is available:
```bash theme={null}
mobilerun ping
```
If successful, you'll see:
```
Portal is installed and accessible. You're good to go!
```
`mobilerun ping` checks Portal. For an ADB-only setup, run `adb devices -l` instead.
### Configure Your LLM
Run the configure wizard to choose your provider, auth method (API key or OAuth), and model:
```bash theme={null}
mobilerun configure
```
The wizard saves your provider and model settings. See the [CLI guide](/framework/guides/cli) for OAuth sign-in commands and configuration file locations.
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 xAI Grok
export XAI_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-5.5
# 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, XAI, Anthropic, etc.)
* `--model` - Model name (gemini-3.7-flash, gpt-5.5, etc.); pass it together with `--provider`
* `--vision` - Enable screenshot processing
* `--vision-only` - Use screenshots without an accessibility tree
* `--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 over ADB, with optional Mobilerun Portal enhancement.
## AndroidDriver
```python theme={null}
class AndroidDriver(DeviceDriver)
```
`AndroidDriver` controls Android devices through ADB. Portal is optional and adds Unicode text input, app labels, richer UI state, and overlay-aware screenshots.
Import the driver from `mobilerun-core-local`:
```python theme={null}
from mobilerun_core_local import AndroidDriver
```
### Constructor
```python theme={null}
driver = AndroidDriver(
serial=None,
use_tcp=False,
portal_mode="auto",
)
```
**Arguments:**
* `serial` — ADB serial, such as `emulator-5554`, a USB serial, or an ADB-over-network address.
* `use_tcp` — Use Portal HTTP through an ADB port forward when available.
* `portal_mode` — `"auto"`, `"required"`, or `"disabled"`.
### Portal modes
| Mode | Connection behavior |
| ---------- | ------------------------------------------------------------ |
| `auto` | Use Portal when available; otherwise use ADB. |
| `required` | Stop with an error if Portal or its keyboard is unavailable. |
| `disabled` | Use ADB only and skip Portal setup. |
An invalid mode raises `ValueError`.
In Framework configuration, `portal_mode="disabled"` also skips automatic Portal setup.
**Examples:**
```python theme={null}
# Use Portal when available
driver = AndroidDriver(serial="emulator-5554", portal_mode="auto")
# Use ADB only
driver = AndroidDriver(serial="emulator-5554", portal_mode="disabled")
# Require Portal and prefer its HTTP transport
driver = AndroidDriver(
serial="emulator-5554",
use_tcp=True,
portal_mode="required",
)
```
## Supported methods
```python theme={null}
AndroidDriver.supported = {
"tap",
"swipe",
"input_text",
"press_button",
"press_key_code",
"start_app",
"install_app",
"stop_app",
"uninstall_app",
"get_apps",
"list_packages",
"screenshot",
"get_ui_tree",
"get_date",
}
AndroidDriver.supported_buttons = {"back", "home", "enter"}
```
`drag()` is not supported. Use `swipe()` instead.
***
## Lifecycle
#### AndroidDriver.connect
```python theme={null}
async def connect() -> None
```
Select and verify an online ADB device. Unless Portal is disabled, the driver connects to Portal and configures its keyboard. In `auto` mode, connection continues through ADB if Portal is unavailable. In `required` mode, it raises an error.
#### AndroidDriver.ensure\_connected
```python theme={null}
async def ensure_connected() -> None
```
Connect once if necessary. It is safe to call repeatedly.
***
## Input actions
#### AndroidDriver.tap
```python theme={null}
async def tap(x: int, y: int) -> None
```
Tap at absolute Android display-pixel coordinates through ADB.
#### AndroidDriver.swipe
```python theme={null}
async def swipe(
x1: int,
y1: int,
x2: int,
y2: int,
duration_ms: float = 1000,
) -> None
```
Swipe between two absolute points over `duration_ms` milliseconds.
#### AndroidDriver.input\_text
```python theme={null}
async def input_text(text: str, clear: bool = False) -> bool
```
Type into the focused field and return `True` on success.
* When the Portal keyboard is available, text input supports Unicode.
* If Portal input fails in `auto` mode, the driver switches to ADB text input.
* ADB-only input supports printable ASCII. It rejects control/non-ASCII characters and the literal substring `%s`, which Android reserves as a space escape.
* `clear=True` clears the focused field first when possible.
#### AndroidDriver.press\_button
```python theme={null}
async def press_button(button: str) -> None
```
Press `back`, `home`, or `enter` (case-insensitive). An unknown name raises `ValueError`.
#### AndroidDriver.press\_key\_code
```python theme={null}
async def press_key_code(key_code: int) -> None
```
Send an Android integer key code through ADB.
#### AndroidDriver.drag
`drag()` raises `NotImplementedError`. Use `swipe()` instead.
***
## App management
#### AndroidDriver.start\_app
```python theme={null}
async def start_app(package: str, activity: str | None = None) -> str
```
Launch an Android package through ADB. If `activity` is omitted, Mobilerun opens the app's launcher activity. It raises `RuntimeError` when the app cannot be launched.
#### AndroidDriver.install\_app
```python theme={null}
async def install_app(path: str, **kwargs) -> str
```
Install a local APK through ADB. `reinstall=False` and `grant_permissions=True` are the defaults. A missing path raises `FileNotFoundError`.
`grant_permissions=True` grants eligible runtime permissions. Grant special access such as Accessibility, Display over other apps, Notification Access, Install unknown apps, and screen-capture consent on the device.
#### AndroidDriver.stop\_app
```python theme={null}
async def stop_app(package: str) -> str
```
Force-stop a package with `am force-stop`.
#### AndroidDriver.uninstall\_app
```python theme={null}
async def uninstall_app(package: str) -> str
```
Uninstall a package with `pm uninstall`.
#### AndroidDriver.list\_packages
```python theme={null}
async def list_packages(include_system: bool = False) -> list[str]
```
List package names through ADB. By default, only third-party packages are returned.
#### AndroidDriver.get\_apps
```python theme={null}
async def get_apps(include_system: bool = True) -> list[dict[str, str]]
```
Return `{"package": ..., "label": ...}` records. Portal supplies display labels when available. Without Portal, package names are used for both fields.
***
## State and observation
#### AndroidDriver.screenshot
```python theme={null}
async def screenshot(hide_overlay: bool = True) -> bytes
```
Return raw PNG bytes. Portal can hide its overlay when `hide_overlay=True`. In `auto` mode, the driver uses an ADB screenshot if Portal is unavailable.
#### AndroidDriver.get\_ui\_tree
```python theme={null}
async def get_ui_tree() -> dict[str, object]
```
Return the accessibility tree, phone state, and device context. ADB-only results may contain less detail than Portal results.
#### AndroidDriver.get\_date
```python theme={null}
async def get_date() -> str
```
Return the output of the device's ADB `date` command.
***
## Portal transport
`use_tcp=True` uses Portal HTTP over an ADB port forward. In `auto` mode, Mobilerun uses the ContentProvider or ADB if HTTP is unavailable. Authentication is handled automatically.
See the Portal [Local API](https://github.com/droidrun/mobilerun-portal/blob/main/docs/local-api.md) for direct API access.
***
## Example
```python theme={null}
import asyncio
from mobilerun_core_local import AndroidDriver
async def main() -> None:
driver = AndroidDriver(
serial="emulator-5554",
portal_mode="auto",
use_tcp=True,
)
await driver.connect()
await driver.start_app("com.android.settings")
await driver.tap(540, 300)
await driver.input_text("Mobilerun")
await driver.press_button("enter")
png = await driver.screenshot()
with open("screen.png", "wb") as output:
output.write(png)
asyncio.run(main())
```
For element-based actions, see [DeviceDriver and actions](/framework/sdk/base-tools).
# 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.
`DeviceDriver` is the base class for asynchronous device drivers in `mobilerun-core-local`. Use `supported` to check optional device operations. Most unavailable operations raise `NotImplementedError`; `input_coordinate_size()` is always available and defaults to the screenshot dimensions.
***
## Quick Reference
**Driver Methods:**
* `connect()`, `ensure_connected()`
* `tap()`, `swipe()`, `input_text()`, `press_button()`, `press_key_code()`, `drag()`
* `start_app()`, `stop_app()`, `install_app()`, `uninstall_app()`, `get_apps()`, `list_packages()`
* `screenshot()`, `input_coordinate_size()`, `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"}`).
***
## How It Works
`DeviceDriver` sends commands to the device. `StateProvider` converts device data into `UIState`. Action functions receive an `ActionContext` and return an `ActionResult`.
### Imports
Import asynchronous drivers from `mobilerun-core-local`:
```python theme={null}
from mobilerun_core_local import (
AndroidDriver,
AndroidPortalHttpDriver,
IOSPortalHttpDriver,
)
from mobilerun_core_local.driver import (
RecordingDriver,
StealthDriver,
VisualRemoteDriver,
)
from mobilerun_core_local.driver.cloud import CloudDriver
```
Install `mobilerun-core-local[cloud]` to use `CloudDriver`.
***
## 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 screen coordinates (pixels on Android; logical points on iOS)
* `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`.
* `press_key_code(key_code: int) -> None` - Press an integer key code.
### App Management
* `start_app(package: str, activity: str | None = None) -> str` - Launch app
* `stop_app(package: str) -> str` - Stop a running app
* `install_app(path: str, **kwargs) -> str` - Install app
* `uninstall_app(package: str) -> str` - Uninstall 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
* `input_coordinate_size(screenshot_width: int, screenshot_height: int) -> tuple[int, int]` - Return the dimensions used for input coordinates. On iOS, these can differ from screenshot dimensions.
* `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 it to support another platform. Its `supported` set lists available UI features, such as element lookup and coordinate conversion.
### AndroidStateProvider
```python theme={null}
class AndroidStateProvider(StateProvider)
```
Fetches and formats device state as a `UIState`. Set `stealth=True` to vary tap coordinates within element bounds.
***
## 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 describes the result.
***
## Action Functions
Action functions 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=None, clear=False)` - Optionally focus an indexed element, then input text (set `clear=True` to clear first)
* `type_text(text, clear=False)` - Input text into the focused field
* `type_secret(secret_id, index)` - Input a configured credential into an indexed element
* `swipe(coordinate, coordinate2, duration=1.0)` - Swipe gesture between two coordinate lists
* `system_button(button)` - Press system buttons (back, home, enter)
* `open_app(...)` - Open an Android app by name or an iOS or Visual Remote app by ID
* `wait(duration=1.0)` - Wait for a duration in seconds
* `complete(success, message)` - Mark task as finished
**Coordinate tools (`click_at`, `click_area`, `long_press_at`) are disabled by default.** Vision enables `click_at` when normalized coordinates are off. Screenshot-only mode enables all three. To enable all three with standard vision, set `disabled_tools: []` in `ToolsConfig`. See [Vision Mode](/framework/features/vision).
***
## 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
)
```
***
## Driver and Platform Comparison
| Driver | Platform / transport | Supported methods and limits |
| ------------------------- | ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `AndroidDriver` | Android over ADB; Portal optional | Tap, swipe, text, named buttons, integer key codes, app start/stop/install/uninstall, packages, screenshot, tree, and date. Drag is unavailable. |
| `AndroidPortalHttpDriver` | Android Portal HTTP; no ADB | Tap, swipe, text, buttons/key codes, app start/stop, packages, screenshot, and tree. A bearer token is required. Install, uninstall, date, and drag are unavailable. |
| `IOSPortalHttpDriver` | iOS through `mobilerun-ios --local` | Tap, coordinate swipe, text with clear, five named buttons, integer key codes, app start/stop/install, packages, screenshot, tree, and date. Uninstall and drag are unavailable. Input coordinates are logical iOS points. |
| `CloudDriver` | Mobilerun Cloud through the async SDK | Tap, swipe, text, Back/Home/Enter, app start, packages, screenshot, tree, and date. Drag, install, stop, uninstall, and key codes are unavailable. |
| `VisualRemoteDriver` | Screenshot/action HTTP server | The server reports its available actions and buttons when it connects. Screenshots are required; an accessibility tree is not available. |
`RecordingDriver` and `StealthDriver` support the same methods as the driver they wrap.
***
## 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
from mobilerun_core_local import 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:
**Unsupported operations:**
```python theme={null}
from mobilerun_core_local import PlatformUnsupportedError
try:
await driver.uninstall_app("com.example.app")
except PlatformUnsupportedError:
print("This platform does not support uninstall")
except NotImplementedError:
print("This driver does not implement uninstall")
```
`PlatformUnsupportedError` is a subclass of `NotImplementedError`. Check `driver.supported` before calling an optional method. `press_button()` raises `ValueError` for names outside `supported_buttons`. `CloudDriver` raises `DeviceDisconnectedError` for SDK connection, timeout, and conflict failures. Other connection and authentication failures may raise connection, HTTP, or permission errors.
**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
* [IOSPortalHttpDriver API](/framework/sdk/ios-tools) - iOS driver
* [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 IOSPortalHttpDriver) |
| `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
vision_only=False, # Use screenshots without an accessibility tree
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=None, # Use the built-in prompt for the selected mode
stateless=False, # Keep Manager conversation history
)
```
Leave `system_prompt` unset to use the built-in prompt for the selected Manager mode. To use a custom prompt, provide a file path or pass `manager_system` to `MobileAgent`. See [Prompt Templates](/framework/concepts/prompts).
**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, # Android ADB serial/IP or iOS/Visual Remote server URL
control_backend=None, # Use the platform default or "visual-remote"
device_id="auto", # Device to control when a server offers more than one
use_tcp=False, # Prefer Android Portal HTTP over ContentProvider
platform="android", # "android" or "ios"
portal_mode="auto", # "auto" | "required" | "disabled"
auto_setup=True, # Auto-install/fix Android Portal 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.
)
```
`portal_mode` controls Android Portal use:
* `auto` (default) uses Portal when available and uses ADB/UIAutomator if Portal cannot perform an action.
* `required` requires Portal and its keyboard. If Portal text input, app listing, screenshots, or UI reading fails, the operation fails instead of switching to ADB.
* `disabled` uses ADB/UIAutomator only.
`auto_setup=True` installs or repairs Portal before an Android run. It is skipped when `portal_mode="disabled"`.
***
### 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=None, # Use Mobilerun defaults; pass a list to choose disabled 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
```
By default, Mobilerun disables `click_at`, `click_area`, and `long_press_at`. With vision enabled, `click_at` becomes available unless normalized coordinates are used. Pass a list to choose which tools to disable, or `[]` to disable none.
Screenshot-only modes require all three coordinate tools and reject configurations that disable them. 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 mobilerun import MobileAgent, load_llm
llm = load_llm("GoogleGenAI", model="gemini-3.7-flash", temperature=0.2)
agent = MobileAgent(goal="...", llms=llm)
```
Create Google models with `mobilerun.load_llm()`, as shown above.
For xAI Grok, set `XAI_API_KEY` and use:
```python theme={null}
from mobilerun import load_llm
llm = load_llm("XAI", model="grok-4.6")
```
### Per-Agent LLMs
```python theme={null}
from mobilerun import MobileAgent, load_llm
from llama_index.llms.openai import OpenAI
agent = MobileAgent(
goal="...",
llms={
"manager": OpenAI(model="gpt-4o"), # Planning
"executor": load_llm("GoogleGenAI", model="gemini-3.7-flash"), # Action selection
"fast_agent": load_llm("GoogleGenAI", model="gemini-3.7-flash"), # Direct execution
"app_opener": OpenAI(model="gpt-4o-mini"), # App launching
"structured_output": load_llm("GoogleGenAI", model="gemini-3.7-flash"), # Output extraction
}
)
```
**LLM Keys:**
* `manager` - Planning (reasoning mode only)
* `executor` - Action selection (reasoning mode only)
* `fast_agent` - Direct execution
* `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}
from mobilerun import AgentConfig, MobileAgent, MobileConfig
custom_manager_prompt = """
You are an expert mobile agent.
Goal: {{ instruction }}
Be precise and efficient.
Return one result:
- ... while work remains.
- ... when complete.
- ... when blocked.
"""
agent = MobileAgent(
goal="...",
config=MobileConfig(agent=AgentConfig(reasoning=True)),
prompts={
"manager_system": custom_manager_prompt,
}
)
```
Each prompt key receives a different context. See [Prompt Templates](/framework/concepts/prompts#context-variables) for the exact variables available to Manager, Executor, and FastAgent templates.
***
## Complete Example
```python theme={null}
from mobilerun import (
MobileAgent, MobileConfig, load_llm,
AgentConfig, FastAgentConfig, DeviceConfig, LoggingConfig, TracingConfig
)
from llama_index.llms.openai import OpenAI
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": load_llm("GoogleGenAI", model="gemini-3.7-flash"), # Action selection
"fast_agent": load_llm("GoogleGenAI", model="gemini-3.7-flash"), # Direct execution
"app_opener": OpenAI(model="gpt-4o-mini"), # App launching
"structured_output": load_llm("GoogleGenAI", model="gemini-3.7-flash"), # 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
vision_only: false
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: null # Use the built-in prompt for the selected mode
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.7-flash
temperature: 0.2
kwargs:
max_tokens: 8192
executor:
provider: GoogleGenAI
model: gemini-3.7-flash
temperature: 0.1
kwargs:
max_tokens: 4096
fast_agent: # Direct execution
provider: GoogleGenAI
model: gemini-3.7-flash
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.7-flash
temperature: 0.0
device:
serial: null
control_backend: null
device_id: auto
use_tcp: false
platform: android
portal_mode: auto # "auto" | "required" | "disabled"
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: null # Use Mobilerun defaults
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.7-flash
# Override logging
mobilerun run "Task" --debug --save-trajectory action --tracing
# Custom config file
mobilerun run "Task" --config /path/to/config.yaml
```
**Run flags:**
* `--config PATH` - Custom config file
* `--device SERIAL` - Device serial/IP
* `--agent NAME` - Run an installed external Android agent
* `--provider PROVIDER` - LLM provider override
* `--model MODEL` - LLM model override; pass it together with `--provider`
* `--temperature FLOAT` - LLM temperature
* `--steps INT` - Max steps
* `--base_url URL` - API base URL (for Ollama/OpenRouter/MiniMax)
* `--api_base URL` - API base URL (for OpenAI-like)
* `--vision/--no-vision` - Enable/disable vision for all agents
* `--vision-only/--no-vision-only` - Use screenshots without an accessibility tree
* `--reasoning/--no-reasoning` - Enable/disable reasoning mode
* `--stream/--no-stream` - Enable/disable streamed console output
* `--tracing/--no-tracing` - Enable/disable tracing
* `--debug/--no-debug` - Enable/disable debug logs
* `--tcp/--no-tcp` - Enable/disable TCP communication
* `--control-backend visual-remote` - Connect to a Visual Remote server
* `--device-id ID` - Select one device when a server offers more than one
* `--save-trajectory none|step|action` - Trajectory saving level
* `--ios` - Run on iOS device
See the [CLI guide](/framework/guides/cli) for provider examples and option defaults.
***
## 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 XAI_API_KEY=your-key
export DEEPSEEK_API_KEY=your-key
export MINIMAX_API_KEY=your-key
export ZAI_API_KEY=your-key
export MOBILERUN_DEVICE_TOKEN=your-token # Optional authenticated iOS local server
export MOBILERUN_CONFIG=/path/to/config.yaml # Custom config path
```
### MiniMax
MiniMax defaults to `MiniMax-M3`. Use `https://api.minimax.io/v1` for global accounts or `https://api.minimaxi.com/v1` for Mainland China accounts:
```bash theme={null}
mobilerun configure \
--provider minimax \
--model MiniMax-M3 \
--base-url https://api.minimax.io/v1
```
If `MiniMax-M3` is unavailable in Mainland China, use `MiniMax-M2.7`. To use `MINIMAX_API_KEY` instead of a saved key, select the environment-key option in the wizard.
# 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 IOSPortalHttpDriver). 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.
# iOS Driver
Source: https://docs.mobilerun.ai/framework/sdk/ios-tools
`IOSPortalHttpDriver` connects to the local server started by `mobilerun-ios --local`. Import it from `mobilerun_core_local`.
***
## IOSPortalHttpDriver
```python theme={null}
class IOSPortalHttpDriver(PortalHttpDriver)
```
Start the local server before creating the driver:
```bash theme={null}
mobilerun-ios --local
```
The server uses WebDriverAgent. Taps and gestures use logical iOS points. Screenshots may use a different pixel size; `input_coordinate_size()` returns the correct input dimensions.
### Constructor
```python theme={null}
def __init__(
url: str,
token: str | None = None,
*,
timeout: float = 30.0,
) -> None
```
* `url` is the server base URL, normally `http://127.0.0.1:8080`.
* `token` is optional for a loopback server. It must match `--local-token` when the server requires authentication.
* A `401` or `403` during connection raises `PermissionError` with token guidance.
### Supported methods
```python theme={null}
IOSPortalHttpDriver.supported = {
"tap",
"swipe",
"input_text",
"press_button",
"press_key_code",
"start_app",
"stop_app",
"install_app",
"get_apps",
"list_packages",
"screenshot",
"get_ui_tree",
"get_date",
}
IOSPortalHttpDriver.supported_buttons = {
"home", "back", "enter", "delete", "app_switch"
}
```
`uninstall_app()` raises `PlatformUnsupportedError`. `drag()` raises `NotImplementedError`; use `swipe()` instead.
### Methods
| Method | Behavior |
| ------------------------------------ | --------------------------------------------------------------- |
| `connect()` | Verifies the Portal HTTP server through `GET /version`. |
| `tap(x, y)` | Taps at logical iOS point coordinates. |
| `swipe(x1, y1, x2, y2, duration_ms)` | Performs a coordinate-based swipe; duration is milliseconds. |
| `input_text(text, clear=False, ...)` | Types into the focused field and can clear it first. |
| `press_button(name)` | Presses `home`, `back`, `enter`, `delete`, or `app_switch`. |
| `press_key_code(code)` | Sends a supported integer key code to the local server. |
| `start_app(bundle_id)` | Launches by bundle identifier. |
| `stop_app(bundle_id)` | Stops an app by bundle identifier. |
| `install_app(path_or_url)` | Installs an app from a path on the Mac or a URL. |
| `get_apps()` / `list_packages()` | Lists installed apps, with optional system-app filtering. |
| `screenshot()` | Returns raw PNG bytes from `/screenshot`. |
| `get_ui_tree()` | Returns the accessibility tree and device state. |
| `get_date()` | Returns the device date and time as an ISO-formatted timestamp. |
### Direct usage
```python theme={null}
import asyncio
from mobilerun_core_local import IOSPortalHttpDriver
async def main() -> None:
driver = IOSPortalHttpDriver("http://127.0.0.1:8080")
await driver.connect()
await driver.start_app("com.apple.Preferences")
await driver.tap(200, 400)
await driver.input_text("hello", clear=True)
await driver.press_button("home")
asyncio.run(main())
```
***
## See Also
* [AndroidDriver](/framework/sdk/adb-tools)
* [DeviceDriver Base Class](/framework/sdk/base-tools)
* [Device Setup — iOS](/framework/guides/device-setup)
# 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 action functions
***
## 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 Cloud with the Mobilerun Portal app.
Use the **Mobilerun Portal** Android app to connect your own phone to Mobilerun Cloud. Once connected, it appears 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
* A phone running Android 8.0 (API 26) or newer
* A stable internet connection
File upload and download require Android 11 or newer. On the Portal main screen, tap **Enable Now** in the file-access banner and approve **All Files Access**.
***
## 1. Install Mobilerun Portal
Download the latest APK from the [Portal releases page](https://github.com/droidrun/mobilerun-portal/releases) and install it on the phone.
To build from source, clone the [Portal repository](https://github.com/droidrun/mobilerun-portal), run its Gradle build, and then install the generated APK:
```bash theme={null}
./gradlew assembleDebug
adb install -r /path/to/generated-portal.apk
```
If Google Play Protect blocks the install, first look for **More details** → **Install anyway**. Only change Play Protect settings if you understand and accept the risk, and restore them after installing the APK from the official Mobilerun repository.
***
## 2. Grant the required access
Open Portal once and complete its setup prompts:
1. Enable **Mobilerun Portal** under Android **Accessibility**. Remote UI state and most control actions require this service.
2. Grant **Display over other apps** when prompted. This lets Portal show its on-screen element overlay.
Then open Portal **Settings** and enable the features your tasks need:
* **Notification permission** — used for background screen-sharing prompts.
* **Notification Access** and event toggles — needed only for notification/device-event forwarding and notification-based triggers.
* **Auto-accept Screen Share** — helps accept the Android screen-sharing prompt; the system can still require manual consent.
* **Auto-accept App Installs** plus Android **Install unknown apps** for Portal — needed only when remote tasks install APKs.
* **Keep Screen Awake** — prevents sleep during unattended tasks.
Installing the APK can grant some permissions, but you must approve Accessibility, Display over other apps, Notification Access, Install unknown apps, and screen-capture consent on the phone.
***
## 3. Connect to Mobilerun Cloud
On the Portal main screen, choose a connection method:
1. **Sign in with Browser** — recommended; authenticate with the Mobilerun account that owns the Personal Phone subscription.
2. **Use API Key** — enter a Mobilerun API key directly.
3. **Custom Connection** — advanced option for a custom WebSocket host and credentials.
After connecting, you can leave the Portal screen; the phone stays connected in the background. Do not force-stop Portal unless you want to take the phone offline.
Cloud Connection can send device state, actions, events, and screen-sharing data to the configured server. To keep device control local, disable Cloud Connection and follow [Framework Device Setup](/framework/guides/device-setup).
For protocol and security details, see the Portal repository's [Reverse Connection](https://github.com/droidrun/mobilerun-portal/blob/main/docs/reverse-connection.md) documentation.
***
## Verify the connection
Open [Devices](https://cloud.mobilerun.ai/devices) in the Mobilerun dashboard. The phone should appear as a connected Personal Phone. Start a task from the Playground and select it to verify end-to-end control.
Because this is your physical phone, its installed apps, files, and signed-in state persist across tasks. Use a dedicated device or test profile when tasks should not access personal data.
***
## Disconnect
Press **Disconnect** on the Portal main screen to take the phone offline. **Sign Out** also removes the saved Mobilerun sign-in.
***
## Reset
Resetting a Personal Phone factory-resets the handset and clears the apps and data accumulated during sessions. The phone goes offline for several minutes while it wipes and reboots. During that time the device shows the `resetting` state. Mobilerun waits up to 5 minutes for the phone to reconnect. Once the phone is back online, the device returns to `ready`. If the phone does not reconnect within that window, Mobilerun terminates the device.
***
## Advanced Portal APIs
For local HTTP, WebSocket, and ADB APIs, see the Portal documentation. Direct WebSocket clients and HTTP calls other than `GET /ping` require a Portal token.
* [Local API](https://github.com/droidrun/mobilerun-portal/blob/main/docs/local-api.md)
* [WebSocket Events](https://github.com/droidrun/mobilerun-portal/blob/main/docs/websocket-events.md)
* [Triggers and Events](https://github.com/droidrun/mobilerun-portal/blob/main/docs/triggers.md)
***
## Need help?
* **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.
# Network inspection
Source: https://docs.mobilerun.ai/integrations/network-inspection
Stream decoded HTTP and WebSocket traffic from a supported Mobilerun device.
Mobilerun can inspect a device's network traffic and stream decoded HTTP/1.1, HTTP/2, HTTP/3, and application WebSocket events to your local machine. The stream is live-only and does not require adb or direct access to the device.
Unlike the [ADB and Frida tunnels](/integrations/adb), network inspection is session-based. You start a session with the REST API, connect to the returned WebSocket with either your API key or the device's existing stream token, and stop the session when you are finished.
## Requirements
Network inspection must be enabled for the device's hosting pool. Contact Mobilerun support at [contact@mobilerun.ai](mailto:contact@mobilerun.ai) if you need access.
* A Mobilerun [API key](/api-keys) (`dr_sk_...`)
* [`curl`](https://curl.se), [`jq`](https://jqlang.org), and [`websocat`](https://github.com/vi/websocat) installed locally
Set the API key and device ID in your shell:
```bash theme={null}
export MOBILERUN_API_KEY='dr_sk_...'
export DEVICE_ID='YOUR_DEVICE_ID'
export MOBILERUN_API='https://api.mobilerun.ai/v1'
```
Check whether the device supports network inspection:
```bash theme={null}
curl --fail-with-body -sS \
-H "Authorization: Bearer $MOBILERUN_API_KEY" \
"$MOBILERUN_API/devices/$DEVICE_ID/capabilities" \
| jq '.capabilities.trafficInspection'
```
The value must be `true`. The capability is granted when a device is placed in an enabled pool. Disabling inspection for that pool also prevents new sessions and stops active sessions.
## Endpoints
| Method | Endpoint | Purpose |
| -------- | ----------------------------------------------------- | ------------------------------------------------------------------------ |
| `POST` | `/v1/devices/{deviceId}/traffic/sessions` | Start a session and receive stream credentials. |
| `GET` | `/v1/devices/{deviceId}/traffic/sessions` | List recent session metadata. |
| `GET` | `/v1/devices/{deviceId}/traffic/sessions/{sessionId}` | Read status and return the device stream credentials for a live session. |
| `DELETE` | `/v1/devices/{deviceId}/traffic/sessions/{sessionId}` | Stop a session and restore device networking. |
| `WSS` | Returned as `stream.url` | Receive live decoded traffic events. |
Use your Mobilerun API key for the REST endpoints. Non-browser WebSocket clients also authenticate with that API key. Browser clients, which cannot set an `Authorization` header on `WebSocket`, use the existing device stream token returned in `stream.token` as a `token` query parameter. Always start with the returned `stream.url` instead of constructing the path yourself.
## Start an inspection session
Create a session with a unique `Idempotency-Key` so retrying the same request does not start another session:
```bash theme={null}
export REQUEST_ID="traffic-$(date +%s)-$$"
SESSION="$(
curl --fail-with-body -sS -X POST \
-H "Authorization: Bearer $MOBILERUN_API_KEY" \
-H "Idempotency-Key: $REQUEST_ID" \
-H 'Content-Type: application/json' \
-d '{}' \
"$MOBILERUN_API/devices/$DEVICE_ID/traffic/sessions"
)"
export SESSION_ID="$(jq -er '.id' <<<"$SESSION")"
jq '{id, state, expiresAt, retention}' <<<"$SESSION"
```
Only one session can be `starting`, `active`, or `stopping` for a device at a time. Starting another returns `409 TRAFFIC_ALREADY_ACTIVE`.
## Wait until the session is active
Starting inspection is asynchronous. Poll the status endpoint until the device producer is ready:
```bash theme={null}
while true; do
STATUS="$(
curl --fail-with-body -sS \
-H "Authorization: Bearer $MOBILERUN_API_KEY" \
"$MOBILERUN_API/devices/$DEVICE_ID/traffic/sessions/$SESSION_ID"
)"
STATE="$(jq -er '.state' <<<"$STATUS")"
[ "$STATE" = 'active' ] && break
if [ "$STATE" != 'starting' ]; then
jq . <<<"$STATUS"
exit 1
fi
sleep 1
done
export STREAM_URL="$(jq -er '.stream.url' <<<"$STATUS")"
export STREAM_PROTOCOL="$(jq -er '.stream.protocol' <<<"$STATUS")"
```
While the session is `starting` or `active`, the status response returns the device's existing stream token. This is the same device-bound credential used by other device WebSocket endpoints; it is not a traffic-specific or session-scoped viewer token. It is revoked when the device is terminated and rotates when device ownership changes. Treat it as a secret and do not log URLs containing it.
## Stream events with websocat
Open the returned WebSocket URL with your Mobilerun API key and the required subprotocol:
```bash theme={null}
websocat --no-async-stdio -B 8388608 \
-H="Authorization: Bearer $MOBILERUN_API_KEY" \
-H="Sec-WebSocket-Protocol: $STREAM_PROTOCOL" \
"$STREAM_URL" \
| jq --unbuffered -c .
```
Do not send `stream.token` as a Bearer token. For non-browser clients, the Bearer credential is your `dr_sk_...` API key.
The `websocat` options keep large JSON events intact when piping them into `jq`, including on macOS.
### Browser WebSocket clients
The browser `WebSocket` API cannot set an `Authorization` header. Add the returned device stream token to the returned URL instead:
```javascript theme={null}
const streamUrl = new URL(session.stream.url);
streamUrl.searchParams.set('token', session.stream.token);
const socket = new WebSocket(streamUrl, session.stream.protocol);
socket.onmessage = ({ data }) => console.log(JSON.parse(data));
```
The edge authenticates the query token against the device before forwarding the WebSocket upgrade. Devices API then verifies that the traffic session belongs to the same device owner and is still `starting` or `active`.
Exercise the app on the device while this command is running. Each line is a JSON event. The first event is `hello`; subsequent event types are:
| Type | Meaning |
| ------------------- | ---------------------------------------------------------------- |
| `ready` | The on-device traffic producer connected. |
| `flow` | A decoded HTTP exchange or an interception failure. |
| `websocket-message` | An application WebSocket message associated with a flow. |
| `gap` | Events were dropped because the bounded device queue overflowed. |
## Event schema
### Stream hello
The gateway sends a `hello` message immediately after the WebSocket opens. This message uses camelCase fields and is separate from the device event envelope:
```json theme={null}
{
"type": "hello",
"schemaVersion": 1,
"sessionId": "9a675d1d-f919-4454-9523-9715f1fc1057",
"deviceId": "3fe83936-fb09-49f1-8ec8-c1bdca194cf1",
"state": "active",
"retention": "none"
}
```
### Device event envelope
All subsequent device events use snake\_case fields:
| Field | Type | Required | Description |
| -------------- | -------------------- | -------- | ---------------------------------------------------------------------------------------- |
| `type` | String | Yes | `ready`, `flow`, `websocket-message`, or `gap`. |
| `session_id` | String | Yes | Traffic session UUID. |
| `timestamp_ms` | Non-negative integer | No | Unix timestamp in milliseconds. The current Android producer includes it on every event. |
The ingest boundary rejects undeclared or duplicate fields, malformed Base64, negative numeric values, and payloads that exceed the session body limit.
A `ready` event has no additional fields:
```json theme={null}
{
"type": "ready",
"session_id": "9a675d1d-f919-4454-9523-9715f1fc1057",
"timestamp_ms": 1787234404000
}
```
### Flow events
A `flow` event adds a `flow` object. Only `id` and `decryption_status` are always present; other fields can be omitted or `null` when an interception fails before that information is available.
```json theme={null}
{
"type": "flow",
"session_id": "9a675d1d-f919-4454-9523-9715f1fc1057",
"timestamp_ms": 1787234404123,
"flow": {
"id": "mitm-flow-id",
"decryption_status": "decrypted",
"host": "example.com",
"port": 443,
"sni": "example.com",
"scheme": "https",
"http_version": "HTTP/2",
"method": "GET",
"path": "/api/items",
"status_code": 200,
"request_headers": [["content-type", "application/json"]],
"response_headers": [["content-type", "application/json"]],
"request_body_base64": null,
"response_body_base64": "eyJvayI6dHJ1ZX0=",
"request_body_bytes": 0,
"response_body_bytes": 11,
"request_body_truncated": false,
"response_body_truncated": false,
"duration_ms": 84
}
}
```
| Field | Type | Required | Description |
| ------------------------- | ----------------------------- | -------- | ---------------------------------------------------------------------------------------------- |
| `id` | String | Yes | Unique flow identifier. |
| `decryption_status` | String | Yes | Current producer values are `decrypted` and `failed`. |
| `error` | String or null | No | Safe interception or transport error. It can be present even when a response was decoded. |
| `host` | String or null | No | HTTP request host. |
| `port` | Non-negative integer or null | No | Upstream server port. |
| `sni` | String or null | No | TLS Server Name Indication. |
| `scheme` | String or null | No | Request scheme, such as `http` or `https`. |
| `http_version` | String or null | No | Negotiated protocol reported by the app connection, such as `HTTP/1.1`, `HTTP/2`, or `HTTP/3`. |
| `method` | String or null | No | HTTP request method. |
| `path` | String or null | No | Request path including its query string. |
| `status_code` | Non-negative integer or null | No | HTTP response status code; absent or null if no response was received. |
| `request_headers` | Array of string pairs or null | No | Request headers as `[[name, value], ...]`. Repeated names remain separate pairs. |
| `response_headers` | Array of string pairs or null | No | Response headers in the same shape. |
| `request_body_base64` | Base64 string or null | No | Captured request body, limited by the session's `maxBodyBytes`. |
| `response_body_base64` | Base64 string or null | No | Captured response body, limited by the session's `maxBodyBytes`. |
| `request_body_bytes` | Non-negative integer or null | No | Original request-body size before truncation. |
| `response_body_bytes` | Non-negative integer or null | No | Original response-body size before truncation. |
| `request_body_truncated` | Boolean or null | No | `true` when the request body exceeded `maxBodyBytes`. |
| `response_body_truncated` | Boolean or null | No | `true` when the response body exceeded `maxBodyBytes`. |
| `duration_ms` | Non-negative integer or null | No | Time from request start to response completion in milliseconds. |
There is no combined `url` field. Construct one from `scheme`, `host`, optional `port`, and `path` if your parser needs it.
A null body field means that no body bytes were captured. Use the corresponding `*_body_bytes` and `*_body_truncated` fields to distinguish an empty body from one clipped by the session limit.
### WebSocket message events
Application WebSocket messages refer back to their HTTP upgrade flow by `flow_id`:
```json theme={null}
{
"type": "websocket-message",
"session_id": "9a675d1d-f919-4454-9523-9715f1fc1057",
"timestamp_ms": 1787234405123,
"message": {
"flow_id": "mitm-flow-id",
"direction": "client",
"opcode": "text",
"payload_base64": "aGVsbG8=",
"payload_bytes": 5,
"payload_truncated": false
}
}
```
| Field | Type | Required | Description |
| ------------------- | ---------------------------- | -------- | ---------------------------------------------------- |
| `flow_id` | String | Yes | ID of the associated HTTP upgrade flow. |
| `direction` | String | Yes | Current producer values are `client` and `server`. |
| `opcode` | String | Yes | Current producer values are `text` and `binary`. |
| `payload_base64` | Base64 string or null | No | Captured message payload, limited by `maxBodyBytes`. |
| `payload_bytes` | Non-negative integer or null | No | Original payload size before truncation. |
| `payload_truncated` | Boolean or null | No | `true` when the payload exceeded `maxBodyBytes`. |
### Gap events
A `gap` event reports that the device queue dropped one or more events:
```json theme={null}
{
"type": "gap",
"session_id": "9a675d1d-f919-4454-9523-9715f1fc1057",
"timestamp_ms": 1787234406123,
"dropped_events": 1
}
```
`dropped_events` is a required positive integer. The event does not identify which flows or messages were dropped.
For example, decode response bodies with `jq`:
```bash theme={null}
websocat --no-async-stdio -B 8388608 \
-H="Authorization: Bearer $MOBILERUN_API_KEY" \
-H="Sec-WebSocket-Protocol: $STREAM_PROTOCOL" \
"$STREAM_URL" \
| jq --unbuffered -r \
'select(.type == "flow" and .flow.response_body_base64) | .flow.response_body_base64 | @base64d'
```
Inspected headers and bodies can contain credentials, personal data, and other secrets. Mobilerun does not retain session events, but anything you print, pipe, or redirect locally may be stored on your machine.
## Interception behavior
Inspection is transparent at the device-networking layer. You do not need to configure an HTTP proxy in the app or Android settings. Starting a session temporarily installs the capture CA and reconfigures the device's existing TUN routing; stopping the session restores the previous routing and trust-store mounts. Android's global `http_proxy` setting is neither required nor modified.
Certificate pinning is not bypassed. A pinned app can reject the inspection certificate before an HTTP request is available, causing the connection to fail without a decoded flow. If the failure reaches the HTTP flow pipeline, the stream reports `decryption_status: "failed"` with an `error`, but clients should not assume every rejected TLS handshake produces an event. Keep using an authorized Frida pinning bypass, or equivalent instrumentation for a native or custom trust store, when inspecting pinned apps.
## Stop the session
Stop inspection when you are finished so the device can restore its normal routing and temporary trust changes:
```bash theme={null}
curl --fail-with-body -sS -X DELETE \
-H "Authorization: Bearer $MOBILERUN_API_KEY" \
"$MOBILERUN_API/devices/$DEVICE_ID/traffic/sessions/$SESSION_ID" \
| jq '{id, state}'
```
The request returns `202 Accepted` while cleanup runs. Repeating it is safe. Sessions also stop automatically when they expire.
## Troubleshooting
* **`TRAFFIC_NOT_SUPPORTED`** — The device image does not support network inspection.
* **`TRAFFIC_NOT_ENTITLED`** — `capabilities.trafficInspection` is `false`; create a device in an enabled pool or contact support.
* **`TRAFFIC_ALREADY_ACTIVE`** — Another session is starting, active, or stopping. Use the session ID in `details.sessionId`, or list sessions with `GET /devices/{deviceId}/traffic/sessions`.
* **`TRAFFIC_DEVICE_NOT_READY`** — Wait until the device reaches the `ready` state and retry with the same idempotency key.
* **`401 Unauthorized` on the WebSocket** — For CLI clients, send your `dr_sk_...` API key as `Authorization: Bearer ...`. For browsers, append the returned `stream.token` as the URL's `token` query parameter. Do not send `stream.token` as a Bearer token.
* **WebSocket subprotocol error** — Send `Sec-WebSocket-Protocol: mobilerun.traffic.v1` with either authentication method.
* **`Incoming message too long` or a `jq` parse error** — Increase `websocat`'s message buffer with `-B`; do not use `-S`, because strict mode drops oversized inspection events. Use `--no-async-stdio` when piping large messages on macOS.
* **The socket closed with `1013`** — Reconnect with the same authentication method. Read the session status again first if the device may have been transferred or replaced. Missed events are not replayed.
# 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
# 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.
# Mobilerun Assistant
Source: https://docs.mobilerun.ai/skills/assistant
Teach any AI agent to talk to the Mobilerun virtual assistant over chat — send tasks, stream replies, and handle human-in-the-loop question and approval cards.
The Mobilerun **Assistant skill** teaches an AI agent to hold a conversation with the Mobilerun virtual assistant (the VA) over the chat API. You talk to the VA the way you'd talk to a person: send it a task, read the streamed reply, and — this is the part that matters — respond when it pauses to ask a human a question or to get sign-off on a sensitive action (human-in-the-loop, or **HITL**).
This skill covers the chat endpoints under `/assistant/chat` and the `client.assistant.conversations.*` methods in the [TypeScript](https://www.npmjs.com/package/@mobilerun/sdk) and [Python](https://pypi.org/project/mobilerun-sdk/) SDKs.
This is the **chat** skill. To have an agent directly control a phone (tap, swipe, type), use the [OpenClaw · Android control](/skills/openclaw) skill instead.
## Install
Download the pre-packaged skill and load it into any compatible agent runtime:
Download the latest release from GitHub
The `.skill` file is a zip archive containing the `SKILL.md` knowledge pack. Refer to your agent runtime's documentation for how to load a `.skill` file. All the skill needs at runtime is a Mobilerun API key in `MOBILERUN_API_KEY` (`dr_sk_...` — create one on the [API Keys](/api-keys) page).
## How a conversation works
A conversation lives in a **session** (a persistent chat thread with a title). Each message you send starts a **turn**: the assistant streams back its reasoning, tool calls, and text until the turn settles.
`POST /assistant/chat/sessions` with a `title`. Returns a session `id`.
`POST /assistant/chat/message` with `{ sessionId, message }` and `Accept: text/event-stream`. Streaming is the only mode that works for long turns or HITL — the buffered JSON mode hard-times-out at 110s.
Handle text deltas and tool parts. The turn ends with a settle signal — `completed`, `error`, or an `aborted-*` reason.
If the assistant raises a question or approval card mid-turn, collect the user's decision and post it back (see below). The turn resumes once resolved.
## Human-in-the-loop (HITL)
Mid-turn, the assistant can pause and ask a human for input. This is the core of the skill — get it right and unattended integrations stay safe.
Stream part `tool-question`. The assistant is asking a clarifying question (e.g. "Which of these two restaurants did you mean?"). Answer with `POST /assistant/chat/question`, or dismiss it with `POST /assistant/chat/question/reject`.
Stream part `tool-hitl-approval`. The assistant wants to perform a sensitive action (a payment, a deletion) and needs a human sign-off first. Respond with `POST /assistant/chat/permission` and `once`, `always`, or `reject`.
Both cards move through the same state machine: `input-available` while the card is open, then `output-available` (resolved) or `output-error` once a human responds.
**Blocked is not broken.** When a card is `input-available`, the turn is paused waiting for a human — not failed. Keep the stream open, surface the card, collect an explicit decision, and post it back. Do **not** retry the send (that starts a second turn and returns `409`), do **not** inject an answer as a follow-up chat message, and do **not** auto-approve an approval card. Use `always` only when the user explicitly asks for durable approval; `reject` is the safe default when they decline.
### Answer a question
`POST /assistant/chat/question` with `{ questionId, answers }`. `answers` is an outer array aligned with the card's `input.questions`; each inner array is nonempty and holds `{label}`, `{custom}`, or `{label, custom}` selections. Include an `Idempotency-Key` header on retries.
```typescript theme={null}
await client.assistant.conversations.answerQuestion(
{ questionId, answers: [[{ label: "A" }]] },
{ headers: { "Idempotency-Key": key } },
);
```
### Answer an approval
`POST /assistant/chat/permission` with `{ permissionId, response }`, where `response` is exactly `once | always | reject`.
```typescript theme={null}
await client.assistant.conversations.answerPermission({
permissionId,
response: "once",
});
```
## Reconnecting and turn lifecycle
* **Reconnect after a drop:** `GET /assistant/chat/stream` replays the active turn from its start (a pending HITL card comes back on reconnect; `204` if nothing is running), then `GET /assistant/chat/messages` refetches full history — which also rehydrates any open card.
* **One turn per session:** sending while a turn is in flight returns `409`. Check `turnActive` from `GET /assistant/chat/messages` before sending.
* **Abort:** `POST /assistant/chat/abort` with `{ sessionId }` stops that session's in-flight turn (idempotent).
* **Stale session:** `404` (unknown/archived) or `410` `session_machine_replaced` (runtime recycled — start a fresh session). `402` means the account is out of credits.
## Reference
The downloadable `SKILL.md` carries the full endpoint table, curl / TypeScript / Python examples for every call, and the complete list of stream settle reasons and pitfalls. For request and response schemas, see the [TypeScript](https://www.npmjs.com/package/@mobilerun/sdk) and [Python](https://pypi.org/project/mobilerun-sdk/) SDK types.
# OpenClaw · Android Control
Source: https://docs.mobilerun.ai/skills/openclaw
Give any AI agent the ability to control a real Android phone through the Mobilerun skill.
The Mobilerun **Android-control skill** teaches an AI agent to drive a real Android phone: take screenshots, tap and swipe, type, manage apps, and run autonomous tasks — all without any manual API wiring. [OpenClaw](https://openclaw.ai) is the agent CLI that loads it, so you can 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 your device.
## What the 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 |
## What it 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.
## 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.
Not using OpenClaw? Download the pre-packaged skill and load it into any compatible runtime:
Download the latest release from GitHub
## 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
## 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.
## 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.
## 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.
## 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 Mobilerun skills are and how the agent uses them.
Talk to the virtual assistant over chat, with human-in-the-loop cards.
Manage your Mobilerun API keys.
Set up your personal Android device.
# Agent Skills
Source: https://docs.mobilerun.ai/skills/overview
Self-contained knowledge packs that teach any AI agent how to use Mobilerun — device control and the virtual assistant.
Mobilerun publishes **agent skills** — self-contained knowledge packs that tell an AI agent everything it needs to know to use a Mobilerun API. Once a skill is installed, the agent gains structured knowledge about the endpoints, authentication, error handling, and usage patterns of that surface, with no 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](https://openclaw.ai)), the agent reads these files at the start of a session and knows how to call the API correctly — including the parts that are easy to get wrong.
## Available skills
Give an agent full control of a real Android phone — screenshot, tap, swipe, type, manage apps, and run autonomous tasks on a device.
Talk to the Mobilerun virtual assistant over chat — send it a task, stream its reply, and handle the human-in-the-loop question and approval cards it raises.
## Requirements
Every skill needs the same two things:
* A Mobilerun account at [cloud.mobilerun.ai](https://cloud.mobilerun.ai)
* An API key from the [API Keys](/api-keys) page (`dr_sk_...`)
Individual skills may add their own requirements (the Android-control skill needs a connected device, for example) — each skill's page lists them.
## How to install a skill
You have two ways to install any Mobilerun skill:
### Via OpenClaw (recommended)
If you use the [OpenClaw](https://openclaw.ai) agent CLI, install a skill as a plugin — for example the Android-control skill:
```bash theme={null}
openclaw plugins install @mobilerun/openclaw-mobilerun
```
Each skill's page gives its exact install command and configuration steps.
### Direct download (other runtimes)
Every skill is also packaged as a `.skill` file — a zip archive containing the full set of reference documents — that you can load into any compatible agent runtime. Download links live on each skill's page:
Android device control
Virtual assistant chat
Refer to your agent runtime's documentation for how to load a `.skill` file.
## Source
The skills are 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 |