# 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.

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

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](/docs/api-reference). If you describe the task to [MAS Agent](/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()`](/docs/api-reference#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()`](/docs/api-reference#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)`](/docs/api-reference#save) 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)`](/docs/api-reference#retrieve) 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)`](/docs/api-reference#retrieve_all) 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)`](/docs/api-reference#clear) 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()`](/docs/api-reference#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](/docs/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](/docs/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](/docs/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](/docs/sdk/errors).
