Skip to main content

Custom Scripts

The built-in scripts cover the common flows. When you need something they do not — a step in a different order, a screen they never touch, or an app that is not TikTok or Instagram — you can write it yourself in any language and let TikMatrix hand you the phone.

Requirements

License Requirement

Custom scripts require a Pro, Team, or Business plan. Starter plan does not have access.

Your plan's device count is also the concurrency limit: a Pro plan (20 devices) can drive 20 phones at once, whether through built-in tasks, custom scripts, or a mix of both.

Two ways to run a script

Standalone

You run your program yourself. TikMatrix just lends you devices.

from tikmatrix import TikMatrix

client = TikMatrix()

for device in client.devices():
if device["busy"]:
continue
with client.device(device["serial"], label="my crawler") as d:
d.press("home")
print(d.info())

Good for one-off jobs, data collection, and anything you want to run from your own scheduler.

Managed

You register the program in TikMatrix and it becomes a task like any other. It gets the task queue, per-plan concurrency, automatic retries, the task log, and schedule templates. TikMatrix leases the device before starting your program and passes the lease id in its environment.

from tikmatrix import TikMatrix

with TikMatrix.from_env() as d: # device already leased
d.click(text="Log in")
print("done") # this line lands in the task log

Good for anything you want to run repeatedly, on a schedule, or across many devices.

Which one to pick

StandaloneManaged
Who starts itYouThe TikMatrix task queue
Device leaseYou acquire itAlready held when your program starts
Retries, schedules, task logYou build themIncluded
Runs across many devicesYou loop yourselfOne task per device, dispatched in parallel
Best forExploration, crawlers, one-off jobsAnything you want to repeat

You can start standalone while you get the flow working, then register the same file as a managed script — TikMatrix.from_env() is the only line that changes.

Getting started

1. Install the client library

pip install requests

Then copy tikmatrix.py from the SDK directory next to your script. The library is a single file with no other dependencies.

You do not have to use it — the API is plain JSON over HTTP, and the raw endpoints are documented below.

2. Write your script

from tikmatrix import TikMatrix

client = TikMatrix()
with client.device("192.168.1.5:5555") as d:
d.press("home")
d.adb("shell", "am", "start", "-a", "android.settings.SETTINGS")
d.wait_for(text="Settings", timeout=15)
d.screenshot("settings.png")

Run it with TikMatrix open and the phone connected. If it prints a device info dictionary, everything is wired up.

3. Register it (managed mode only)

Go to Devices → Custom Scripts → Add Script:

FieldMeaning
NameShown in the script list and the task log
CommandThe program line to run, e.g. python C:/scripts/my_flow.py
Working directoryOptional. Where the program starts
PlatformSee platform modes below
TimeoutSeconds before the script is killed and the task marked failed. Default 1800
Extra environment variablesOptional JSON object merged into the program's environment
EnabledTurn a script off without deleting it. A disabled script cannot be dispatched

Then press ▶ on the script row and pick your devices, exactly like a built-in script.

Let the assistant write it for you

The AI Assistant can draft a custom script from a plain-language description and register it in one step. It shows you the whole file before anything is written to disk.

Device leases

A phone can only be driven by one thing at a time. Leasing it tells TikMatrix the device is busy, so:

  • the task queue will not dispatch a task onto the same screen, and
  • your JSON-RPC calls report agent health exactly as a built-in script does, so the watchdog sees a busy agent rather than a silent one.

A lease also consumes one device slot from your plan.

Leases expire — 120 seconds by default, 600 maximum. The Python library renews yours on a background thread and releases it when the with block ends, so a crashed script frees its device within seconds instead of holding it until you restart the app. If you are calling the API directly, you must send heartbeats yourself.

You can see every live lease, and force-release one, under Settings → Developer API → Active device sessions.

Platform modes

A registered script declares what it targets:

Generic — the device is handed over untouched. No app is started, no account switching, no input-method check, and nothing is closed afterwards. Use this to automate anything that is not TikTok or Instagram.

TikTok / Instagram — the app is opened and the account switched before your program starts, and the app is closed when it finishes, exactly as for a built-in script. TIKMATRIX_PACKAGE tells you which package was resolved. Use this to add a step the built-in scripts do not cover.

Environment variables

A managed script receives:

VariableMeaning
TIKMATRIX_API_BASEServer URL, e.g. http://127.0.0.1:50809
TIKMATRIX_SESSION_IDThe lease already held on your behalf
TIKMATRIX_SERIALThe device this task was dispatched to
TIKMATRIX_PACKAGEResolved app package
TIKMATRIX_PLATFORMtiktok, instagram, or generic

