iOSScript Development

How to Write iOS No-Jailbreak Scripts: From Setup to a First Working Script

The slow part of writing iOS no-jailbreak scripts is not the logic, it is working out what each function returns. This walks the real path: environment setup, the script skeleton, a function map, coordinates and waiting, debugging, and rolling out to many devices, with the documented function names and parameters throughout.

14 min readUpdated

One chair, three hours

Someone I know runs outsourced e-commerce customer service. He wanted to hand over one job: walking through the order status of two dozen back ends every morning.

He started with the documentation, found a wall of functions and no obvious entry point. Then he ran into the vocabulary — no-jailbreak, USB HID, proxy mode, Bluetooth — all familiar, none of them obviously connected to writing a script.

His three days went like this. Day one, installing. Day two, trying to read one example. Day three, stuck on which event module to use. Actual code started on day four.

This article compresses those four days. The order follows the real path a script takes from nothing to running, with the documented function names and parameters at each step. Writing iOS no-jailbreak scripts is time-consuming not because of logic, but because of working out what each function returns.


1. What “no-jailbreak” actually means here

The first thing to settle is the term itself, because most early confusion comes from it.

No-jailbreak does not mean “installs nothing”. It means the system is not modified. The sandbox, the signing rules and system integrity all stay as they are; your phone still updates and still keeps its warranty. Automation manages this because it only needs two things:

  1. See the screen — a screenshot or a mirroring feed
  2. Deliver input — taps, swipes, typing

Both have channels iOS already exposes, so nothing has to be opened up. The full meaning of a “no-jailbreak script” is therefore: programmatically doing “read the screen plus deliver input” without modifying the system.

That framing dissolves a lot of questions. Can a no jailbreak script change system settings? No, because it never touches the system. And almost no business process needs to — what it needs is a batch of fixed actions performed on time.

This is also why the entry bar for iOS automation scripts is lower than people expect. You need two capabilities, and nothing else.


2. Where the three routes differ

This is where beginners stall. The same business logic calls a different event module depending on the route:

Route Event module System requirement What else you need
Proxy mode agentEvent iOS 13+ A signed IPA
USB HID usbHidEvent iOS 17+ One data cable
Bluetooth BLE bleEvent iOS 17+ An ESP32 board
OTG HID otgEvent iOS 17+ A board plus an adapter

iOS 17 is a hard threshold. The documentation is explicit: USB HID and Bluetooth BLE both require iOS 17 or above, and anything lower has to use proxy mode. On older hardware, confirm the version before starting; it saves a lot of wasted effort.

The good news is that all four modules share the same method names:

Available in all four modules:
  clickPoint(x, y)                      tap a coordinate
  press(x, y, duration)                 long press
  doubleClickPoint(x, y)                double tap
  swipeToPoint(x1,y1,x2,y2,duration)    swipe
  multiTouch(fingers)                   multi-finger gesture
  touchDown / touchMove / touchUp       build custom gestures
  systemKey(...)                        Home, volume and similar
  keyPress / keyPressChar               keys and characters

So the logic is written once and switching routes changes a module name. Worth exploiting: wrap your click operations, and a route change touches one place.

// Wrap the event module; a route change touches only this line
const EV = usbHidEvent;   // agentEvent for proxy, bleEvent for Bluetooth, otgEvent for OTG

function tap(x, y) {
    return EV.clickPoint(x, y);
}

Each module has a few extras for its own route — bleEvent.openSerial and setWifiInfo on Bluetooth, otgEvent.scanOtgDevice and isOtgConnect on OTG — which you look up when you need them.

A note on no signing: proxy mode needs a signed IPA, while USB HID, Bluetooth and OTG need no signing at all. No certificate to buy, no expiry to track. For many teams that is the first reason to pick a hardware route.


3. Setup: four things to confirm first

  1. iOS version — it decides which route you can take (see the table above)
  2. Automation service state — the central control shows whether it is ready; nothing runs without it
  3. Licence type — running scripts requires a USB device licence; mirroring requires a USB mirroring licence, and they are not the same. Buying the wrong one is the most common mistake at this stage
  4. Screen size — every coordinate depends on it

