# Android Automation in Python: mas SDK Overview

> Android automation in Python with the mas package: how a MAS script launches, drives an emulator or cloud device, and where every function lives.

Source: https://automationmacro.com/docs/sdk (Python SDK, updated 2026-09-04)

The `mas` package is the Python SDK for Android automation in Macro Automation Studio (MAS). A macro is a plain Python script that imports `mas`, looks at the screen of an Android emulator or cloud device, and taps, types and swipes its way through an app. This page explains what the package is, how MAS launches a script, how a project is laid out, and where each function lives. It is for developers about to write a first macro or read one written by someone else. If you would rather describe the task than write the script, [MAS Agent](/agent) writes this code for you.

## The mas package: Android automation in pure Python

- **Pure Python, no dependencies.** The package is standard library only, version `0.1.0`, and needs Python 3.10 or newer.
- **It ships inside the app.** MAS keeps its own Python 3.13 environment and installs the SDK wheel into it. There is nothing to `pip install` for a project, and `import mas` works in the Code Editor from the first line. Leave `mas` out of `requirements.txt`.
- **It talks to the app, not to adb.** Every call is a JSON-RPC 2.0 request to the running app, which owns the device connection. The same script runs unchanged on an emulator over adb or on a cloud device.
- **The device is already bound.** MAS starts your script with a device attached to the session. There is no connect or disconnect call; the first `mas.*` call opens the connection.

## How a script is launched

When you click **Run** in the Code Editor, or start a macro from a device card, MAS runs the entry file as a module:

```bash
python -u -m src.app --max_items 20 --headless
```

The project root is the working directory and is on `PYTHONPATH`, so `src` is importable as a package. `-u` keeps output unbuffered, so `print` lines reach the run console as they happen. Values from an argument form arrive as command-line flags, one `--key value` pair per field, with a checkbox sent as a bare `--key` when it is on.

MAS sets these environment variables for the process:

| Variable | Value | Purpose |
|---|---|---|
| `MAS_RPC_PORT` | a local port | Where the app listens for JSON-RPC |
| `MAS_RPC_HOST` | `localhost` | The RPC host |
| `MAS_DEVICE_ID` | the device id | The device this run is bound to |
| `MAS_SESSION_TOKEN` | a token | Pairs the script's connection with the run |
| `PYTHONPATH` | the project root | Makes `src` importable |
| `PYTHONUNBUFFERED` | `1` | Unbuffered output |

On the first `mas.*` call the SDK connects over TCP, registers the session token, and from then on sends one request per call and waits for the reply. The connect timeout is 5 seconds and the read timeout is 30 seconds. In a cloud run the app sets `MAS_RPC_SOCKET` instead, and the SDK talks over a unix socket; the script does not change.

When the script exits, MAS records the exit code with the run. Zero is a success; anything else marks the run as failed, and a `macro.failed` webhook event fires if you have an endpoint subscribed. See [Errors](/docs/sdk/errors) for how to exit deliberately.

> [!NOTE]
> A script started from an outside terminal can still connect while the app is running. The SDK reads the port from a file the app writes (`macro-automation/mas_rpc_port` under `%APPDATA%` on Windows or `~/Library/Application Support` on Mac) and binds the device named in `MAS_DEVICE_ID`: `emulator-<port>`, an adb serial such as `127.0.0.1:<port>`, or `desktop`. The interpreter must be one that can import `mas`, and the account needs an active subscription.

## Project layout

A code project is a folder MAS creates for you. Only `src/app.py` is required.

| File | Who writes it | What it is |
|---|---|---|
| `src/app.py` | You | The entry point. Runs as the module `src.app` |
| `src/script_args.py` | The UI Builder | Generated from the argument form. Parses the command line at import time; do not hand-edit it |
| `ui.xml` | The UI Builder | The argument form the app renders before a run |
| `*.uibproj` | The UI Builder | The argument form design |
| `*.uibrt` | The UI Builder | The runtime dashboard design; the first match in the folder is used |
| `requirements.txt` | You | Extra packages your script needs. Never `mas` |

Template images are not files in the project. They live in your Image Library in the cloud, each with an integer ID, and you crop them in Asset Lab. The script names them in one `mas.images` call, which the publisher also reads to bundle the right images with a Marketplace listing.

Read form values through the generated module:

```python
from src.script_args import args

target = args.general.target_url    # tab "General", key "target_url"
limit = args.general.max_items      # Number Input widgets parse as int or float
headless = args.advanced.headless   # Checkbox widgets parse as bool
```

