Python SDK
MAS SDK Reference: Python Android Automation API
The mas SDK reference: every function, class, type and exception in the Python package with exact signatures and defaults for emulator and cloud device macros.
- Windows
- Mac
- Emulator
- Cloud device
- Phone
- Python SDK
On this page
- Namespace map of the mas SDK reference
- Types
- Coordinates
- Region
- ColorConversion
- SearchStrategy
- DeviceType
- DeviceInfo
- ScreenSize
- ObjectMatch
- Screenshot
- TextRecognitionResult
- KeyCode
- Modifier
- LogLevel
- ExecutionResult
- Interaction
- click
- swipe
- input_text
- key_press
- zoom_in
- zoom_out
- Vision
- take_screenshot
- find_object
- find_objects
- find_any_object
- find_object_retry
- find_any_object_retry
- read_text
- wait_for_object
- Image registry
- images
- ImageRef
- ImageMap
- Elements
- ElementMatch
- dump_hierarchy
- host_capabilities
- find_element
- find_elements
- find_element_retry
- read_element
- scroll_to_element
- read_page
- App
- open_app
- close_app
- get_current_app
- get_app_state
- is_app_focused
- Device
- get_screen_size
- get_device_info
- get_host_machine_id
- Storage
- save
- retrieve
- retrieve_all
- clear
- get_current_device_port
- Utilities and webhooks
- log
- get_clipboard
- webhook
- RPCClient
- get_client
- Runtime UI (mas.ui)
- UIEvent
- set_text
- set_progress
- add_data_point
- set_chart_data
- clear_chart
- append_text
- set_textarea
- clear_text
- set_table_data
- append_table_row
- clear_table
- cell_button
- set_cell_button_enabled
- wait_for_event
- get_input_value
- on_click
- on_change
- start_listener
- stop_listener
- batch
- Exceptions
- MASError
- RPCError
- ConnectionError
- ParseError
- InvalidRequestError
- MethodNotFoundError
- InvalidParamsError
- InternalError
- DeviceNotConnectedError
- DeviceNotFoundError
- CommandFailedError
- ImageNotFoundError
- TimeoutError
- AuthenticationError
- SubscriptionError
- ElementsUnavailable
This page is the mas SDK reference: every public name in the mas package that ships with Macro Automation Studio (MAS), with the signature as written in the source, each parameter with its default, what comes back, what can raise, and a short example. Read the SDK overview first if you have not written a macro yet. The namespace pages explain when to use each call; this page is the lookup table. If you describe the task to MAS Agent, it writes this code for you.
Every signature below is copied from the SDK source. mas.__version__ is "0.1.0".
Namespace map of the mas SDK reference
| Module | What it holds | Section |
|---|---|---|
types.py | Dataclasses, enums and aliases shared by every call: Region, ObjectMatch, KeyCode, ColorConversion and more | Types |
interaction.py | Touch and keyboard input: click, swipe, input_text, key_press, zoom_in, zoom_out | Interaction |
vision.py | Screenshots, template matching and OCR: take_screenshot, find_object, find_objects, find_any_object, find_object_retry, find_any_object_retry, read_text, wait_for_object | Vision |
registry.py | Readable names for Image Library IDs: images, ImageRef, ImageMap | Image registry |
elements.py | The accessibility tree: find_element, find_elements, find_element_retry, read_element, scroll_to_element, read_page, dump_hierarchy, host_capabilities, ElementMatch | Elements |
app.py | Launch and inspect apps: open_app, close_app, get_current_app, get_app_state, is_app_focused and the app state constants | App |
device.py | Device facts: get_screen_size, get_device_info, get_host_machine_id | Device |
storage.py | State that survives a run: save, retrieve, retrieve_all, clear, get_current_device_port | Storage |
utils.py, webhook.py, client.py | log, get_clipboard, webhook, RPCClient, get_client | Utilities and webhooks |
ui.py as mas.ui | Runtime dashboard updates and events: set_text, set_progress, charts, text areas, tables, wait_for_event, on_click, batch | Runtime UI |
exceptions.py | MASError, RPCError and its coded subclasses, ElementsUnavailable | Exceptions |
Everything is importable from mas directly, for example from mas import Region, KeyCode. Only the runtime UI lives in its own namespace, mas.ui.
Types
The types are plain dataclasses, enums and type aliases. Import them from mas.
Coordinates
Coordinates = tuple[int, int]An (x, y) pair in pixels, measured from the top-left corner of the screen. swipe, zoom_in and zoom_out take one; ObjectMatch.center and ElementMatch.center return one.
import mas
start: mas.Coordinates = (540, 1600)
end: mas.Coordinates = (540, 600)
mas.swipe(start, end, duration_ms=400)Region
class RegionA rectangle on screen given by two corners. find_object, find_objects and find_any_object take one as search_region; read_text takes one as region.
| Field | Type | Default | Meaning |
|---|---|---|---|
x1 | int | required | Left edge, pixels from the left |
y1 | int | required | Top edge, pixels from the top |
x2 | int | required | Right edge, pixels from the left |
y2 | int | required | Bottom edge, pixels from the top |
import mas
from mas import Region
score_box = Region(x1=800, y1=10, x2=1050, y2=60)
result = mas.read_text(region=score_box)ColorConversion
class ColorConversion(Enum)Preprocessing applied before OCR in read_text. The values match OpenCV’s color conversion codes; two are MAS additions. Members that share a value are aliases of each other.
| Member | Value | Meaning |
|---|---|---|
NONE | -1 | No conversion (the default) |
BLACK_WHITE | -2 | Grayscale plus an automatic threshold, so the image is pure black and white; the best choice for high-contrast text on noisy or colored backgrounds |
BGR_TO_BGRA, RGB_TO_RGBA | 0 | Add an alpha channel |
BGRA_TO_BGR, RGBA_TO_RGB | 1 | Drop the alpha channel |
BGR_TO_RGBA, RGB_TO_BGRA | 2 | Swap the channel order and add alpha |
RGBA_TO_BGR, BGRA_TO_RGB | 3 | Swap the channel order and drop alpha |
BGR_TO_RGB, RGB_TO_BGR | 4 | Swap the channel order |
BGRA_TO_RGBA, RGBA_TO_BGRA | 5 | Swap the channel order, keep alpha |
BGR_TO_GRAY | 6 | Grayscale from BGR |
RGB_TO_GRAY | 7 | Grayscale from RGB |
GRAY_TO_BGR, GRAY_TO_RGB | 8 | Expand grayscale to three channels |
GRAY_TO_BGRA, GRAY_TO_RGBA | 9 | Expand grayscale to four channels |
BGRA_TO_GRAY | 10 | Grayscale from BGRA |
RGBA_TO_GRAY | 11 | Grayscale from RGBA |
BGR_TO_HSV | 40 | HSV from BGR |
RGB_TO_HSV | 41 | HSV from RGB |
There is no GRAYSCALE or BINARY member. Use BGR_TO_GRAY for grayscale and BLACK_WHITE for a binary image.
import mas
from mas import ColorConversion, Region
word = mas.read_text(region=Region(100, 200, 400, 260), psm=8,
color_conversion=ColorConversion.BLACK_WHITE)SearchStrategy
SearchStrategy = Literal["first_match", "best_match", "priority_order"]How find_any_object chooses among several templates.
| Value | Behavior |
|---|---|
"first_match" | Return the first template that matches (the default and the fastest) |
"best_match" | Try every template and return the highest-confidence hit |
"priority_order" | Prefer templates that appear earlier in the list |
DeviceType
DeviceType = Literal["android", "desktop"]The kind of device bound to the run, as reported in DeviceInfo.type.
DeviceInfo
class DeviceInfoReturned by get_device_info.
| Field | Type | Default | Meaning |
|---|---|---|---|
id | str | required | Device identifier, for example emulator-5556 |
name | str | required | The display name of the device |
type | DeviceType | required | "android" or "desktop" |
screen_width | int | required | Screen width in pixels |
screen_height | int | required | Screen height in pixels |
connected | bool | required | Whether the device is reachable |
ScreenSize
class ScreenSizeReturned by get_screen_size.
| Field | Type | Default | Meaning |
|---|---|---|---|
width | int | required | Screen width in pixels |
height | int | required | Screen height in pixels |
ObjectMatch
class ObjectMatchThe result of a template match. x and y are the center of the matched area. There is no confidence, width or height field: the host applies threshold before it answers, so a returned match already passed it.
| Field | Type | Default | Meaning |
|---|---|---|---|
x | int | required | Center x of the match |
y | int | required | Center y of the match |
matched_template_id | int | None | None | The image ID that matched; set by find_any_object |
Properties: center returns (x, y) as Coordinates; template_id is an alias for matched_template_id.
import mas
match = mas.find_object(42)
if match:
mas.click(*match.center)
print(match.x, match.y, match.template_id)Screenshot
class ScreenshotReturned by take_screenshot. The image stays in memory as base64-encoded PNG.
| Field | Type | Default | Meaning |
|---|---|---|---|
base64 | str | required | Base64-encoded PNG data |
width | int | required | Width in pixels |
height | int | required | Height in pixels |
timestamp | str | required | Capture time in ISO 8601 format |
TextRecognitionResult
class TextRecognitionResultReturned by read_text.
| Field | Type | Default | Meaning |
|---|---|---|---|
text | str | required | The recognized text |
confidence | float | required | Recognition confidence from 0.0 to 1.0 |
region | Region | required | The area the text was read from |
KeyCode
class KeyCode(Enum)Android hardware key codes for key_press. Each member’s value is the Android key code string. There are 33 members; there is no ESCAPE.
| Member | Value |
|---|---|
BACK, HOME, MENU, APP_SWITCH | KEYCODE_BACK, KEYCODE_HOME, KEYCODE_MENU, KEYCODE_APP_SWITCH |
ENTER, DELETE, SPACE, TAB | KEYCODE_ENTER, KEYCODE_DEL, KEYCODE_SPACE, KEYCODE_TAB |
POWER, CAMERA | KEYCODE_POWER, KEYCODE_CAMERA |
VOLUME_UP, VOLUME_DOWN | KEYCODE_VOLUME_UP, KEYCODE_VOLUME_DOWN |
BRIGHTNESS_UP, BRIGHTNESS_DOWN | KEYCODE_BRIGHTNESS_UP, KEYCODE_BRIGHTNESS_DOWN |
DPAD_UP, DPAD_DOWN, DPAD_LEFT, DPAD_RIGHT, DPAD_CENTER | KEYCODE_DPAD_UP, KEYCODE_DPAD_DOWN, KEYCODE_DPAD_LEFT, KEYCODE_DPAD_RIGHT, KEYCODE_DPAD_CENTER |
MEDIA_PLAY, MEDIA_PAUSE, MEDIA_NEXT, MEDIA_PREVIOUS | KEYCODE_MEDIA_PLAY, KEYCODE_MEDIA_PAUSE, KEYCODE_MEDIA_NEXT, KEYCODE_MEDIA_PREVIOUS |
NUM_0 to NUM_9 | KEYCODE_0 to KEYCODE_9 |
import mas
from mas import KeyCode
mas.key_press(KeyCode.BACK)
mas.key_press(KeyCode.DELETE, repeat=10)Modifier
class Modifier(Enum)Modifier keys for key_press: SHIFT ("shift"), CTRL ("ctrl"), ALT ("alt"), META ("meta").
LogLevel
class LogLevel(Enum)Severity for log: DEBUG ("debug"), INFO ("info"), WARNING ("warning"), ERROR ("error"). log also accepts the plain strings.
ExecutionResult
class ExecutionResultA generic command result. It is exported for typing; no function on this page returns it.
| Field | Type | Default | Meaning |
|---|---|---|---|
success | bool | required | Whether the command succeeded |
message | str | None | required | A human-readable note |
data | dict | None | required | Extra data from the host |
duration_ms | int | required | How long the command took |
Interaction
Touch and keyboard input. Coordinates are pixels on the device screen. Each call blocks until the host confirms the command and then returns None. A failure on the device surfaces as an RPCError subclass (see Exceptions): DeviceNotConnectedError when no device is bound to the run, CommandFailedError when the device rejects the command.
click
def click(x: int, y: int, delay_ms: int = 1000) -> NoneTap once at (x, y), then pause for delay_ms so the app can react.
| Parameter | Type | Default | Meaning |
|---|---|---|---|
x | int | required | Horizontal position in pixels from the left edge |
y | int | required | Vertical position in pixels from the top edge |
delay_ms | int | 1000 | Pause after the tap, in milliseconds |
Returns: None.
Raises: an RPCError subclass when the host reports a failure.
import mas
mas.click(540, 1200) # tap, then wait 1000 ms
mas.click(540, 1200, delay_ms=0) # no pause, for tight loops
match = mas.find_object_retry(42)
if match:
mas.click(match.x, match.y, delay_ms=1500)swipe
def swipe(from_coords: Coordinates, to_coords: Coordinates, duration_ms: int = 1000) -> NoneDrag from one point to another. A short duration is a flick; a long one is a controlled drag.
| Parameter | Type | Default | Meaning |
|---|---|---|---|
from_coords | Coordinates | required | Starting (x, y) |
to_coords | Coordinates | required | Ending (x, y) |
duration_ms | int | 1000 | Gesture duration in milliseconds; shorter is faster |
Returns: None.
Raises: an RPCError subclass when the host reports a failure.
import mas
mas.swipe((500, 1500), (500, 500), duration_ms=400) # swipe up to scroll down
mas.swipe((100, 500), (900, 500), duration_ms=300) # swipe right
mas.swipe((200, 300), (800, 300), duration_ms=1000) # slow draginput_text
def input_text(text: str, delay_ms: int = 0, clear: bool = False) -> NoneType into the field that currently has focus. Tap the field first. With clear=True the cursor moves to the end of the field and the existing characters are deleted before typing, so the field does not need to be empty.
| Parameter | Type | Default | Meaning |
|---|---|---|---|
text | str | required | The text to type |
delay_ms | int | 0 | Pause after typing, in milliseconds |
clear | bool | False | Delete the field’s current contents first |
Returns: None.
Raises: an RPCError subclass when the host reports a failure.
import mas
mas.click(300, 450) # focus the field
mas.input_text("user@example.com")
mas.input_text("new value", clear=True) # replace whatever is there
mas.input_text("done", delay_ms=500)key_press
def key_press(key_code: KeyCode, modifiers: list[Modifier] | None = None, duration_ms: int = 100, repeat: int = 1) -> NonePress a hardware key, optionally with modifiers. A duration_ms of 500 or more sends a long press; smaller values send a normal press. The Android input bridge has no arbitrary hold, so the exact number of milliseconds is not honored beyond that distinction. All repeat presses go to the device in one command, which is much faster than a Python loop. Holding DELETE does not clear a field; use repeat to delete several characters, or input_text(..., clear=True).
| Parameter | Type | Default | Meaning |
|---|---|---|---|
key_code | KeyCode | required | The key to press |
modifiers | list[Modifier] | None | None | Modifier keys held during the press |
duration_ms | int | 100 | Hold time; 500 or more means a long press |
repeat | int | 1 | Number of presses, 1 to 100, sent as one command |
Returns: None.
Raises: an RPCError subclass when the host reports a failure.
import mas
from mas import KeyCode, Modifier
mas.key_press(KeyCode.BACK)
mas.key_press(KeyCode.ENTER)
mas.key_press(KeyCode.POWER, duration_ms=3000) # long press
mas.key_press(KeyCode.DELETE, repeat=10) # delete 10 characters
mas.key_press(KeyCode.TAB, modifiers=[Modifier.SHIFT])zoom_in
def zoom_in(center: Coordinates | None = None, percent: int = 50, duration_ms: int = 0, steps: int = 10) -> NoneA two-finger pinch that spreads the fingers apart. The gesture is synthesized through the emulator’s touch input device, which is detected automatically.
| Parameter | Type | Default | Meaning |
|---|---|---|---|
center | Coordinates | None | None | Center of the gesture; None means the center of the screen |
percent | int | 50 | Intensity from 1 to 100: how far the fingers spread, as a share of the largest pinch that fits around the center |
duration_ms | int | 0 | Total gesture time; 0 runs it as fast as the device executes it |
steps | int | 10 | Finger movement steps from 2 to 100; more steps make a smoother gesture |
Returns: None.
Raises: an RPCError subclass when the host reports a failure.
import mas
mas.zoom_in() # at the screen center
mas.zoom_in(center=(540, 960), percent=80) # strong zoom at a pointzoom_out
def zoom_out(center: Coordinates | None = None, percent: int = 50, duration_ms: int = 0, steps: int = 10) -> NoneA two-finger pinch that brings the fingers together. Same mechanics and parameters as zoom_in; percent is how far apart the fingers start.
| Parameter | Type | Default | Meaning |
|---|---|---|---|
center | Coordinates | None | None | Center of the gesture; None means the center of the screen |
percent | int | 50 | Intensity from 1 to 100: how far apart the fingers start |
duration_ms | int | 0 | Total gesture time; 0 runs it as fast as the device executes it |
steps | int | 10 | Finger movement steps from 2 to 100 |
Returns: None.
Raises: an RPCError subclass when the host reports a failure.
import mas
mas.zoom_out()
mas.zoom_out(center=(540, 960), percent=30) # gentle zoom out at a pointVision
A macro decides what to do by looking at the screen. Template images live in your Image Library in the cloud, each with an integer ID; you crop them in Asset Lab. Vision calls take that ID, or an ImageRef from images. Every find_* call returns None (or an empty list) on a miss and never raises for a miss. Take one screenshot and pass it to several searches when you need to check more than one thing on the same frame.
take_screenshot
def take_screenshot() -> ScreenshotCapture the device screen as an in-memory base64 PNG.
Returns: a Screenshot with base64, width, height and timestamp.
Raises: an RPCError subclass when the capture fails.
import mas
import base64
shot = mas.take_screenshot()
play = mas.find_object(42, screenshot=shot)
coins = mas.find_object(43, screenshot=shot)
with open("screen.png", "wb") as f:
f.write(base64.b64decode(shot.base64))find_object
def find_object(image_id: int | ImageRef, *, threshold: float = 0.8, screenshot: Screenshot | None = None, continuous_mode: bool = False, timeout_ms: int = 1000, capture_interval_ms: int = 500, max_matches: int = 1, search_region: Region | None = None) -> ObjectMatch | NoneFind one template on screen by template matching. All options after image_id are keyword-only.
| Parameter | Type | Default | Meaning |
|---|---|---|---|
image_id | int | ImageRef | required | Image Library ID, or an ImageRef from images |
threshold | float | 0.8 | Match confidence from 0.0 to 1.0; higher is stricter |
screenshot | Screenshot | None | None | Reuse a frame from take_screenshot instead of capturing |
continuous_mode | bool | False | Keep capturing and searching until found or timeout_ms elapses |
timeout_ms | int | 1000 | Time budget, used only in continuous mode |
capture_interval_ms | int | 500 | Pause between captures in continuous mode |
max_matches | int | 1 | Candidates the host looks for; find_object returns one match whatever the value |
search_region | Region | None | None | Restrict the search to a rectangle; faster and fewer false hits |
Returns: an ObjectMatch when a match reaches threshold, otherwise None.
Raises: ImageNotFoundError when the ID is not in your Image Library or cannot be loaded; TypeError when image_id is neither an int nor an ImageRef; other RPCError subclasses on host failures.
import mas
from mas import Region
images = mas.images({"login_btn": 123, "badge": 124})
match = mas.find_object(images.login_btn)
if match:
mas.click(*match.center)
top_left = mas.find_object(images.badge, search_region=Region(0, 0, 500, 400))find_objects
def find_objects(image_id: int | ImageRef, *, threshold: float = 0.8, screenshot: Screenshot | None = None, continuous_mode: bool = False, timeout_ms: int = 1000, capture_interval_ms: int = 500, max_matches: int = 10, search_region: Region | None = None) -> list[ObjectMatch]Find every occurrence of one template. The parameters match find_object; only the max_matches default differs.
| Parameter | Type | Default | Meaning |
|---|---|---|---|
image_id | int | ImageRef | required | Image Library ID, or an ImageRef |
threshold | float | 0.8 | Match confidence from 0.0 to 1.0 |
screenshot | Screenshot | None | None | Reuse a captured frame |
continuous_mode | bool | False | Keep searching until found or timeout |
timeout_ms | int | 1000 | Time budget in continuous mode |
capture_interval_ms | int | 500 | Pause between captures in continuous mode |
max_matches | int | 10 | Maximum number of matches to return |
search_region | Region | None | None | Restrict the search to a rectangle |
Returns: a list of ObjectMatch sorted by confidence, best first; an empty list when nothing matches.
Raises: ImageNotFoundError, TypeError, or another RPCError subclass, as for find_object.
import mas
images = mas.images({"collectible": 88})
for star in mas.find_objects(images.collectible, max_matches=20):
mas.click(*star.center, delay_ms=300)find_any_object
def find_any_object(image_ids: list[int | ImageRef], *, threshold: float = 0.8, screenshot: Screenshot | None = None, continuous_mode: bool = False, timeout_ms: int = 1000, capture_interval_ms: int = 500, max_matches: int = 1, search_strategy: SearchStrategy = "first_match", search_region: Region | None = None) -> ObjectMatch | NoneSearch for several templates at once. Use it when one control has variants: languages, themes or states. The returned match’s matched_template_id tells you which template hit.
| Parameter | Type | Default | Meaning |
|---|---|---|---|
image_ids | list[int | ImageRef] | required | Templates to look for |
threshold | float | 0.8 | Match confidence from 0.0 to 1.0 |
screenshot | Screenshot | None | None | Reuse a captured frame |
continuous_mode | bool | False | Keep searching until found or timeout |
timeout_ms | int | 1000 | Time budget in continuous mode |
capture_interval_ms | int | 500 | Pause between captures in continuous mode |
max_matches | int | 1 | Candidates per template |
search_strategy | SearchStrategy | "first_match" | "first_match", "best_match" or "priority_order" |
search_region | Region | None | None | Restrict the search to a rectangle |
Returns: an ObjectMatch with x, y and matched_template_id, or None.
Raises: ImageNotFoundError, TypeError, or another RPCError subclass.
import mas
images = mas.images({"claim_en": 123, "claim_fr": 124, "claim_es": 125})
match = mas.find_any_object([images.claim_en, images.claim_fr, images.claim_es],
search_strategy="best_match")
if match:
print(f"Matched image {match.template_id} at {match.center}")
mas.click(*match.center)find_object_retry
def find_object_retry(image_id: int | ImageRef, *, total_tries: int = 3, time_sleep: float = 2.0, **kwargs) -> ObjectMatch | NoneThe house wait primitive. Call find_object up to total_tries times, sleeping time_sleep seconds between failed attempts (never after the last one), and return the first match, or None when every attempt fails. Discrete tries with a flat pause read plainly, cost a bounded and predictable amount of time, and behave the same on every host. Every other keyword argument (threshold, screenshot, max_matches, search_region and so on) is forwarded to find_object by name. A caller-supplied screenshot is a frozen frame, so the call collapses to one attempt.
| Parameter | Type | Default | Meaning |
|---|---|---|---|
image_id | int | ImageRef | required | Image Library ID, or an ImageRef |
total_tries | int | 3 | Attempts before giving up; values below 1 count as 1 |
time_sleep | float | 2.0 | Seconds to sleep between failed attempts |
**kwargs | keyword arguments | none | Forwarded to find_object unchanged |
Returns: the first ObjectMatch, or None.
Raises: whatever find_object raises.
import mas
images = mas.images({"play": 784})
button = mas.find_object_retry(images.play, total_tries=5, time_sleep=1.5, threshold=0.85)
if button is None:
mas.log("Play button not found, will try again later", level="warning")
else:
mas.click(button.x, button.y, delay_ms=1000)find_any_object_retry
def find_any_object_retry(image_ids: list[int | ImageRef], *, total_tries: int = 3, time_sleep: float = 2.0, **kwargs) -> ObjectMatch | Nonefind_object_retry over a list of templates. Identical semantics: up to total_tries attempts, a flat time_sleep pause between failed ones, every other keyword forwarded to find_any_object by name, and a caller-supplied screenshot collapsing to one attempt. Use it where several variants of one control are acceptable.
| Parameter | Type | Default | Meaning |
|---|---|---|---|
image_ids | list[int | ImageRef] | required | Templates to look for |
total_tries | int | 3 | Attempts before giving up |
time_sleep | float | 2.0 | Seconds to sleep between failed attempts |
**kwargs | keyword arguments | none | Forwarded to find_any_object unchanged |
Returns: the first ObjectMatch, or None.
Raises: whatever find_any_object raises.
import mas
images = mas.images({"ok_light": 201, "ok_dark": 202})
ok = mas.find_any_object_retry([images.ok_light, images.ok_dark], total_tries=4)
if ok:
mas.click(*ok.center)read_text
def read_text(region: Region | None = None, screenshot: Screenshot | None = None, model: str = "eng_best", psm: int = 7, color_conversion: ColorConversion = ColorConversion.NONE, timeout_ms: int = 30000) -> TextRecognitionResultRead text off the screen with OCR (Tesseract). Always pass a region when you can: OCR on a tight box is faster and far more accurate than reading the whole screen. psm tells the engine the shape of the text.
| Parameter | Type | Default | Meaning |
|---|---|---|---|
region | Region | None | None | Area to read; None reads the whole screen |
screenshot | Screenshot | None | None | Reuse a captured frame |
model | str | "eng_best" | OCR model or language: "eng_best", "eng_fast" or "eng" |
psm | int | 7 | Page segmentation mode 0 to 13: 6 uniform block, 7 single line, 8 single word, 11 sparse text |
color_conversion | ColorConversion | ColorConversion.NONE | Preprocessing before OCR; BLACK_WHITE for text on noisy backgrounds, BGR_TO_GRAY for plain grayscale |
timeout_ms | int | 30000 | OCR time budget in milliseconds |
Returns: a TextRecognitionResult with text, confidence and region.
Raises: an RPCError subclass when OCR fails or the host times out.
import mas
from mas import ColorConversion, Region
result = mas.read_text(region=Region(420, 90, 660, 140))
score = int(result.text) if result.text.strip().isdigit() else 0
word = mas.read_text(region=Region(100, 200, 400, 260), psm=8,
color_conversion=ColorConversion.BLACK_WHITE)
print(word.text, word.confidence)wait_for_object
def wait_for_object(image_id: int | ImageRef, timeout_ms: int = 10000, threshold: float = 0.8) -> ObjectMatch | NoneWait on the host until a template appears or timeout_ms elapses. This call is kept for existing macros. Prefer find_object_retry: discrete tries with a flat pause read plainly and behave identically on every host.
| Parameter | Type | Default | Meaning |
|---|---|---|---|
image_id | int | ImageRef | required | Image Library ID, or an ImageRef |
timeout_ms | int | 10000 | Maximum wait in milliseconds |
threshold | float | 0.8 | Match confidence from 0.0 to 1.0 |
Returns: an ObjectMatch when the template appears in time, otherwise None.
Raises: ImageNotFoundError, TypeError, or another RPCError subclass.
import mas
images = mas.images({"home_screen": 456})
mas.click(150, 300)
home = mas.wait_for_object(images.home_screen, timeout_ms=30000)
if home is None:
raise RuntimeError("Home screen did not load")Image registry
Template images do not live in your project folder. They live in your Image Library in the cloud, where each image has a numeric ID. You could pass raw integers to every vision call, but that scatters meaningless numbers through the code. Declare the IDs once with images and refer to them by name. The publisher reads that call to know which images to bundle with the macro, so keep every image in one images call near the top of src/app.py. When a script uses images and still passes a raw integer somewhere, the SDK prints a hint on exit listing the IDs to add.
images
def images(mapping: dict[str, int]) -> ImageMapDeclare the image IDs your script uses under readable names.
| Parameter | Type | Default | Meaning |
|---|---|---|---|
mapping | dict[str, int] | required | Name to Image Library ID; names must be valid Python identifiers |
Returns: an ImageMap with one ImageRef per entry, reached by dot access.
Raises: TypeError when a name is not a string or an ID is not an integer; ValueError when a name is not a valid identifier.
import mas
images = mas.images({
"login_btn": 123,
"home_screen": 456,
"daily_reward": 789,
})
match = mas.find_object(images.login_btn)ImageRef
class ImageRefA reference to one registered image. images creates them; you do not construct one by hand. Any vision call accepts an ImageRef in place of an integer. It compares equal to another ImageRef with the same ID and to the plain integer, and int(ref) returns the ID.
| Field | Type | Default | Meaning |
|---|---|---|---|
id | int | required | The Image Library ID |
name | str | required | The name given in images |
import mas
images = mas.images({"login_btn": 123})
ref = images.login_btn
print(ref) # ImageRef('login_btn', id=123)
print(int(ref) == 123, ref == 123)ImageMap
class ImageMapThe read-only container returned by images. Each key of the mapping becomes an attribute holding an ImageRef. Assigning a new attribute raises AttributeError; define every image in the images call instead.
| Method | Type | Default | Meaning |
|---|---|---|---|
get_all_ids() | list[int] | none | Every registered ID, in declaration order |
import mas
images = mas.images({"login_btn": 123, "home": 456})
print(images.get_all_ids()) # [123, 456]
print(images) # ImageMap(login_btn=123, home=456)Elements
The accessibility tree is the second way to see the screen. Where vision matches pixels, elements read the widget tree the app exposes to Android: resource IDs, text, content descriptions and bounds. The device side is one wire method, a hierarchy dump; matching, ranking and scrolling run inside the SDK, so every host that can dump the tree serves the whole API.
A selector is a plain dict. Every key is optional, and the same dict shape is what MAS Agent harvests as <NAME>_SELECTOR constants.
{"rid": "com.app:id/claim", "text": "Claim", "desc": "Claim reward",
"cls": "Button", "index": 0, "clickable": True}| Key | Meaning |
|---|---|
rid | Resource ID. Matches the full ID or its trailing id/... part. Outranks every other key |
desc | Content description, case-insensitive; an exact match ranks above a contains match |
text | Visible text, case-insensitive; exact above contains |
cls | Class name, exact or suffix match. When rid already matched, a differing class does not veto the node |
index | Which of the equally ranked matches to pick, from 0 |
clickable | When True, only clickable nodes qualify |
Ranking is rid over desc over text over cls. The match_count field on a result says how many nodes matched at all: 1 is unambiguous, more means the pick was by rank and index.
The tri-state rule: a tree that cannot be read is never the same as an element that is absent. A game drawing into a canvas, a secure screen, a desktop host, a dump that failed or an RPC timeout all raise ElementsUnavailable. Catch it and fall back to vision or retry; never treat it as “already done”. A readable tree with no match returns None.
The host answers from one of two channels. The rich dump carries text and content descriptions but needs the screen to hold still. The structural dump is fast and works while the screen animates, but it carries no text or descriptions. The SDK tells the host whether the selector needs text. When only the structural tree could be read and the selector offers nothing but text or desc, the call raises ElementsUnavailable with a message that says so; anchor such selectors on a resource ID, or use a vision asset for that screen.
ElementMatch
class ElementMatchOne matched element. x and y are the center, which is the tap target.
| Field | Type | Default | Meaning |
|---|---|---|---|
x | int | required | Center x |
y | int | required | Center y |
text | str | "" | The element’s text |
desc | str | "" | The content description |
rid | str | "" | The resource ID |
cls | str | "" | The class name |
bounds | tuple[int, int, int, int] | (0, 0, 0, 0) | (x1, y1, x2, y2) on screen |
checked | bool | False | Checked state |
enabled | bool | True | Enabled state |
clickable | bool | False | Whether the node is clickable |
scrollable | bool | False | Whether the node scrolls |
match_count | int | 1 | How many nodes matched the selector |
The center property returns (x, y) as Coordinates, mirroring ObjectMatch.
import mas
btn = mas.find_element({"text": "Start"})
if btn:
print(btn.rid, btn.bounds, btn.match_count)
mas.click(*btn.center)dump_hierarchy
def dump_hierarchy() -> strReturn the current screen’s accessibility tree as uiautomator XML. Useful while authoring, to see which resource IDs and texts exist.
Returns: the XML as a string; empty when the host returned nothing.
Raises: ElementsUnavailable when this host cannot provide a dump.
import mas
xml = mas.dump_hierarchy()
print(xml[:500])host_capabilities
def host_capabilities() -> list[str]The RPC methods the connected host dispatches. Use it to prove a host’s surface instead of assuming it: a method one host wires and another lacks fails mid-run with MethodNotFoundError.
Returns: a list of method names; an empty list on older hosts that do not answer.
Raises: nothing; MethodNotFoundError is swallowed and yields [].
import mas
methods = mas.host_capabilities()
if "dump_hierarchy" in methods:
btn = mas.find_element({"rid": "com.app:id/start"})find_element
def find_element(selector: dict) -> ElementMatch | NoneFind one element on the current screen by selector and return the best-ranked match.
| Parameter | Type | Default | Meaning |
|---|---|---|---|
selector | dict | required | Keys rid, text, desc, cls, index, clickable as described above |
Returns: an ElementMatch, or None when the tree is readable but nothing matches.
Raises: ElementsUnavailable when the tree cannot describe this screen.
import mas
try:
btn = mas.find_element({"rid": "com.app:id/start"})
except mas.ElementsUnavailable:
btn = None
if btn:
mas.click(*btn.center)find_elements
def find_elements(selector: dict, *, max_matches: int = 10) -> list[ElementMatch]Every element matching the selector, best-ranked first.
| Parameter | Type | Default | Meaning |
|---|---|---|---|
selector | dict | required | The selector dict |
max_matches | int | 10 | Maximum results; values below 1 count as 1 |
Returns: a list of ElementMatch; empty when nothing matches.
Raises: ElementsUnavailable when the tree cannot describe this screen.
import mas
rows = mas.find_elements({"cls": "TextView", "clickable": True}, max_matches=5)
for row in rows:
print(row.text, row.center)find_element_retry
def find_element_retry(selector: dict, *, total_tries: int = 3, time_sleep: float = 2.0) -> ElementMatch | Nonefind_element with discrete retries, mirroring find_object_retry. A transiently unreadable tree counts as a failed attempt; ElementsUnavailable propagates only when the final attempt still cannot read the tree. One flaky dump never fails a run, and a treeless screen still refuses to look like “element absent”.
| Parameter | Type | Default | Meaning |
|---|---|---|---|
selector | dict | required | The selector dict |
total_tries | int | 3 | Attempts before giving up |
time_sleep | float | 2.0 | Seconds between failed attempts |
Returns: the first ElementMatch, or None.
Raises: ElementsUnavailable when the last attempt could not read the tree.
import mas
claim = mas.find_element_retry({"rid": "com.app:id/claim"}, total_tries=5, time_sleep=1.0)
if claim:
mas.click(*claim.center)read_element
def read_element(selector: dict) -> strThe text of the best-matching element: text first, then the content description. This is an exact string from the UI toolkit, where OCR would approximate.
| Parameter | Type | Default | Meaning |
|---|---|---|---|
selector | dict | required | The selector dict |
Returns: the string, or "" when nothing matches.
Raises: ElementsUnavailable when the tree cannot describe this screen.
import mas
balance = mas.read_element({"rid": "com.app:id/gold_balance"})
if balance.replace(",", "").isdigit():
print(int(balance.replace(",", "")))scroll_to_element
def scroll_to_element(selector: dict, *, container: dict | None = None, max_swipes: int = 8) -> ElementMatch | NoneScroll until the selector matches or the swipes run out. Each step swipes inside the container’s bounds (or the biggest scrollable element, or the visible extent of the tree) from 75 percent of its height to 25 percent, then waits for the fling to settle before the next dump.
| Parameter | Type | Default | Meaning |
|---|---|---|---|
selector | dict | required | The element to reach |
container | dict | None | None | Selector for the scrolling container; None picks the biggest scrollable element |
max_swipes | int | 8 | Swipes to spend; the screen is checked once more after the last swipe |
Returns: the ElementMatch once visible, or None.
Raises: ElementsUnavailable when the tree cannot describe this screen.
import mas
row = mas.scroll_to_element({"text": "Advanced settings"}, max_swipes=6)
if row:
mas.click(*row.center)read_page
def read_page(container: dict | None = None, *, fields: list[str] | None = None, max_swipes: int = 8, row_cap: int = 200) -> list[dict]Scroll through a list screen and collect its repeated rows. The call finds the container (by selector, or the biggest scrollable element), detects the repeated row structure, scrolls while collecting, and returns the rows deduplicated in reading order. It stops early when a full swipe surfaces nothing new.
| Parameter | Type | Default | Meaning |
|---|---|---|---|
container | dict | None | None | Selector for the list container; None picks the biggest scrollable element |
fields | list[str] | None | None | Cell keys to keep out of text, desc, rid, center; text is always kept |
max_swipes | int | 8 | Scroll budget, clamped to 0 to 20 |
row_cap | int | 200 | Stop after this many rows, clamped to 1 to 500 |
Returns: a list of {"cells": [...]} dicts. Each cell is {"text", "desc", "rid", "center"} for one content-bearing element of the row, in document order.
Raises: ElementsUnavailable when the tree cannot describe this screen.
import mas
rows = mas.read_page(fields=["text", "center"], max_swipes=10)
for row in rows:
print([cell["text"] for cell in row["cells"]])App
Launch, close and inspect apps by package name. Package names look like com.android.settings.
open_app
def open_app(package_name: str, timeout_ms: int = 2000) -> NoneLaunch an app by package name.
| Parameter | Type | Default | Meaning |
|---|---|---|---|
package_name | str | required | The Android package identifier |
timeout_ms | int | 2000 | How long to wait for the launch, in milliseconds |
Returns: None.
Raises: an RPCError subclass when the host reports a failure.
import mas
mas.open_app("com.android.settings")
mas.open_app("com.android.chrome", timeout_ms=5000)close_app
def close_app(package_name: str) -> NoneForce stop an app.
| Parameter | Type | Default | Meaning |
|---|---|---|---|
package_name | str | required | The package to stop |
Returns: None.
Raises: an RPCError subclass when the host reports a failure.
import mas
mas.open_app("com.example.game")
mas.log("automation runs here")
mas.close_app("com.example.game")get_current_app
def get_current_app() -> strThe package name of the foreground app.
Returns: the package name as a string.
Raises: an RPCError subclass when the host reports a failure.
import mas
import time
mas.open_app("com.android.chrome")
while "chrome" not in mas.get_current_app().lower():
time.sleep(0.5)
print("Chrome is active")get_app_state
def get_app_state(package_name: str) -> intThe state of an app as an integer. Compare it with the module constants.
| Constant | Value | Meaning |
|---|---|---|
mas.RUNNING_IN_FOREGROUND | 4 | Visible and focused |
mas.RUNNING_IN_BACKGROUND | 3 | Running, not visible |
mas.RUNNING_IN_BACKGROUND_SUSPENDED | 2 | In the background and frozen |
mas.NOT_RUNNING | 1 | Installed but not running |
mas.NOT_INSTALLED | 0 | Not on the device |
| Parameter | Type | Default | Meaning |
|---|---|---|---|
package_name | str | required | The package to check, or a desktop application name on a desktop host |
Returns: one of the five integers above.
Raises: an RPCError subclass when the host reports a failure.
import mas
if mas.get_app_state("com.example.game") == mas.NOT_RUNNING:
mas.open_app("com.example.game")
elif mas.get_app_state("com.example.game") == mas.NOT_INSTALLED:
mas.log("Game is not installed", level="error")is_app_focused
def is_app_focused(package_name: str) -> boolWhether the app currently has input focus, according to the Android window manager. A good guard before a tap or typing.
| Parameter | Type | Default | Meaning |
|---|---|---|---|
package_name | str | required | The package to check |
Returns: True when the app is focused, otherwise False.
Raises: an RPCError subclass when the host reports a failure.
import mas
import time
mas.open_app("com.example.game")
while not mas.is_app_focused("com.example.game"):
time.sleep(0.5)Device
Facts about the device and the computer the macro runs on.
get_screen_size
def get_screen_size() -> ScreenSizeThe device screen dimensions in pixels. Use it to compute coordinates so a macro survives a resolution change.
Returns: a ScreenSize with width and height.
Raises: an RPCError subclass when the host reports a failure.
import mas
size = mas.get_screen_size()
mas.click(size.width // 2, size.height // 2) # tap the centerget_device_info
def get_device_info() -> DeviceInfoDetails about the bound device.
Returns: a DeviceInfo with id, name, type, screen_width, screen_height and connected.
Raises: an RPCError subclass when the host reports a failure.
import mas
info = mas.get_device_info()
print(f"{info.name} ({info.type}) {info.screen_width}x{info.screen_height}")get_host_machine_id
def get_host_machine_id() -> strA stable UUID for the computer the macro runs on. It is generated once, survives restarts and is shared by every macro on that machine. It is the default machine_id for storage, so you rarely call it yourself.
Returns: the UUID as a string.
Raises: RPCError when the host returns no usable ID. The SDK refuses to fall back to an empty string, because storage is keyed by (machine_id, port, task_name) and an empty ID would corrupt every later save.
import mas
machine_id = mas.get_host_machine_id()
print(machine_id) # 550e8400-e29b-41d4-a716-446655440000Storage
save and retrieve give a macro memory across runs: a streak counter, a checkpoint, a “last processed” marker. Data is any JSON-serializable dict. Every entry is keyed by the triple (machine_id, port, task_name), so two emulators running the same macro keep separate state. Leave machine_id and port at their defaults; they resolve to this computer and the current device. Passing a port that is not the current device’s port raises ValueError.
save
def save(task_name: str, data: Dict[str, Any], machine_id: Optional[str] = None, port: int = 0) -> boolPersist a dict for a task. Saving the same key again updates the entry rather than duplicating it.
| Parameter | Type | Default | Meaning |
|---|---|---|---|
task_name | str | required | Identifier for the task or script |
data | Dict[str, Any] | required | JSON-serializable data |
machine_id | Optional[str] | None | Defaults to get_host_machine_id() |
port | int | 0 | 0 means the current device port |
Returns: True when the save succeeded.
Raises: ValueError when data is not JSON-serializable or port does not match the current device; RPCError when the machine ID cannot be read.
import mas
state = mas.retrieve("daily_login")
streak = state.get("streak", 0) + 1
mas.save("daily_login", {"streak": streak, "last_run": "2026-09-04"})retrieve
def retrieve(task_name: str, machine_id: Optional[str] = None, port: int = 0) -> Dict[str, Any]Read an entry back.
| Parameter | Type | Default | Meaning |
|---|---|---|---|
task_name | str | required | Task identifier |
machine_id | Optional[str] | None | Defaults to get_host_machine_id() |
port | int | 0 | 0 means the current device port |
Returns: the stored dict, or an empty dict when nothing was stored.
Raises: ValueError when port does not match the current device; RPCError when the machine ID cannot be read.
import mas
state = mas.retrieve("harvest") # {} on the first run
last_field = state.get("last_field", 0)retrieve_all
def retrieve_all(task_name: str) -> List[Dict[str, Any]]Every entry stored under a task name, across all machines and ports. Use it to roll up results from many devices running the same macro.
| Parameter | Type | Default | Meaning |
|---|---|---|---|
task_name | str | required | Task identifier |
Returns: a list of dicts, each with machine_id, port, data and updated_at.
Raises: an RPCError subclass when the host reports a failure.
import mas
entries = mas.retrieve_all("harvest")
total = sum(e["data"].get("collected", 0) for e in entries)
mas.log(f"Collected {total} across {len(entries)} devices")clear
def clear(task_name: str, machine_id: Optional[str] = None, port: int = 0) -> boolDelete a stored entry.
| Parameter | Type | Default | Meaning |
|---|---|---|---|
task_name | str | required | Task identifier |
machine_id | Optional[str] | None | Defaults to get_host_machine_id() |
port | int | 0 | 0 means the current device port |
Returns: True when the entry was cleared.
Raises: ValueError when port does not match the current device; RPCError when the machine ID cannot be read.
import mas
if mas.retrieve("daily_login").get("streak", 0) > 30:
mas.clear("daily_login")get_current_device_port
def get_current_device_port() -> intThe port of the device this run is bound to, for example 5556 for emulator-5556. The value is fetched once and cached. Useful for labelling per-device output.
Returns: the port as an integer.
Raises: RPCError when no device is connected.
import mas
port = mas.get_current_device_port()
mas.log(f"Running on port {port}")Utilities and webhooks
Logging, the clipboard, custom webhook events, and the JSON-RPC client that every call uses underneath.
log
def log(message: str, level: Union[LogLevel, str] = LogLevel.INFO) -> NoneSend a leveled line to the run console in Studio. Unlike print, the line carries a severity and is surfaced in the app’s log view. No device is required.
| Parameter | Type | Default | Meaning |
|---|---|---|---|
message | str | required | The text to log |
level | Union[LogLevel, str] | LogLevel.INFO | A LogLevel, or "debug", "info", "warning", "error" |
Returns: None.
Raises: an RPCError subclass when the app cannot be reached.
import mas
mas.log("Run started")
mas.log("retries running low", level="warning")
mas.log("step failed", level=mas.LogLevel.ERROR)get_clipboard
def get_clipboard() -> strRead the device clipboard as text.
Returns: the clipboard contents.
Raises: an RPCError subclass when the host reports a failure.
import mas
mas.click(300, 500, delay_ms=1500) # select text in the app
mas.click(400, 300) # tap the app's Copy control
copied = mas.get_clipboard()
print(copied)webhook
def webhook(event: str, data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]Send a custom webhook event to your configured endpoints. Use it to signal things only the script knows: a level reached, a captcha hit, an inventory full. MAS’s servers deliver the event, so it is signed, retried on failure and recorded in the delivery log next to macro.started and macro.failed. Your event name is namespaced under custom., so "level.reached" arrives as custom.level.reached; a script can never impersonate a lifecycle event. The run and device are attached automatically. Set up an endpoint first on the Webhooks page of the app, subscribed to custom.* or *; see Webhooks.
| Parameter | Type | Default | Meaning |
|---|---|---|---|
event | str | required | Letters, numbers, dots, dashes and underscores; 64 characters or fewer |
data | Optional[Dict[str, Any]] | None | JSON-serializable details, up to 50 keys |
Returns: a dict with event_id (the receiver’s dedupe key), event_type (the namespaced name) and queued (how many endpoints it went to). queued of 0 is not an error; no endpoint is subscribed to this event.
Raises: RuntimeError when the event could not be queued.
import mas
result = mas.webhook("level.reached", {"level": 40, "account": "alt-3"})
print(result["event_type"], result["queued"])RPCClient
class RPCClientThe JSON-RPC 2.0 client that every mas.* call uses. You rarely construct one yourself; get_client returns the shared instance. The constructor discovers the transport from the environment: MAS_RPC_SOCKET (a unix socket, used inside the cloud sandbox) takes precedence over TCP; otherwise MAS_RPC_HOST (default localhost) and MAS_RPC_PORT, then the port file the app writes, and finally ConnectionError when no port can be found. On connect it registers the session from MAS_SESSION_TOKEN, or binds a device from MAS_DEVICE_ID when a script was started outside Studio.
def __init__(self, host: Optional[str] = None, port: Optional[int] = None, connect_timeout: float = DEFAULT_CONNECT_TIMEOUT, read_timeout: float = DEFAULT_READ_TIMEOUT)| Parameter | Type | Default | Meaning |
|---|---|---|---|
host | Optional[str] | None | RPC host; defaults to MAS_RPC_HOST or localhost |
port | Optional[int] | None | RPC port; defaults to MAS_RPC_PORT, then the port file |
connect_timeout | float | DEFAULT_CONNECT_TIMEOUT (5.0) | Seconds to wait for the connection |
read_timeout | float | DEFAULT_READ_TIMEOUT (30.0) | Seconds to wait for a response |
Methods:
| Method | Returns | Meaning |
|---|---|---|
connect() | None | Open the socket and register the session or device; a no-op when already connected |
disconnect() | None | Close the socket |
call(method: str, params: Dict[str, Any]) | Any | Send one request and return its result; raises the mapped RPCError subclass on an error response |
The client is a context manager: with RPCClient() as client: connects on entry and disconnects on exit. Calls are serialized with a lock, so one client is safe to share across threads. A read that exceeds read_timeout raises RPCError and marks the client disconnected; the next call reconnects.
import mas
from mas import RPCClient
client = RPCClient(read_timeout=60.0)
try:
methods = client.call("host_capabilities", {})
print(methods.get("methods", []))
finally:
client.disconnect()get_client
def get_client() -> RPCClientGet or create the global RPCClient that the SDK functions share.
Returns: the shared RPCClient.
Raises: ConnectionError on first use when no RPC port can be discovered.
import mas
client = mas.get_client()
size = client.call("get_screen_size", {})
print(size["width"], size["height"])Runtime UI (mas.ui)
mas.ui drives the runtime dashboard, the live panel shown next to a running macro. You design the dashboard in the UI Builder as a .uibrt file, give every widget a name in the Inspector, and address it by that name from the script. A name that does not exist in the dashboard updates nothing and raises nothing, so a typo is the most common dashboard bug. Each call is one round trip to the app; wrap several updates in batch to send them together. See Runtime UI for the widget-by-widget guide and UI Builder for the designer.
| Widget | Call |
|---|---|
| Label, or the caption of a progress bar or button | set_text |
| Progress bar | set_progress |
| Chart | add_data_point, set_chart_data, clear_chart |
| Text area | append_text, set_textarea, clear_text |
| Table | set_table_data, append_table_row, clear_table, cell_button, set_cell_button_enabled |
| Button (read) | wait_for_event, on_click |
| Text input (read) | get_input_value, on_change, wait_for_event |
UIEvent
class UIEventA user interaction reported by wait_for_event or dispatched to a handler. It is a frozen dataclass.
| Field | Type | Default | Meaning |
|---|---|---|---|
widget_id | str | required | The widget’s name from the .uibrt, or a cell_button id |
event_type | str | required | "click" or "change" |
value | Any | required | The current value for a change; None for a click |
import mas
event = mas.ui.wait_for_event(timeout=60)
if event and event.widget_id == "start_btn" and event.event_type == "click":
print("Start pressed")set_text
def set_text(name: str, text: str) -> NoneSet the text of a label, a text area, a button, or the caption of a progress bar. The value is converted with str.
| Parameter | Type | Default | Meaning |
|---|---|---|---|
name | str | required | The widget name |
text | str | required | The new text |
Returns: None.
Raises: an RPCError subclass when the app cannot be reached.
import mas
mas.ui.set_text("status", "Logging in...")
mas.ui.set_text("status", f"Step {3}/{10}")set_progress
def set_progress(name: str, value: Union[int, float]) -> NoneSet the value of a progress bar. The app clamps it to the widget’s [min, max].
| Parameter | Type | Default | Meaning |
|---|---|---|---|
name | str | required | The widget name |
value | Union[int, float] | required | The new value |
Returns: None.
Raises: an RPCError subclass when the app cannot be reached.
import mas
for i in range(101):
mas.ui.set_progress("main_bar", i)add_data_point
def add_data_point(name: str, value: Union[int, float], label: str, series: Optional[str] = None) -> NoneAppend one point to a streaming chart. The app drops points beyond the widget’s maxDataPoints.
| Parameter | Type | Default | Meaning |
|---|---|---|---|
name | str | required | The chart name |
value | Union[int, float] | required | The y value |
label | str | required | The x label |
series | Optional[str] | None | Series name for grouped charts |
Returns: None.
Raises: an RPCError subclass when the app cannot be reached.
import mas
mas.ui.add_data_point("cpu_chart", value=42.5, label="t=12", series="CPU")
mas.ui.add_data_point("cpu_chart", value=61.0, label="t=13", series="CPU")set_chart_data
def set_chart_data(name: str, data: List[Dict[str, Any]]) -> NoneReplace a chart’s whole dataset. Each entry needs label (a string) and value (a number); series is optional.
| Parameter | Type | Default | Meaning |
|---|---|---|---|
name | str | required | The chart name |
data | List[Dict[str, Any]] | required | The points |
Returns: None.
Raises: an RPCError subclass when the app cannot be reached.
import mas
mas.ui.set_chart_data("scores", [
{"label": "Gold", "value": 1250},
{"label": "XP", "value": 45000},
])clear_chart
def clear_chart(name: str) -> NoneRemove every point from a chart.
| Parameter | Type | Default | Meaning |
|---|---|---|---|
name | str | required | The chart name |
Returns: None.
Raises: an RPCError subclass when the app cannot be reached.
import mas
mas.ui.clear_chart("scores")append_text
def append_text(name: str, line: str) -> NoneAppend a line to a text area. The panel auto-scrolls, and lines beyond the widget’s maxLines drop off the top. The SDK keeps a local copy of the text so it can compose the next append without a round trip.
| Parameter | Type | Default | Meaning |
|---|---|---|---|
name | str | required | The text area name |
line | str | required | The line to add |
Returns: None.
Raises: an RPCError subclass when the app cannot be reached.
import mas
mas.ui.append_text("log", "Run started")
mas.ui.append_text("log", "Step 1 complete")set_textarea
def set_textarea(name: str, text: str) -> NoneReplace the whole content of a text area.
| Parameter | Type | Default | Meaning |
|---|---|---|---|
name | str | required | The text area name |
text | str | required | The new content |
Returns: None.
Raises: an RPCError subclass when the app cannot be reached.
import mas
mas.ui.set_textarea("log", "fresh contents")clear_text
def clear_text(name: str) -> NoneClear a text area.
| Parameter | Type | Default | Meaning |
|---|---|---|---|
name | str | required | The text area name |
Returns: None.
Raises: an RPCError subclass when the app cannot be reached.
import mas
mas.ui.clear_text("log")set_table_data
def set_table_data(name: str, rows: List[Dict[str, Any]]) -> NoneReplace every row of a table. Each row is a dict whose keys match the widget’s columns.
| Parameter | Type | Default | Meaning |
|---|---|---|---|
name | str | required | The table name |
rows | List[Dict[str, Any]] | required | The rows |
Returns: None.
Raises: an RPCError subclass when the app cannot be reached.
import mas
mas.ui.set_table_data("results", [
{"Name": "Gold", "Value": "1,250", "Status": "OK"},
{"Name": "Stone", "Value": "12", "Status": "low"},
])append_table_row
def append_table_row(name: str, row: Dict[str, Any]) -> NoneAppend one row to a table. The SDK keeps the rows locally and re-sends the full table.
| Parameter | Type | Default | Meaning |
|---|---|---|---|
name | str | required | The table name |
row | Dict[str, Any] | required | Keys matching the widget’s columns |
Returns: None.
Raises: an RPCError subclass when the app cannot be reached.
import mas
mas.ui.append_table_row("results", {"Name": "Wood", "Value": "300", "Status": "OK"})clear_table
def clear_table(name: str) -> NoneRemove every row from a table.
| Parameter | Type | Default | Meaning |
|---|---|---|---|
name | str | required | The table name |
Returns: None.
Raises: an RPCError subclass when the app cannot be reached.
import mas
mas.ui.clear_table("results")cell_button
def cell_button(*, id: str, label: str, variant: str = "default", disabled: bool = False) -> Dict[str, Any]Build a button to place inside a table cell. Put the returned dict in a row passed to set_table_data or append_table_row. A click emits a UI event whose widget_id equals id, so the script handles it with the same on_click or wait_for_event channel as a standalone button. All parameters are keyword-only.
| Parameter | Type | Default | Meaning |
|---|---|---|---|
id | str | required | The event id for this button |
label | str | required | The button text |
variant | str | "default" | "default", "outline" or "destructive" |
disabled | bool | False | Start disabled |
Returns: a dict describing the button.
Raises: ValueError when variant is not one of the three values.
import mas
mas.ui.append_table_row("orders", {
"Name": "Gold",
"Action": mas.ui.cell_button(id="refund_gold", label="Refund", variant="destructive"),
})
mas.ui.on_click("refund_gold", lambda: mas.log("Refund requested"))
mas.ui.start_listener()set_cell_button_enabled
def set_cell_button_enabled(table_name: str, button_id: str, enabled: bool) -> NoneEnable or disable a button cell in a table that was already rendered. The SDK finds the cell by button_id in its local copy of the table, flips the flag and re-sends the table. Nothing happens when no cell has that id or the flag already matches.
| Parameter | Type | Default | Meaning |
|---|---|---|---|
table_name | str | required | The table name |
button_id | str | required | The id given to cell_button |
enabled | bool | required | True to enable, False to disable |
Returns: None.
Raises: an RPCError subclass when the app cannot be reached.
import mas
mas.ui.set_cell_button_enabled("orders", "refund_gold", False)wait_for_event
def wait_for_event(timeout: float = 30.0) -> Optional[UIEvent]Block until the user clicks a button or changes an input, or until timeout seconds pass. This is the blocking model: the script stops and waits.
| Parameter | Type | Default | Meaning |
|---|---|---|---|
timeout | float | 30.0 | Seconds to wait |
Returns: a UIEvent, or None when the timeout elapsed with no event.
Raises: an RPCError subclass when the app cannot be reached.
import mas
mas.ui.set_text("status", "Press Start when the game is on the home screen")
event = mas.ui.wait_for_event(timeout=120)
if event is None or event.widget_id != "start_btn":
raise SystemExit(1)get_input_value
def get_input_value(name: str) -> Optional[str]The most recent value typed into an input widget. No listener is needed.
| Parameter | Type | Default | Meaning |
|---|---|---|---|
name | str | required | The input widget name |
Returns: the value, or None when the widget has none.
Raises: an RPCError subclass when the app cannot be reached.
import mas
query = mas.ui.get_input_value("search_box")
if query:
mas.input_text(query)on_click
def on_click(name: str, handler: Callable[[], None]) -> NoneRegister a callback for clicks on a button or a cell_button. The callback fires only while the listener thread runs; call start_listener once after registering every handler. Registering a name again replaces the previous handler.
| Parameter | Type | Default | Meaning |
|---|---|---|---|
name | str | required | The button name or cell button id |
handler | Callable[[], None] | required | Called with no arguments on the listener thread |
Returns: None.
Raises: nothing.
import mas
stop = False
def on_stop():
global stop
stop = True
mas.ui.on_click("stop_btn", on_stop)
mas.ui.start_listener()on_change
def on_change(name: str, handler: Callable[[Any], None]) -> NoneRegister a callback for value changes on an input widget. The callback receives the new value as its only argument and fires only while the listener thread runs.
| Parameter | Type | Default | Meaning |
|---|---|---|---|
name | str | required | The input widget name |
handler | Callable[[Any], None] | required | Called with the new value on the listener thread |
Returns: None.
Raises: nothing.
import mas
def on_delay_change(value):
mas.log(f"Delay set to {value}")
mas.ui.on_change("delay_input", on_delay_change)
mas.ui.start_listener()start_listener
def start_listener() -> NoneStart the daemon thread that dispatches UI events to on_click and on_change handlers. It is idempotent: a second call while the thread is alive does nothing. The thread opens its own RPC connection so its long waits never block mas.* calls on the main thread, and it is a daemon, so it does not keep the interpreter alive after the script returns. A handler that raises is caught and its traceback printed; the listener keeps running.
Returns: None, immediately.
Raises: ConnectionError when the listener’s connection cannot be opened.
import mas
paused = False
def toggle():
global paused
paused = not paused
mas.ui.on_click("pause_btn", toggle)
mas.ui.start_listener()stop_listener
def stop_listener() -> NoneSignal the listener thread to exit and close its connection. Mostly useful in tests; a finished script does not need it.
Returns: None.
Raises: nothing.
import mas
mas.ui.start_listener()
mas.ui.set_text("status", "Running")
mas.ui.stop_listener()batch
def batch() -> Iterator[None]A context manager that coalesces every update inside the with block into one RPC call. This is the difference between a smooth dashboard and a flickering one. Nesting is not supported. The buffer is process-global, not thread-local: updates from other threads while a batch is open are pulled into it, so finish the batch before letting listener handlers fire their own updates.
Returns: a context manager; the block yields None.
Raises: RuntimeError when a batch is opened inside another batch.
import mas
with mas.ui.batch():
mas.ui.set_text("status", "Step 3/10")
mas.ui.set_progress("main_bar", 30)
mas.ui.add_data_point("cpu_chart", value=42, label="t=3", series="CPU")Exceptions
Rule of thumb: None means “not found, and that is fine”, so handle it with if. An exception means something is wrong, so let it stop the macro unless you have a recovery. Every SDK error descends from MASError. Errors that come back from the app carry a JSON-RPC code and are raised as the matching RPCError subclass; an unknown code raises plain RPCError. ElementsUnavailable is the one exception that is not an RPC error. The Errors page shows handling patterns.
MASError
RPCError .code, .message, .data
ConnectionError could not reach the app (code -1)
ParseError -32700
InvalidRequestError -32600
MethodNotFoundError -32601
InvalidParamsError -32602
InternalError -32603
DeviceNotConnectedError -32001
DeviceNotFoundError -32002
CommandFailedError -32003
ImageNotFoundError -32004
TimeoutError -32005
AuthenticationError -32010
SubscriptionError -32011
ElementsUnavailable the accessibility tree could not be readConnectionError and TimeoutError shadow the Python built-ins of the same name inside the mas namespace. Refer to them as mas.ConnectionError and mas.TimeoutError to keep the two apart.
MASError
class MASError(Exception)The base class for every SDK error. Catch it to handle any SDK failure in one place.
import mas
try:
mas.open_app("com.example.game")
except mas.MASError as e:
mas.log(f"SDK error: {e}", level="error")
raiseRPCError
class RPCError(MASError)Raised when an RPC call fails. It is the base class of every coded error below, and the class you get for a code that has no subclass, an invalid response, a socket error, or a response that took longer than the client’s read timeout.
def __init__(self, message: str, code: int = -1, data: Optional[Any] = None)| Attribute | Type | Default | Meaning |
|---|---|---|---|
code | int | -1 | The JSON-RPC error code |
message | str | required | The human-readable message |
data | Optional[Any] | None | Extra data from the app |
str(error) renders as RPC Error (<code>): <message>.
import mas
try:
mas.click(100, 100)
except mas.RPCError as e:
print(e.code, e.message, e.data)ConnectionError
class ConnectionError(RPCError)Raised when the SDK cannot reach the app: no RPC port could be discovered, the connection was refused, or connecting timed out. The message says to make sure MAS is running. Its code is -1.
import mas
try:
size = mas.get_screen_size()
except mas.ConnectionError:
print("Start Macro Automation Studio, then run the script again")ParseError
class ParseError(RPCError)Raised when the app received invalid JSON (code -32700). The SDK builds every request itself, so this indicates a transport problem, not a script bug.
import mas
try:
mas.log("hello")
except mas.ParseError as e:
print(e.message)InvalidRequestError
class InvalidRequestError(RPCError)Raised when the JSON-RPC request is malformed (code -32600).
import mas
try:
mas.get_current_app()
except mas.InvalidRequestError as e:
print(e.message)MethodNotFoundError
class MethodNotFoundError(RPCError)Raised when the requested method does not exist on this host (code -32601). Check host_capabilities before relying on a method that not every host provides.
import mas
try:
xml = mas.dump_hierarchy()
except (mas.MethodNotFoundError, mas.ElementsUnavailable):
xml = ""InvalidParamsError
class InvalidParamsError(RPCError)Raised when the method parameters are invalid (code -32602), for example a value outside the range the host accepts.
import mas
try:
mas.key_press(mas.KeyCode.DELETE, repeat=500)
except mas.InvalidParamsError as e:
print(e.message)InternalError
class InternalError(RPCError)Raised when the app hits an internal error (code -32603).
import mas
try:
shot = mas.take_screenshot()
except mas.InternalError as e:
mas.log(f"Host error: {e.message}", level="error")
raiseDeviceNotConnectedError
class DeviceNotConnectedError(RPCError)Raised when a device operation runs with no device bound to this connection (code -32001). The emulator stopped, the cloud device went away, or the script was started without a device.
import mas
try:
mas.click(540, 960)
except mas.DeviceNotConnectedError:
mas.log("No device connected", level="error")
raise SystemExit(1)DeviceNotFoundError
class DeviceNotFoundError(RPCError)Raised when the requested device does not exist or is not available (code -32002), for example when MAS_DEVICE_ID names a device the app cannot see.
import mas
try:
info = mas.get_device_info()
except mas.DeviceNotFoundError as e:
print(e.message)CommandFailedError
class CommandFailedError(RPCError)Raised when a device command such as a click, swipe or screenshot fails to complete on the device (code -32003).
import mas
try:
mas.swipe((500, 1500), (500, 500))
except mas.CommandFailedError as e:
mas.log(f"Swipe failed: {e.message}", level="warning")ImageNotFoundError
class ImageNotFoundError(RPCError)Raised when a template image cannot be used (code -32004): the ID does not exist in your Image Library, or the file cannot be loaded. A template that exists but does not appear on screen is not an error; find_object returns None for that.
import mas
try:
match = mas.find_object(999999)
except mas.ImageNotFoundError:
mas.log("Image 999999 is not in your Image Library", level="error")
raiseTimeoutError
class TimeoutError(RPCError)Raised when an operation on the host exceeds its time budget (code -32005). Note that wait_for_object returns None on a timeout instead of raising; this error comes from the host when a command itself times out.
import mas
try:
text = mas.read_text(timeout_ms=5000)
except mas.TimeoutError:
mas.log("OCR took longer than 5 seconds", level="warning")AuthenticationError
class AuthenticationError(RPCError)Raised when you are not logged in to MAS (code -32010). Log in and run the script again.
import mas
try:
mas.log("start")
except mas.AuthenticationError:
print("Log in to Macro Automation Studio, then run the script again")
raise SystemExit(1)SubscriptionError
class SubscriptionError(RPCError)Raised when an active subscription is required (code -32011). The subscription is verified once when a script starts, so this should not appear mid-run. If it does, the app may have been unable to confirm your subscription with the server, for example after a network problem; it does not necessarily mean the subscription has lapsed. See Billing.
import mas
try:
mas.get_screen_size()
except mas.SubscriptionError as e:
print(e.message)
raise SystemExit(1)ElementsUnavailable
class ElementsUnavailable(MASError)Raised when the accessibility tree cannot be read for this screen: a game or canvas surface, a secure screen, a host without dump support, a device-side dump failure, or a dump that did not arrive in time. It means the tree is unavailable, not that the element you looked for is absent. Catch it to fall back to vision or to retry later; never treat it as “already done”. It descends from MASError directly, not from RPCError, and carries no code.
import mas
images = mas.images({"claim": 321})
try:
target = mas.find_element({"rid": "com.app:id/claim"})
except mas.ElementsUnavailable:
target = mas.find_object_retry(images.claim)
if target:
mas.click(*target.center)Next steps
Related pages
Thanks. If something is wrong, tell us in Discord.
Questions? Ask in Discord