The first three come together once the environment is installed. The fourth needs saying separately: coordinates share the same pixel system as the mirroring view and screenshots, so measure from there, or use the node’s own coordinates. On USB HID you must set the screen size explicitly (usbHidEvent.setScreenSize(w, h)); without it, coordinate conversion has no basis and taps land wherever.


4. The skeleton of a first script

Whichever route you take, the opening is fixed. This can be copied as-is:

function main() {
    logd("Checking the automation environment...");
    if (!autoServiceStart(3)) {
        logw("Automation service failed to start, cannot run");
        exit();
        return;
    }

    // Set node fetch parameters once, at the top
    setFetchNodeParam({
        "labelFilter": "2",       // only nodes that have a label
        "visibleFilter": "2",     // only visible = true nodes
        "maxDepth": "20",         // fewer levels is faster, 1-500 advised
        "excludedAttributes": "visible,selected,enable,accessible"
    });

    // ... your logic

    logd("Script finished");
}

// Environment check: the pattern used in the official examples
function autoServiceStart(time) {
    for (let i = 0; i < time; i++) {
        if (isServiceOk()) {
            return true;
        }
        let started = startEnv();
        logd("Service start attempt " + (i + 1) + ": " + started);
        if (isServiceOk()) {
            return true;
        }
    }
    return isServiceOk();
}

main();

On autoServiceStart : it is not a built-in. It is the helper pattern from the official examples — call startEnv() in a loop and check isServiceOk(), up to time attempts. That beats a sleep(5000) and a hope, and is worth copying.

On setFetchNodeParam : this parameter table affects performance more than you would guess. The documentation says excludedAttributes “increases fetch speed” — with many devices, skipping unused attributes adds up. maxDepth is best kept at 1–500, smaller being faster.

On USB HID , the opening is the HID session instead:

let r = usbHidEvent.sessionStart(true);   // parameter: try enhanced compatibility, default true
if (!(r == null || r === "")) {
    logw("Failed to open HID session: " + r);
    return;
}
r = usbHidEvent.setScreenSize(1170, 2532);  // coordinate conversion depends on this

Remember the return convention : null or an empty string means success, any other string is an error message. Every check in this article is built on it, and it is the detail beginners most often invert.


5. The function map: five groups cover it

The documentation lists a lot of functions, but a few dozen carry the daily work.

Environment and service

Function Purpose
isServiceOk() Whether the automation service is ready
startEnv() / closeEnv() Start / stop the runtime environment
isDeviceOnline() Whether the device is online
isDeviceAuthOk() / getDeviceAuth() Licence state
sleep(ms) Pause execution
logd / logi / logw / loge Four log levels

Interaction

Identical method names across the four event modules, listed above. Proxy mode also has agentEvent.touchDown / touchMove / touchUp for custom gestures.

Nodes — the most-used group

Node queries are chained, not passed a config object:

// Find by text (regex), wait up to 5 seconds
let nd = labelMatch("发布").getOneNodeInfo(5000);
// Find by id
let nd2 = id("com.example.app:id/btn").getOneNodeInfo(3000);
// Get all matches
let list = labelMatch(".*订单.*").getNodeInfo(5000);

Available filters: id and idMatch, label and labelMatch, name and nameMatch, type and typeMatch, value and valueMatch, xpath, plus attribute matches such as visible and enable.

A node exposes id, xpath, label, name, type, value, bounds, index, depth, visible, enable, and the methods clickCenter, clickRandom, parent, child, allChildren, siblings, previousSiblings, nextSiblings.

Call releaseNode() then lockNode() before each query — the official examples do this consistently. Release the previous data, lock the current UI, or you may read nodes from the previous screen.

Text input

Proxy mode goes through the IME module:

if (imeApi.isOk()) {
    let r = imeApi.input("text to enter");
    logd("Input result: " + r);   // empty string means failure, non-empty is what was entered
}
imeApi.setClipboard("#hashtag");   // long text is steadier via the clipboard
imeApi.paste();
imeApi.dismiss();                  // do not forget this

