Longarm Batch Tasks
Batch tasks let longarm save reusable multi-step workflows and run them
later from the app, REST API, or MCP.
Key behavior
- one batch run can be active at a time
- run requests return immediately with a
runId - progress is checked through status and history
- screenshots captured by
screenshotsteps are stored in batch history - default history retention is
30runs
Recorded tasks
Stopping a recording in the Batch tab creates a normal batch task containing the classified tap, long-press, swipe, and delay steps. A recorded task is not a special immutable source — it can be edited, duplicated, deleted, run, or updated through the same app, REST, and MCP operations as any manually created task.
When UI inspection is enabled in settings at recording start, each recorded
gesture is preceded by a ui_context annotation describing the UI state
right before that gesture. It is shown as read-only JSON in the app and
preserved in saved tasks, JSON exports/imports, and REST/MCP task data. The
runner skips the entire annotation during execution — it contributes no
execution count or step log, and the original coordinate gesture is
unchanged. AI editors can use the annotation’s node text, viewId, class,
bounds, state, and actions to refine a recorded gesture into a semantic
target.
With UI inspection disabled at recording time, recording saves gestures only, with no annotation. Older saved tasks with legacy recording metadata load as ordinary batch tasks; the obsolete metadata is dropped the next time the task is saved.
Task JSON shape
Top-level task:
{
"id": "bt_...",
"name": "Open Settings and capture",
"steps": [],
"repeat": 1,
"variables": {},
"createdAt": "2026-07-13T00:00:00.000Z",
"updatedAt": "2026-07-13T00:00:00.000Z"
}
For POST /api/batch and batch_save, provide name, steps, optional
repeat, and optional variables. The server can assign IDs and
timestamps.
Step JSON shape
{
"id": "step_1",
"type": "tap",
"params": { "x": 540, "y": 1200, "duration": 80 },
"repeat": 1,
"delayAfterMs": 0,
"continueOnError": false,
"children": []
}
children holds nested steps and is used by loop.
Supported step types
Free:
taplong_pressscreenshotopen_appopen_intentapp_function_queryapp_function_calloverlay_showoverlay_hidedelaysetui_actionset_textui_scrollwait_for_uiloop
Plus:
swipepinchtwo_finger_swiperotate
Common examples
Tap
{
"type": "tap",
"params": { "x": 540, "y": 1200, "duration": 80 }
}
Target-based tap:
{
"type": "tap",
"params": {
"target": { "text": "Sign in", "role": "button" },
"mode": "semantic_then_gesture"
}
}
Swipe (Plus)
{
"type": "swipe",
"params": {
"startX": 540,
"startY": 1800,
"endX": 540,
"endY": 600,
"duration": 700
}
}
Screenshot
{
"type": "screenshot",
"params": {
"gridSize": 1,
"gridUnit": "cm",
"scale": true
}
}
Open app
{
"type": "open_app",
"params": {
"packageName": "com.android.settings"
}
}
Delay
{
"type": "delay",
"params": { "ms": 1500 }
}
Query and call an AppFunction
app_function_query can save its complete discovery result into a batch
variable, and app_function_call can reference it and optionally save the
decoded return value:
{
"steps": [
{
"type": "app_function_query",
"params": {
"packageName": "com.example.notes",
"maxResults": 20,
"resultVariable": "availableFunctions"
}
},
{
"type": "app_function_call",
"params": {
"packageName": "com.example.notes",
"functionId": "{{availableFunctions.functions.0.functionId}}",
"arguments": { "title": "Groceries" },
"resultVariable": "createdNote"
}
}
]
}
These steps do not require accessibility. They still require Android
AppFunctions support and Android authorization for longarm and the target
function.
UI action
{
"type": "ui_action",
"params": {
"selector": { "text": "Continue" },
"action": "click",
"fallback": "none"
}
}
Wait for UI
{
"type": "wait_for_ui",
"params": {
"selector": { "text": "Welcome" },
"condition": "exists",
"timeoutMs": 5000,
"stableForMs": 0
}
}
Variables
Use {{name}} substitution inside string params:
{
"variables": {
"packageName": "com.android.settings"
},
"steps": [
{
"type": "open_app",
"params": { "packageName": "{{packageName}}" }
}
]
}
A param whose entire value is a single {{name}} placeholder preserves the
variable’s original JSON type (number, boolean, object) instead of
converting it to a string. Nested lookups are also supported, such as
{{user.name}}.
Loops
loop steps execute their children, and support four modes:
Fixed count:
{
"type": "loop",
"params": { "count": 3 },
"children": [
{ "type": "tap", "params": { "x": 540, "y": 1800 } }
]
}
Numeric range:
{
"type": "loop",
"params": { "var": "i", "from": 1, "to": 5, "step": 1 },
"children": [
{ "type": "delay", "params": { "ms": 250 } }
]
}
For-each over a list:
{
"type": "loop",
"params": {
"var": "pkg",
"in": ["com.android.settings", "com.android.chrome"]
},
"children": [
{ "type": "open_app", "params": { "packageName": "{{pkg}}" } }
]
}
While-like loop:
{
"type": "loop",
"params": { "whileVar": "keepGoing", "max": 10 },
"children": [
{ "type": "delay", "params": { "ms": 500 } }
]
}
Notes: loop nesting depth is limited, a loop with step: 0 is invalid, and
whileVar loops require max.
Full example
{
"name": "Open Google News and scroll",
"variables": {
"packageName": "com.google.android.apps.magazines"
},
"steps": [
{
"type": "open_app",
"params": { "packageName": "{{packageName}}" },
"delayAfterMs": 2500
},
{
"type": "swipe",
"params": {
"startX": 540,
"startY": 1900,
"endX": 540,
"endY": 650,
"duration": 700
}
},
{
"type": "screenshot",
"params": { "gridSize": 1, "gridUnit": "cm", "scale": true }
}
]
}
REST operations
Save and list tasks:
GET /api/batchPOST /api/batchGET /api/batch/<id>PUT /api/batch/<id>DELETE /api/batch/<id>
Task validation requires a name and a non-empty steps array.
Run a task — saved task with POST /api/batch/<id>/run, or an inline unsaved
task with POST /api/batch/run. Both accept an optional keepScreenOn
boolean (JSON body or ?keepScreenOn=true query parameter). When true,
longarm holds the screen on for the whole run and releases it as soon as
the run finishes, fails, or is stopped — useful for long runs that would
otherwise hit the device’s screen timeout. Pressing the power button still
turns the screen off. This matches the Screen on item in a task’s ⋯
menu in the app; it defaults to false for API-triggered runs.
{ "runId": "run_123", "status": "processing" }
If another run is already active, the server returns HTTP 409 Conflict
with { "error": "Another batch task is already running" }.
Runner status — GET /api/batch/status:
{
"state": "running",
"currentTaskId": "bt_123",
"currentRunId": "run_123",
"currentStepIndex": 2,
"totalSteps": 5
}
state is one of idle, running, stopping.
Batch history
List history with GET /api/batch/history, or one run with
GET /api/batch/history/<runId>:
{
"runId": "run_123",
"taskId": "bt_123",
"taskName": "Open Google News and scroll",
"status": "finished",
"startedAt": "2026-07-13T00:00:00.000Z",
"endedAt": "2026-07-13T00:00:05.000Z",
"totalSteps": 3,
"executedSteps": 3,
"message": "Completed",
"screenshots": [
{
"id": "shot_1",
"stepId": "step_3",
"stepIndex": 3,
"capturedAt": "2026-07-13T00:00:04.000Z"
}
]
}
Run status values: processing, finished, failed, aborted.
Delete history with DELETE /api/batch/history/<runId> (one run) or
DELETE /api/batch/history (all runs).
Screenshot export requires Plus:
GET /api/batch/history/<runId>/screenshots/<screenshotId>— single screenshotGET /api/batch/history/<runId>/export— all screenshots as a zip
MCP equivalents
The same batch capabilities are available through MCP tools: batch_list,
batch_get, batch_save, batch_delete, batch_run, batch_run_inline,
batch_status, batch_history_list, batch_history_get,
batch_history_export, batch_history_delete.