TikMatrix.from_env() reads all of these for you.

Standalone scripts get none of them — lease a device explicitly instead.

Anything you put in Extra environment variables is merged on top, which is the usual way to give one registered script per-run settings without editing the file.

Python library reference

TikMatrix — the connection

CallWhat it does
TikMatrix(base_url=None, timeout=30.0)Connect. Falls back to TIKMATRIX_API_BASE, then http://127.0.0.1:50809
client.devices()Online devices, each with serial, real_serial and busy
client.sessions()Every live lease, including ones this process does not own
client.device(serial, label=..., ttl_secs=120)Lease a device and return a Device
TikMatrix.from_env()Adopt the device a managed script was started with

Device — the phone

CallWhat it does
d.info()UIAutomator2 device info
d.window_size()(width, height)
d.screenshot(path=None)PNG bytes, optionally written to path
d.hierarchy()The current UI tree as XML
d.find(text=, resource_id=, description=, class_name=)Matching nodes, each with bounds and center
d.exists(**criteria)Whether anything matches
d.wait_for(timeout=10.0, interval=1.0, **criteria)Block until it appears, then return it
d.click(timeout=10.0, **criteria)Wait for an element, then tap its centre
d.click_xy(x, y)Tap a coordinate
d.swipe(sx, sy, ex, ey, steps=20)Swipe
d.press(key)back, home, recent, enter, …
d.input_text(text)Type into the focused field via the bundled fast-input IME
d.jsonrpc(method, params=None, timeout=10)Any UIAutomator2 method
d.adb(*args, timeout_ms=None)Run an ADB command
d.release()Release the lease. with does this for you

find matches against the dumped UI tree, so when a selector misses you can print(d.hierarchy()) and look at exactly what it searched. The Element Inspector in the device view shows the same tree visually, which is usually the fastest way to find a resource-id.

input_text needs ADB

It sends a broadcast to the bundled input method, which goes through adb shell. Enable ADB access before using it, or it fails with 403.

Errors

The library raises two exceptions, both subclasses of RuntimeError:

ExceptionWhen
DeviceBusyErrorHTTP 409 — the device is already leased, or your plan has no free device slot
TikMatrixErrorEverything else: plan too low, lease expired, ADB disabled, selector never matched
from tikmatrix import TikMatrix, TikMatrixError, DeviceBusyError

client = TikMatrix()
try:
with client.device("192.168.1.5:5555") as d:
d.click(text="Log in", timeout=20)
except DeviceBusyError:
print("someone else has that phone — try another one")
except TikMatrixError as exc:
print("failed:", exc)

In a managed script, letting the exception escape is usually the right thing to do: the non-zero exit marks the task failed and the traceback lands in the task log.

HTTP endpoints

Device operations need an x-session-id header naming a live lease. There is no API key: like the rest of the local API, these endpoints are unauthenticated — reaching the machine on the network is the access control. They send no CORS headers, so call them from a program (curl, Python, anything server-side) rather than from a page in a browser.

MethodPathPurpose
GET/api/v1/rpc/devicesList online devices and whether each is busy
POST/api/v1/rpc/sessionLease a device → session_id
POST/api/v1/rpc/session/{id}/heartbeatExtend the lease
DELETE/api/v1/rpc/session/{id}Release the lease
GET/api/v1/rpc/sessionList live leases
POST/api/v1/rpc/jsonrpcCall a UIAutomator2 method
POST/api/v1/rpc/adbRun an ADB command
GET/api/v1/rpc/hierarchy?serial=Current UI tree as XML
GET/api/v1/rpc/screenshot?serial=Current screen as PNG

JSON replies use the same envelope as the rest of the local API — {"code": 0, "message": "success", "data": ...}, with a non-zero code on failure. hierarchy and screenshot return the raw body instead.

Example

# Lease a device
curl -X POST http://127.0.0.1:50809/api/v1/rpc/session \
-H "Content-Type: application/json" \
-d '{"serial":"192.168.1.5:5555","label":"curl test","ttl_secs":120}'

# {"code":0,"message":"success","data":{"session_id":"ff3ae079-...","serial":"192.168.1.5:5555", ...}}

# Drive it
curl -X POST http://127.0.0.1:50809/api/v1/rpc/jsonrpc \
-H "x-session-id: ff3ae079-..." \
-H "Content-Type: application/json" \
-d '{"serial":"192.168.1.5:5555","method":"deviceInfo","params":[]}'

# Keep it alive while you work
curl -X POST http://127.0.0.1:50809/api/v1/rpc/session/ff3ae079-.../heartbeat \
-H "Content-Type: application/json" \
-d '{"ttl_secs":120}'