imeApi.dismiss() is the step most often skipped. Leave the keyboard up and it occupies the lower half of the screen, covering whatever button sits beneath it. Your tap lands on the keyboard, and the failure raises no error — it just never takes effect.

USB HID input is a separate module: usbHidEvent.inputText() pastes via the clipboard, and usbHidEvent.typeText() types through the keyboard, falling back to paste for non-English.

Image, screenshot and device

Function Purpose
image.captureFullScreen() Full-screen screenshot
image.findImage(...) / findImageByColor(...) Template and colour matching
image.cmpColor(...) Colour comparison
device.getDeviceInfo() Device info, including live screen width and height
device.getDeviceId() / getDeviceName() / getModel() / getOSVersion() Identity and version
device.getBattery() / isCharging() Power
device.applist() Installed applications

When a node cannot be found, image matching is the fallback; when you need evidence, the screenshot is standard equipment.


6. Getting coordinates right

Coordinate problems account for about half of beginner failures, so they deserve their own section.

Prefer the node’s own coordinates; do not measure by hand. A node carries bounds, so compute the centre:

let nd = labelMatch("确认").getOneNodeInfo(5000);
if (nd) {
    clickPoint(nd.bounds.centerX(), nd.bounds.centerY());
}

The advantage is that nothing needs changing when the model or resolution changes — the coordinate is computed from the current device.

When hard coordinates are unavoidable, three things matter:

  1. The basis is the pixel coordinates of the mirroring view or screenshot, not logical resolution
  2. After a resolution change or a rotation, set the screen size again — setScreenSize(w, h) on USB HID, with equivalent calls on Bluetooth and OTG
  3. Portrait and landscape are two coordinate systems; mixing them requires switching with adjustScreenOrientation rather than hoping

If coordinates can be avoided, avoid them. One rule of thumb: if a position can only be reached by coordinate, it is probably a variable-length list or an icon — exactly the place an app update breaks first.


7. Wait for the element, not the clock

This is the dividing line between a script that holds up and one that does not.

A fixed wait is disconnected from reality: it wastes time on a fast network and is too short on a slow one. Tolerable as a stopgap, never as a design.

The right move is to let the query function do the waiting. The timeout inside getOneNodeInfo(timeout) is your element wait: pass 15000 for “up to 15 seconds”, and it returns the moment the element appears.

// Poor
sleep(3000);
clickPoint(585, 2280);

// Better
let btn = labelMatch("下一步").getOneNodeInfo(15000);
if (btn) {
    clickPoint(btn.bounds.centerX(), btn.bounds.centerY());
} else {
    logw("Not found within 15s - the UI may have changed");
}

To wait for an element to disappear, poll in a loop with a short sleep (300–500ms per step, a few dozen iterations) rather than one long sleep.


8. Debugging with three layers

When a script fails, work through these in order. It is faster than staring at the code.

Layer one: logs. One logd("step name") before each critical action costs a line and buys you the location of every failure.

Layer two: screenshots. Add image.captureFullScreen() to failure branches. Most failures stop on an unexpected screen — a popup, an update prompt, an expired session. The screenshot usually settles it.

Layer three: the real-time log and execution history in the central control. The first shows per-device output step by step; the second aggregates results across devices and shows which one failed and where. It is the entry point for multi-device problems.

A habit worth forming: run a new script on one device, and make the first version do the shortest possible thing. Start with “open the app and confirm the home screen appeared”, then add. Writing the whole flow at once leaves you unable to tell an environment problem from a locating problem from a logic problem.


9. From one device to many

Once it runs on one device, rolling out is direct:

  1. Put the compiled iec file into the script directory at the bottom left of the central control, right-click, refresh
  2. Select the target devices — or group them in the group bar first — right-click the script and run it

Grouping stops being a nicety and becomes a requirement past ten devices. Dozens of devices on one screen means you cannot see which one failed; in groups, the answer is visible at a glance.

