You are automating an interface somebody else ships and can change without telling you. TikTok moves a button. Android adds a permission dialog. A phone auto-locks halfway through. None of that is a defect in your automation — it is the operating condition.
So the question is not how do I stop it breaking. It is how fast do I find out, and what does it cost me in between.
Catch it at save time, not at 3am
Most breakage is not the app changing. It is a wiring mistake made at the keyboard. Saving a routine's graph runs the same checks the executor will, duplicated in a second language on purpose — the alternative is a routine that saves cleanly, ships to a Mac, and turns out on Tuesday night to have an edge pointing at a node somebody deleted.
Three refusals earn their keep: an edge pointing at a node that does not exist, a box nothing can reach from the start, and a step this build cannot run — there are 17 kinds of step and anything else is rejected by name. A misspelled key is refused rather than ignored, because the key most likely to be mistyped is an edge: a dropped next leaves a node with no successor, and the executor reads that as the routine ends here, successfully. A routine skipping its own checkpoint, reporting a win.
Fail loudly, and attach the picture
Every step screenshots on failure and the image rides with the job record. “No node matched Text("Post")” on board fourteen is a guessing game; the picture is usually the whole diagnosis, and nine times in ten it is a consent dialog, a login wall or an update banner sitting over the control the step wanted.
A tap that cannot fail is not a checkpoint
adb shell input tap returns success whether or not anything was under the finger. A routine made only of coordinates reports success on all twenty phones while pressing whatever happens to be at those pixels — and if nineteen of your boards have identical panels, it will pass every test you think to run.
Not hypothetical. A compiled routine here walked into an app's settings on a board it had not been taught on, matched a generic button2, and pressed Uninstall; it survived only because the confirmation dialog did not match the next selector. Two rules came out of that: ten destructive labels are refused unless a step says out loud that it means to press one, and a routine with no assertion is marked as having no checkpoint rather than implying a proof that is not there.
Rank your selectors and refuse the bad ones while they are being written: id, then accessibility label, then visible text. Minified ids are skipped — TikTok's are three characters long, stable within a build and gone at the next release, so there a label is the more durable choice. Why input tap is not automation takes that apart properly.
Never retry blindly
Retry is safe for some steps and catastrophic for others. Re-running open the app → go to the editor costs seconds. Re-running a routine whose upload succeeded and whose confirmation timed out costs a second post on a client's account.
A failed job retries three times with backoff, then stops and keeps the screenshot; every job has a five-minute ceiling so one hung phone cannot block its queue for ever. The dedupe gate sits underneath it all, at the boundary rather than inside the routine, because a routine cannot know what a previous run of itself already did.
Run it once and watch it
Trust is earned per routine, not granted per product. Run it once executes on the phone you choose with the boxes colouring in as it goes: green where it worked, red where it stopped. There is one executor, not a debug twin — the watched run is the same code the unattended job runs, with a progress callback added. A step-through that ran different code from the nightly job would be debugging a program nobody runs.
The failure to fear is the one that looks healthy
We shipped one, in the part of the app that mirrors twenty phones at once. A stream that ended never freed its channel, and the poll that restarts a stream only restarts a phone with no channel — so once a phone's mirror died, it stayed dead. Nobody noticed, because a tile with no live stream falls back to a 3-second polled screenshot, which looks perfectly fine on a mostly-static phone. The fallback that kept the grid looking healthy is exactly what hid the corpse.
The iPhone has the same shape twice over: WebDriverAgent returns a valid all-black PNG when the display is asleep rather than erroring, and a phone that auto-locks mid-run accepts every gesture with HTTP 200 while doing nothing. Write down what each fallback is hiding, and keep something upstream that can still tell alive from dead.
The honest ceiling
All of that shortens the gap between the break and the alert. None of it removes the break. Running a routine unattended against an app somebody else controls needs a person who checks it, and that maintenance is the running cost of not going through an API — which what each posting API will actually let you do lays out in full.
Two absences, plainly. Selector config is versioned here as a convention, not yet as a pack you can roll back from the server. And there is no self-healing locator quietly repairing a broken step: we would rather refuse the routine than have it press something adjacent and report success.
The phone-farm vendors sell unattended perfection, usually next to a story about faking the device. It does not exist. What exists is a short list of choices that decide whether you find out at save time, in a watched run, in an alert — or from the client. Pick the leftmost one you can afford.