# Give it back
curl -X DELETE http://127.0.0.1:50809/api/v1/rpc/session/ff3ae079-...

Errors

StatusMeaning
403Plan below Pro, no lease, lease expired, or ADB access disabled
409Device already leased, or your plan has no free device slot

Writing in another language

Nothing here is Python-specific. Any runtime that can make an HTTP request works — the managed-mode contract is only "read three environment variables, exit 0 on success".

// my_flow.js — register with: node C:/scripts/my_flow.js
const base = process.env.TIKMATRIX_API_BASE || "http://127.0.0.1:50809";
const serial = process.env.TIKMATRIX_SERIAL;
const session = process.env.TIKMATRIX_SESSION_ID;

async function jsonrpc(method, params = []) {
const res = await fetch(`${base}/api/v1/rpc/jsonrpc`, {
method: "POST",
headers: { "content-type": "application/json", "x-session-id": session },
body: JSON.stringify({ serial, method, params }),
});
const body = await res.json();
if (!res.ok || body.code !== 0) throw new Error(body.message || res.statusText);
return body.data;
}

console.log(await jsonrpc("deviceInfo"));

If the interpreter is not on PATH, give its full path in Command, e.g. C:/Program Files/nodejs/node.exe C:/scripts/my_flow.js.

Triggering a custom script from the API

Registered scripts can also be started through the Task Management API, so one script can queue follow-up work:

curl -X POST http://127.0.0.1:50809/api/v1/task \
-H "Content-Type: application/json" \
-d '{
"serials": ["192.168.1.5:5555"],
"script_name": "custom_script",
"script_config": {
"custom_script_id": 1,
"custom_script_platform": "generic"
}
}'

custom_script_id is the id of the script you registered.

ADB access

/api/v1/rpc/adb gives your scripts a device shell — you need it for pushing media, installing APKs, and changing system settings. Because it is a full shell on an endpoint that has no API key, it ships turned off. Enable it under Settings → Developer API → Allow ADB commands when you have a script that needs it; UI automation over /rpc/jsonrpc works without it.

While it is off, /api/v1/rpc/adb answers 403 and the rest of the API keeps working. Every ADB command a script runs is written to your log file.

Writing scripts that keep working

  • Wait for the screen, do not sleep for it. d.wait_for(...) returns as soon as the element is there; a fixed sleep is either slower than it needs to be, or too short on a bad day.
  • Check before you tap. d.exists(...) on a consent dialog or a "not now" prompt costs one hierarchy dump and saves a run that would otherwise tap into nothing.
  • Print what you did. In managed mode stdout is the task log, and it is the only record of a run nobody was watching.
  • Make a rerun safe. A retry re-runs the whole program, so a script that posts should check whether it already posted rather than assuming it starts from scratch.
  • Keep one script to one job. Concurrency is per device, so ten small tasks across ten phones finish far sooner than one script looping over ten phones.

Notes and limits

  • The command is executed directly, not through a shell, so && and | are treated as arguments rather than operators. Register cmd /c "..." (Windows) or sh -c "..." (macOS) if you want shell behaviour.
  • Quote paths that contain spaces: "C:/Program Files/Python/python.exe" my_script.py.
  • A script that exceeds its timeout is terminated and the task is marked failed.
  • A non-zero exit code marks the task failed; everything the script writes to stdout and stderr lands in the task log.
  • Scripts run with the same permissions as TikMatrix itself. Only register programs you wrote or trust.

Troubleshooting

API access requires Pro or higher plan (403) The licence on this machine is Starter or inactive. Check Settings → License.

Connection refused on 127.0.0.1:50809 TikMatrix is not running, or it is running as a different user. The server exists only while the app is open.

409 on every lease attempt Either the phone is genuinely busy — check Settings → Developer API → Active device sessions — or every device slot in your plan is already taken by running tasks.

The lease expires in the middle of a long step The default TTL is 120 s and the library renews it in the background, so this usually means the script blocked its main thread for longer than the TTL. Raise ttl_secs (up to 600), or move the long work off that thread.

d.adb(...) fails with 403 ADB access is off. Turn it on under Settings → Developer API → Allow ADB commands.

A selector never matches print(d.hierarchy()) shows the exact tree find searched. Text is matched exactly, so a trailing space or a localised label is the usual cause; matching on resource_id is more stable than on text.

The task is marked failed but the phone looks fine Read the task log. A non-zero exit — including an uncaught exception at the end of a successful run — fails the task even when the automation itself worked.

Next steps