Search

Python SDK

Persist Macro State: Save Data Between Runs

Persist macro state and save data between runs with the MAS SDK storage functions, keyed by machine, emulator port and task name, with device group patterns.

  • Windows
  • Mac
  • Emulator
  • Cloud device
  • Python SDK
Intermediate Updated 5 min read
On this page
  1. Persist macro state: how entries are keyed
  2. save
  3. retrieve and retrieve_all
  4. clear
  5. get_current_device_port
  6. Pattern: save data between runs with a counter
  7. Pattern: skip work already done today
  8. Pattern: per-device state in a device group
  9. Pattern: a checkpoint inside a long run
  10. Pattern: resetting state
  11. Gotchas

A macro process starts empty on every run, so nothing in memory survives. Storage is the small key-value store Macro Automation Studio (MAS) keeps so a script can persist macro state and save data between runs: a counter, a date or a checkpoint. The functions are top-level on mas: save, retrieve, retrieve_all, clear and get_current_device_port. Full signatures are on the API reference. If you describe the task to MAS Agent, it writes this code for you.

Persist macro state: how entries are keyed

Every entry is addressed by three values: the machine ID, the device port and a task name.

  • The machine ID is the persistent UUID of the computer running MAS, from get_host_machine_id(). It is filled in for you.
  • The port is the adb port of the device the script runs on, such as 5556 for emulator-5556. It is also filled in for you, from get_current_device_port().
  • The task name is a string you choose, such as "daily_login".

The consequence is that the same script on two emulators keeps two separate entries, one per port, under one task name. That is what makes per-device state in a device group work without extra code. Saving again to the same key updates the entry instead of adding a second one.

On the desktop app the store is a local database on your computer, so entries survive app restarts and are visible to every macro on that machine. Cloud devices serve the same calls; there the machine ID identifies the cloud runner rather than your computer.

save

save(task_name, data, machine_id=None, port=0) stores a dictionary and returns True on success. data must be JSON-serializable: strings, numbers, booleans, None, and lists and dictionaries of those. A datetime, a set or a dataclass raises ValueError before anything is sent, so convert first (datetime.isoformat(), list(...)).

port=0 means the current device. Passing any other port raises ValueError with a port mismatch message: a script can only write its own device’s entry. machine_id can be overridden, but leave it alone unless you deliberately share an entry across machines.

save replaces the whole dictionary; it does not merge keys. Read first, change what you need, and write the full dictionary back.

retrieve and retrieve_all

retrieve(task_name, machine_id=None, port=0) returns the stored dictionary for this machine, this port and the task, or an empty dictionary when there is none. It never raises for a missing entry, so mas.retrieve(task).get("count", 0) is the idiom for a first run. The same port rule applies: you can only read your own device’s entry this way.

retrieve_all(task_name) returns every entry stored under the task name, across all ports and machines. Each item is a dictionary with machine_id, port, data and updated_at. This is how one device sees what the others in a device group have done.

clear

clear(task_name, machine_id=None, port=0) deletes the entry for this machine, this port and the task, and returns True. Use it to reset a counter, or to recover from a stored value that turned out to be wrong.

get_current_device_port

get_current_device_port() returns the port of the device the script is bound to. The value is fetched once and cached for the rest of the run. Beyond storage, it is a good tag for log lines and webhook payloads when several devices run the same macro.

Pattern: save data between runs with a counter

Read, increment, write. Because the entry is keyed by port, each emulator keeps its own count.

python
import mas
from datetime import datetime, timezone

TASK = "daily_login"

state = mas.retrieve(TASK)
runs = state.get("runs", 0) + 1
mas.save(TASK, {
    "runs": runs,
    "last_run": datetime.now(timezone.utc).isoformat(),
})
mas.log(f"run number {runs} on port {mas.get_current_device_port()}")

Pattern: skip work already done today

Store the date of the last success and compare on the next run. ISO date strings compare as text, so no parsing is needed.

python
import mas
from datetime import date

TASK = "claim_daily_reward"
today = date.today().isoformat()

if mas.retrieve(TASK).get("claimed_on") == today:
    mas.log("already claimed today, nothing to do")
    raise SystemExit(0)

mas.log("claiming the reward")     # your tapping steps go here
mas.save(TASK, {"claimed_on": today})

Pair this with the Scheduler and a daily recurrence, and the macro becomes safe to run as often as you like.

Pattern: per-device state in a device group

Every device in a group runs the script on its own port, so plain save and retrieve already give you per-device state. To coordinate across devices, read everything with retrieve_all.

python
import mas

TASK = "farm_progress"
me = mas.get_current_device_port()

mine = mas.retrieve(TASK)
mas.save(TASK, {"gold": mine.get("gold", 0) + 150, "port": me})

total = 0
for entry in mas.retrieve_all(TASK):
    gold = entry["data"].get("gold", 0)
    total += gold
    mas.log(f"port {entry['port']}: {gold} (updated {entry['updated_at']})")
mas.log(f"group total: {total}")

Because a device can only write its own entry, there is no race between devices: each one owns one row. See Device groups for how the group runs devices one after another.

Pattern: a checkpoint inside a long run

Save the step you reached, so a run that is stopped or crashes can resume instead of starting over.

python
import mas

TASK = "quest_chain"
steps = ["accept", "travel", "fight", "turn_in"]

done = mas.retrieve(TASK).get("done", [])
for step in steps:
    if step in done:
        continue
    mas.log(f"running {step}")
    # ... perform the step ...
    done.append(step)
    mas.save(TASK, {"done": done})

mas.clear(TASK)          # the chain is complete; start fresh next time
mas.log("quest chain finished")

Pattern: resetting state

python
import mas

TASK = "farm_progress"

if mas.retrieve(TASK).get("gold", 0) < 0:
    mas.log("corrupt gold counter, resetting", level="warning")
    mas.clear(TASK)

A reset checkbox on the macro’s argument form is a friendly way to expose this; see UI Builder for how a checkbox reaches your script.

Gotchas

  • Task names are global on the machine. Two macros that both save "progress" on the same port overwrite each other. Prefix the task name with the macro name, as in "myfarm.progress".
  • Only JSON goes in. Convert dates to ISO strings and sets to lists before save.
  • save replaces, it does not merge. Always write the full dictionary.
  • retrieve and clear cannot target another device’s port. Use retrieve_all to read across devices.
  • Keep entries small. The store is for counters and checkpoints, not for screenshots or logs.
  • get_current_device_port and get_host_machine_id raise RPCError when no device is bound or the host has no machine ID; storage calls surface the same error. See Errors and exceptions.

Next steps

Related pages

Was this page helpful?

Questions? Ask in Discord