# LDPlayer Macro Loop, Stop Conditions and Scheduling

> Loop a macro on LDPlayer or BlueStacks with stop conditions, keep progress between runs, exit with a status code, and schedule it daily in the MAS Scheduler.

Source: https://automationmacro.com/docs/guides/loops-and-scheduling (Guides, updated 2026-09-04)

A macro that runs unattended needs three things: a loop that knows when to stop, a way to remember where it got to, and a schedule that starts it without you. This guide covers all three for an LDPlayer or BlueStacks macro in Macro Automation Studio (MAS). You get the loop patterns, the stop conditions, storage between runs, exit codes that webhooks can report, and the Scheduler. It is for anyone moving a macro from "I click Run" to "it runs every morning".

## Before you start

- MAS is installed and signed in. It is a [free download](/download) for the trial.
- A Code-Based project runs on an emulator from the Code Editor. See [Getting started](/docs/getting-started).
- The emulator is added as a device with a port you can find on its card. See [Devices](/docs/devices).
- Your templates and regions are captured. See the [image recognition guide](/docs/guides/image-recognition-macros).

## Macro loop patterns for LDPlayer and BlueStacks

Three shapes cover nearly every macro.

**While, with a stop condition.** The loop runs until something on screen or in your counters says stop.

```python
import mas

images = mas.images({"collect": 401, "done": 402})

collected = 0
while collected < 50:
    if mas.find_object(images.done):
        break
    button = mas.find_object_retry(images.collect, total_tries=3, time_sleep=2.0)
    if button is None:
        continue
    mas.click(button.x, button.y, delay_ms=1000)
    collected += 1
```

**For a fixed count.** You know how many times to do the thing.

```python
for round_no in range(1, 11):
    mas.log(f"Round {round_no} of 10")
    button = mas.find_object_retry(images.collect)
    if button is None:
        mas.log("Nothing to collect, stopping early", level="warning")
        break
    mas.click(button.x, button.y)
```

**Until a template appears.** A wait with a deadline, used for loading screens and long timers.

```python
import time

def wait_for(image, timeout_s=120, every_s=3.0):
    deadline = time.monotonic() + timeout_s
    while time.monotonic() < deadline:
        match = mas.find_object(image)
        if match:
            return match
        time.sleep(every_s)
    return None
```

Every loop above has an exit. `find_object` returns `None` on a miss and never raises, so a missing template on its own cannot end a `while True`. Give every loop a bound.

## Macro loop stop conditions

Combine at least two of these. One guards against the game, the other against your own bug.

- **A counter.** `while collected < 50`. Cheap and predictable.
- **A time budget.** `time.monotonic()` counts seconds and never jumps when the clock changes. Use it for the outer loop so a scheduled run always ends before the next one starts.
- **A screen state.** A template such as "out of energy", "inventory full" or a daily-limit message. Capture it in Asset Lab like any other template and check it at the top of each pass.
- **Consecutive misses.** Count how many passes in a row found nothing and give up after five. A screen you did not plan for looks exactly like this.

```python
started = time.monotonic()
misses = 0
while time.monotonic() - started < 20 * 60:
    if mas.find_object(images.out_of_energy):
        break
    button = mas.find_object_retry(images.collect)
    if button is None:
        misses += 1
        if misses >= 5:
            break
        continue
    misses = 0
    mas.click(button.x, button.y)
```

## Sleeps and humanized timing

`click` already waits `delay_ms=1000` after the tap and `swipe` takes `duration_ms=1000` by default. Between passes, add a pause that varies.

```python
import random

def pause(low=0.8, high=2.5):
    time.sleep(random.uniform(low, high))
```

Fixed intervals look mechanical and also race the game. A popup that appears 900 ms after a tap slips past a fixed 1000 ms delay on a slow day. A random pause smooths both. Keep `delay_ms` on `click` for the app's own reaction time and use `pause()` for the human rhythm.

## Persist progress with storage

A scheduled run that stops halfway should pick up where it left off the next time. The storage functions keep a small JSON document per task, keyed by this computer, the device's port and a task name you choose.

```python
TASK = "daily_collect"

state = mas.retrieve(TASK)          # {} on the first run
collected = state.get("collected", 0)
last_day = state.get("day")

mas.save(TASK, {"collected": collected, "day": today})   # after each pass of the loop

if collected >= 50:
    mas.clear(TASK)                 # start fresh next time
```

`save(task_name, data)` writes or replaces the entry; `retrieve(task_name)` returns the dictionary or an empty one; `clear(task_name)` deletes it. `data` must be JSON serialisable. Because the port is part of the key, two emulator instances running the same macro never share a counter. `retrieve_all(task_name)` returns every instance's entry when you want a total. See [Storage](/docs/sdk/storage).

> [!TIP]
> Store the date with the counter. A daily macro can then tell a resumed run from a new day and reset itself.

## Exit with a code so webhooks report status

MAS reads the process exit code when your script ends. Zero means the run completed; anything else marks it as failed. Webhook subscribers receive `macro.completed` or `macro.failed` accordingly, with the `exit_code` in the payload.