One boundary bears repeating: running scripts requires a USB device licence, and mirroring requires a separate USB mirroring licence. Also, one device runs only one task at a time, so throughput comes from device count rather than concurrency.

This is also the line between iOS cluster control and “writing a few more scripts”. A script answers how to do one thing; cluster control answers how to do the same thing correctly across dozens of devices, and how to know which one did not.

When an external system needs to trigger the work — an e-commerce back office, for instance — the open API is the route: JSON in and out, callable from Python, Node or cURL. That is a separate article in itself; the entry point is port 8019 on the central control.


10. Three things to do once it runs

Engineer it. A long script in a single file gets hard to maintain. The documentation covers npm and TypeScript support, so you can organise the code properly.

Protect it. Once a script goes to someone else, the source is the asset. There is dedicated documentation on JavaScript obfuscation to run before distribution.

Parameterise it. Move accounts, keywords and timings into configuration rather than hard-coding them. This decides how long your script lasts: one that needs the source re-read after every change is one nobody maintains.


11. Which setup to pick, by situation

If you want the conclusion without the ten sections above, this is it. Five situations cover most teams, and all five map to different entry points inside one central control.

Your situation Recommendation Why
iOS 13–16, or a complex flow needing precise element targeting EasyClick proxy mode The most capable route, with a node selector and the easiest debugging
iOS 17+, want to drop signing cost, stable interface EasyClick with USB HID One cable, no signing, no board, and a gentler risk profile
iOS 17+, real risk-control pressure, or devices spread out EasyClick with Bluetooth HID Friendliest risk profile, no cable-length limit
You would rather not write code at all EasyClick AI agent Describe the task in plain language, or drag out a workflow
Your own system needs to trigger device actions EasyClick open API Port 8019, HTTP plus JSON, callable from any language

All five share one thing: the same central control, the same licences, the same devices. So in practice it is rarely “pick one” — the usual combination is to prove the logic on proxy mode, move to HID once risk pressure appears, hand ad-hoc work to the AI agent, and run fixed batch work on a script with a schedule.

In one line: choosing the platform matters more than choosing a single route. A single route decides how one particular job is done; the platform decides whether these routes and entry points can share one set of devices and licences. Every row in that table is an entry point into the same platform rather than a separate tool to learn.

12. Common questions

Can a no-jailbreak script handle logged-in work?

Yes. Logging in is application-level and unrelated to jailbreak. What matters is that sessions expire, so the script needs a branch for “asked to log in → run the login flow”.

Should I start with the most capable route?

No. The routes differ in what they can do, but the business logic is the same one. Validate the logic on the easiest route to get running, then move for risk or scale reasons — changing only the event module.

How long should the waits be?

Generous, not tight. getOneNodeInfo(15000) does not actually wait 15 seconds; it returns when the element appears. Set the ceiling at 3000 and a slightly slow network fails the step. Users do not begrudge fifteen seconds, but they do mind unexplained failures.

Why does the same script fail on another device?

Three usual causes, in this order: different resolution with hard-coded coordinates, a different iOS version with UI differences, or that device’s automation service and licence state.


The bar in scripting is not the language. That operator started by writing only “open the back end, wait for the order list, take a screenshot”, and added actions once it ran. Four hours later he had a working script; three of his four days had gone into not knowing where to begin.

Get the environment and service checks right, pick the shortest single action, and add steps from there. iPhone automation costs less to start than most people assume; the hard part is keeping it steady.

To see how a specific route plays out, read choosing between the three iOS no-jailbreak routes; for USB HID internals and the API, see USB HID explained with the full API; for a complete worked example, read writing a short video publishing script.

About EasyClick: A phone automation AI-agent platform covering Android no-root, iOS no-jailbreak (proxy / Bluetooth HID / OTG HID) and HarmonyOS Next, offering script development, Apple cluster control, local central control & mirroring, and cloud control systems. → Explore all products

Ready to build it for real?

Every approach in this article can be built on the EasyClick phone automation platform — full documentation, developer tools and cluster/cloud-control products, free to try.

Visit EasyClick →