The [UI Builder](/docs/ui-builder) page covers the form designer.

## The import surface

Everything is exported flat on `mas`. Functions, types, constants and exceptions all live at the top level:

```python
import mas
from mas import Region, KeyCode, ColorConversion

mas.click(540, 960)
mas.key_press(KeyCode.BACK)
if mas.get_app_state("com.example.game") == mas.NOT_RUNNING:
    mas.open_app("com.example.game")
```

The one sub-namespace is `mas.ui`, which drives the runtime dashboard: `mas.ui.set_text`, `mas.ui.set_progress`, `mas.ui.batch` and so on. It is imported with `mas`; there is no separate import.

## Module map

| Page | What it covers | Functions |
|---|---|---|
| [Interaction](/docs/sdk/interaction) | Touch and keys | `click`, `swipe`, `input_text`, `key_press`, `zoom_in`, `zoom_out` |
| [Vision](/docs/sdk/vision) | Screenshots, template matching, OCR, image names | `take_screenshot`, `find_object`, `find_objects`, `find_any_object`, `find_object_retry`, `find_any_object_retry`, `read_text`, `wait_for_object`, `images` |
| [Elements](/docs/sdk/elements) | The accessibility tree | `find_element`, `find_elements`, `find_element_retry`, `read_element`, `scroll_to_element`, `read_page`, `dump_hierarchy`, `host_capabilities` |
| [Apps and device](/docs/sdk/apps-and-device) | Apps, device facts, logging, webhooks | `open_app`, `close_app`, `get_current_app`, `get_app_state`, `is_app_focused`, `get_screen_size`, `get_device_info`, `get_host_machine_id`, `log`, `get_clipboard`, `webhook` |
| [Storage](/docs/sdk/storage) | State between runs | `save`, `retrieve`, `retrieve_all`, `clear`, `get_current_device_port` |
| [Runtime UI](/docs/sdk/ui) | Live dashboards | `mas.ui.*` |
| [Errors](/docs/sdk/errors) | Exceptions and handling | `MASError`, `RPCError`, `ElementsUnavailable` and the coded subclasses |
| [API reference](/docs/api-reference) | Every signature on one page | All of the above, plus `RPCClient` and the types |

## A minimal Python Android emulator automation script

This script opens a game, waits for its home screen, taps Play, and counts the run in storage. It exits with `1` when a screen never appears, so the run shows as failed.

```python
import mas

images = mas.images({"play_button": 42, "home_screen": 43})


def main() -> int:
    info = mas.get_device_info()
    mas.log(f"Running on {info.name} ({info.screen_width}x{info.screen_height})")

    mas.open_app("com.example.game")
    home = mas.find_object_retry(images.home_screen, total_tries=5, time_sleep=2.0)
    if home is None:
        mas.log("Home screen did not appear", level="error")
        return 1

    play = mas.find_object_retry(images.play_button)
    if play is None:
        mas.log("Play button not found", level="warning")
        return 1
    mas.click(play.x, play.y, delay_ms=1000)

    state = mas.retrieve("daily_play")
    runs = state.get("runs", 0) + 1
    mas.save("daily_play", {"runs": runs})
    mas.log(f"Done, run number {runs}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
```

## Conventions

- **`find_object_retry` is the wait primitive.** It calls `find_object` up to `total_tries` times (default 3) with a flat `time_sleep` pause (default 2.0 seconds) and returns `None` when every try fails. Prefer it over `wait_for_object`, which is kept for existing macros, and over `continuous_mode`.
- **Name your images.** One `mas.images({...})` call near the top of `src/app.py` gives every Image Library ID a readable name and returns `ImageRef` objects that any vision call accepts. Raw integers still work, but the SDK prints a hint on exit listing the ones you forgot to declare.
- **`None` is a state, not an error.** Every `find_*` call returns `None` or an empty list on a miss and never raises for one. Branch on it with `if`.
- **Log with `mas.log`.** It writes a leveled line to the run console; `print` also shows there. Use `level="warning"` and `level="error"` so the log is scannable.
- **Exit on purpose.** Return a non-zero code from `main` when the macro cannot do its job, and let unexpected exceptions propagate. Both mark the run as failed. The patterns are on the [Errors](/docs/sdk/errors) page.
- **Keep the default pauses in mind.** `click` waits 1000 ms after the tap and `swipe` lasts 1000 ms by default; lower them in tight loops.