```python
import sys

if energy is None:
    mas.log("Could not read energy", level="error")
    sys.exit(2)      # macro.failed, exit_code 2

mas.log("All done")
sys.exit(0)          # macro.completed
```

An unhandled exception also exits non-zero, so a crash is reported as a failure without any extra code. For events only your script knows about, `mas.webhook("level.reached", {"level": 40})` sends a `custom.level.reached` event to endpoints subscribed to custom events. Set endpoints up on the **Webhooks** page; see [Webhooks](/docs/webhooks).

## A complete example

The macro collects up to fifty rewards a day, remembers its count, stops on "out of energy" or after twenty minutes, and reports through its exit code. Replace the IDs with yours.

```python
import datetime
import random
import sys
import time

import mas

images = mas.images({
    "collect": 401,
    "close_popup": 402,
    "out_of_energy": 403,
})

TASK = "daily_collect"
DAILY_LIMIT = 50
TIME_BUDGET_S = 20 * 60


def pause(low=0.8, high=2.5):
    time.sleep(random.uniform(low, high))


def main():
    today = datetime.date.today().isoformat()
    state = mas.retrieve(TASK)
    collected = state.get("collected", 0) if state.get("day") == today else 0
    mas.log(f"{today}: starting at {collected} of {DAILY_LIMIT}")

    started = time.monotonic()
    misses = 0
    while collected < DAILY_LIMIT:
        if time.monotonic() - started > TIME_BUDGET_S:
            mas.log("Time budget reached", level="warning")
            break
        popup = mas.find_object(images.close_popup)
        if popup:
            mas.click(popup.x, popup.y, delay_ms=800)
            continue
        if mas.find_object(images.out_of_energy):
            mas.log("Out of energy")
            break
        button = mas.find_object_retry(images.collect, total_tries=3, time_sleep=2.0)
        if button is None:
            misses += 1
            if misses >= 5:
                mas.log("Five misses in a row, giving up", level="error")
                mas.save(TASK, {"collected": collected, "day": today})
                sys.exit(2)
            continue
        misses = 0
        mas.click(button.x, button.y, delay_ms=1000)
        collected += 1
        mas.save(TASK, {"collected": collected, "day": today})
        pause()

    mas.log(f"Finished at {collected} of {DAILY_LIMIT}")
    sys.exit(0)


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

Run it once from the Code Editor with **Run** (<kbd>F5</kbd>) and confirm it ends on its own before you schedule it.

## Schedule the macro in the MAS Scheduler on BlueStacks or LDPlayer

The Scheduler lives inside the desktop app and starts macros on your emulators at the times you set.

1. Open **Scheduler** and click **Create New Schedule**.
2. Enter a **Name** (3 to 100 characters) and pick the **Macro**.
3. Under **Emulator Port**, click **Scan** and choose the port of the instance, or type it.
4. Set **Date** and **Time**. The time is your computer's local time.
5. Set **Recurrence**: **None** for a single run, **Daily**, **Weekly** with the **Days of Week** ticked, or **Monthly**.
6. Leave **Repeat Count** at 1 unless you want the macro to run several times back to back at each firing.
7. Keep **Active** on and click **Create Schedule**.

What to know before you rely on it:

- The app must stay open and signed in. The scheduler is not a system service and there is no cron syntax.
- Jobs are checked every 30 seconds, so a run can start up to half a minute after its time.
- A schedule targets one port. If the port is busy with another run when the time comes, the scheduler skips that cycle and tries again on the next one.
- Two schedules cannot share a time slot on the same port; the app refuses with "Time slot is occupied on this port".
- Up to 20 scheduled jobs can run at the same time.
- With **Repeat Count** above 1, a failed run (non-zero exit) stops the remaining repeats and marks the job as Failed.
- Scheduled runs use the arguments saved on the device card, or the settings profile the device follows. See [Settings profiles](/docs/settings-profiles).
- **View History** on a job shows every run with its status and logs.

The [Scheduler](/docs/scheduler) page covers editing, skipping and cancelling jobs.

## Run it on a group

To run the same macro on several instances by hand, put them in one device group, assign the macro to each device and click **Start All**. Devices start one after another, half a second apart, and each gets its own run, logs and exit code. To schedule the group, create one scheduled job per port with the same macro and time. The [multi-instance guide](/docs/guides/multi-instance) covers per-device arguments, proxies and storage.

## What can go wrong

### The loop never ends

A `while True` with no bound, or a stop condition that depends on a template that never appears. Add a time budget with `time.monotonic()` and a consecutive-miss counter; those two end every loop.

### The schedule did not fire

The app was closed or asleep at the time, the port was busy, or the job is not **Active**. Check **View History** for a skipped or failed entry, and keep the computer awake for the scheduled window.

### Time slot is occupied on this port

Another job on the same port already owns that time. Move one job a few minutes, or put the second macro inside the first with **Repeat Count**.

### The run shows Failed but the macro finished

The script ended with a non-zero exit code or an unhandled exception after its last action. Read the last lines of the log; a `sys.exit(1)` in a cleanup path or a `KeyError` on the stored state are the usual causes.

No automation tool is 100% risk-free, so automate responsibly and at your own discretion.
