FindNode User Guide · Version 21 · May 28, 2026 · Full reference
Introduction
FindNode finds UI elements on an Android screen with a small query language and then acts on them, so a script never needs screen coordinates. devices.click("OK") replaces devices.click(100, 200): FindNode locates the node with the text "OK" at runtime, and the same script runs on phones with different resolutions and screen sizes.
This page collects the 204 Terminal examples from the FindNode User Guide, version 21, with a short explanation of each. The User Guide remains the full reference.
How to read an example
A line that starts with >> is a command typed in the Terminal. The lines under it are the output.
Commands are sent with device.sendAai({query:"…", action:"…"}) for one device, or devices.sendAai(…) for several.
A query has up to three parts, run in this order: the template (TP), the basic query (BQ) and the expanded query (EQ). The nodes it finds are kept in the Matching List (ML), and actions work on that list.
A successful call returns JSON, usually with the value under retval. On an error it returns null, and lastError() gives the message.
Copy takes one command without the >>. Screenshots enlarge when clicked.
Chapter 1 of 10
Getting started
How commands are sent to FindNode, and what a query with an action looks like.
sendAai
Every FindNode command goes through sendAai(), called on one device (device) or on several (devices).
TP:basic is the template, T:Android version is the basic query that finds the label, and OY:1 is the expanded query that moves one node down to the value.
The About phone screen. The value sits under its label.
A query is a string of key:value pairs joined by &&, and a node must meet all of them to match. It has three parts: the template (TP) produces the first nodes, the basic query (BQ) tests each node's own properties, and the expanded query (EQ) works across several nodes.
Overview
Four ways to read values from one Storage Analysis screen.
The parser splits the query into template (TP:all), basic query (T:Main storage) and expanded query (VG:2&&XT:Audio&&OX:1), and runs them in that order to reach the Audio size.
The first command saves the IDs of the digit buttons 1 to 9. The second passes them as elements, clicks each one with forEach(nsClick), and reads the result field.
A template (TP) is required in every query. It produces the starting list of nodes, the Matching List (ML), that the rest of the query and every action work on. Only one template is allowed per query. If none is given, the default TP:more is used.
Example: Template Limits the Query Scope
A query can only reach nodes that its template returned. Offsets and all other operations stay inside that list.
TP:findText,/^[0-9]$/ returns only the digit keys. OX:1 from "3" points at "%", which is not in that list, so the result is null and lastError() gives the reason.
UI Explorer with TP:findText,/^[0-9]$/. Only the digit keys are outlined.
These four templates go through every node on the screen and differ in how many they keep. all keeps everything, including the root node. more removes layout nodes and is the default. basic keeps only nodes with no children. reduced keeps the nodes FindNode judges most meaningful.
getCount counts the current ML: 237 with the default TP:more. +TP:all replaces the ML, so the third count is also 242. The last three counts are for more, basic and reduced.
TP:reduced (a template), RN (a query filter) and reduceNodes (an action) perform similar node reduction in different places. This screen has 59 nodes, and each of the three brings the list down to 44.
Returns the nodes whose text (anyText) or description (anyDescription) is not empty. An optional minimum and maximum length, both inclusive, narrow the result. Zero is not allowed.
TP:anyText returns all nodes that have text, and getText reads them. On the calculator this lists the title, the function keys, and every digit and operator.
Returns the nodes whose text or description matches the given text. The text argument is required and accepts the same forms as T:<text> in a basic query.
A regular expression matches the calculator's digit keys, which come back top to bottom. The second command matches one to four capital letters, and the wildcard *T returns the entries ending in T.
Returns the editable nodes (text fields), optionally only the ones with the given hint text. Hint text needs Android 8.0 or later. BP:editable gives the same result.
The first command reads the text of all five text fields. The second reads their hint text, which is supported from API 26. The third selects the field whose hint is "Email" and types an address into it.
Groups the fixed elements above or below the scrollable area into lines and returns one line. Line numbers start at 1. Negative numbers count from the end, so -1 is the last line. This is a best-effort feature and mainly applies to apps with a scrollable container.
TP:line,bottom,-1 returns the last line of the bottom region, which has five nodes. The second command picks the editable node on that line and sets its text to Hello.
Introduced in version 20. Returns the icons and labels of an app's bottom navigation bar: icons first, then labels. Add icons or labels to get only one kind. BP:selected finds the selected tab in most apps.
The bar has four tabs, so TP:appNavBar returns eight nodes: four icons, then four labels. A tab can be clicked by its label text or by its icon with IX:3, and BP:selected finds the selected tab.
The "+" icon covers both the icon row and the label row, so its node (21d62) appears twice. To get only the real labels, keep the nodes that have text, with T:/./ or filter(node.text).
The "+" icon spans the height of both icons and labels.
In this app each tab is a single node that holds both the icon and the label, so the bar returns three nodes. A tab can still be clicked by position with IX:, not by text with T:.
Three tabs, each one node, as outlined in UI Explorer.
Returns the on-screen items (leaf nodes) of one scrollable area. Choose the area by position, counted from 0 from top to bottom, or by a query that matches it. BP:scrollable, by contrast, matches every scrollable area on the screen.
Three actions in one call: open Android Settings, query the titles (android:id/title) inside the scrollable area, and read their text. The result has one entry per action.
The drawer opens as a popup among several scrollable areas. It can be selected by position (2) or by a query that matches something inside it (T:Sent). Both return the same 39 nodes.
The menu icon (three lines) opens the navigation drawer.The open drawer in UI Explorer with TP:more.The drawer identified with TP:scrollableAsPopup,2.
Three ways to read the clock (R:.clock) of com.android.systemui. An optional query does not support P, so the third form selects the app with newQuery first.
T matches text, D matches the description, and TD matches either. T0 is T with BP:none, T1 is T with BP:!editable, and TN normalizes hyphens and spaces before matching.
The text on screen uses another Unicode hyphen, so T:*Wi-Fi* finds nothing. TN converts such hyphens and spaces to the keyboard characters and finds both entries.
{count: 2, retval: ['Enable Wi‑Fi Verbose Logging','Increase Wi‑Fi logging level, show per SSID RSSI in Wi‑Fi Picker']}
Bound In (BI):
BI returns nodes by screen position: those that contain a point, touch a horizontal or vertical line, or lie inside a rectangle. Coordinates do not carry over between devices, so this is mainly for debugging.
BI:[-1,645] returns the nodes that a horizontal line at y = 645 touches, and IX:-1 picks the last one to click. BI:[0, 0, 1000, 1000] returns the nodes inside that rectangle.
Matches nodes by their Boolean properties, such as clickable, editable or checked. List several separated by commas, and put ! in front of one to match false.
The arrow is disabled, so BP:enabled returns null and lastError() reports no match. The last command reads the labels in the same row as "Day" with TX.
A query with no match returns null, and lastError() shows the message. An unknown key such as X gives a different error.
>>device.sendAai({query:"T:X", action:"getText"})
null
>>lastError()
No match found
>>device.sendAai({query:"X:X", action:"getText"})
null
>>lastError()
Error:Cannot find the key for "X"
Chapter 5 of 10
Expanded query in detail
Expanded query (EQ) keys work across several nodes: offsets, groups, intersections and sorting. They run from left to right, and the same key can be used more than once.
Index (IX) M O
IX:<number> picks one node from the ML by position, starting at 0. A negative number counts from the end.
VG goes up from a node to the group that encloses it and puts the whole group in the ML, with the group node first. VG:<level> goes up more levels; the default is 1.
If the title lands on the last line of the screen, the rest of the option may be out of view. Adding VG scrolls the whole option into view, and the switch is then found with BP:checkable.
With VG, the group covers the title, the description and the switch.
A basic-query key written after an EQ key still runs first, as part of the BQ. To run it at that point of the EQ, wrap it in BQ:'…', prefix it with X, or turn on selector:allowBqAfterEq.
Actions do something with the nodes a query found, or work on their own without nodes. action takes one command, several commands separated by ;, or an array of commands.
Overview
When a call fails, sendAai returns null and lastError() holds the reason.
Action arguments can be strings, integers, doubles, booleans and null. Version 17 added arrays, objects and functions. They follow JavaScript conventions.
echo returns its argument, which shows how each type is read. Strings may be unquoted or in single or double quotes. Arrays and objects can be indexed with [n] and .key, also when nested.
>>device.sendAai({action:"echo(this is a string)"})
{retval: 'this is a string'}
>>device.sendAai({action:"echo('this is a string')"})
{retval: 'this is a string'}
>>device.sendAai({action:'echo("this is a string")'})
In the calculator only buttons are clickable, so node.isClickable leaves the same 20 nodes. The second command echoes the class and bounds of the first node.
When part of a query or action is only known at runtime, build the string in JavaScript: with concatenation, with template literals, or with the aaix and buildAction helpers.
A template literal lets the actions of a function span several lines. enterText sets the text, checks it, clicks the node two places to the right and sends Back.
echo returns the text with the escaped quote, commas and double quotes in place.
>>device.sendAai({action:"echo(John\\'s ball)"});
{retval: 'John's ball'}
>>device.sendAai({action:'echo(a\\, b\\, c)'});
{retval: 'a, b, c'}
>>device.sendAai({action:'echo(he said \\"it was me\\")'});
{retval: 'he said "it was me"'}
Optional Query (OQ)
An action can carry its own query (OQ). With no prefix it searches within the ML and leaves the ML unchanged. + runs a new query and builds a new ML. * searches within the ML and replaces the ML with the result.
The condition can be a literal or a function such as exists. When it is false the third argument runs; here error stops with a message. The last command uses node.editable as the condition.
The isTrue function echoes how its argument is judged. 0, an empty string, false and null are false. Non-zero numbers, non-empty strings, arrays and objects are true.
getAndroidVersion opens the settings and reads the version. ensureAndroidVersion compares it with its argument, 14 by default, and returns true or false.
The first two commands show the activity names for Gmail and Chrome. The last two save the current activity in a variable and open it again with openActivity.
getBoolProp returns which of a node's main Boolean properties apply: editable, progressible, clickable, checkable, scrollable or longClickable. getAllBoolProps returns all of the node's Boolean properties.
The first two commands count all nodes and the nodes whose text contains a digit. A query with no match returns null with an error, while the same query given inside getCount returns 0.
getFuncRetval() with no argument is the last command of testEcho, so calling the function returns the output of all five echo commands.
>>device.sendAai({action:"function(testEcho)", testEcho:`
echo(This is a test to echo in different types);
echo(true);
echo(1000);
echo(3.14159);
echo({a:1, b:{x:1, y:'string'}, c:[100, 200, 300]});
getFuncRetval()`})
{retval: true}
>>device.sendAai({action:"testEcho"})
{retval: [{retval: 'This is a test to echo in different types'},{retval: true},{retval: 1000},{retval: 3.14159},{retval: {a: 1, b: {x: 1, y: 'string'}, c: [100,200,300]}}]}
An invalid query sets the error. getLastError returns the same message, and after clearLastError it is empty.
>>device.sendAai({query:"Y:123"})
null
>>lastError()
Query Error: Invalid query type "Y"
>>device.sendAai({action:"getLastError"})
{retval: 'Query Error: Invalid query type "Y"'}
>>device.sendAai({action:"clearLastError"})
{retval: true}
>>device.sendAai({action:"getLastError"})
{retval: ''}
getNodes (M)
Returns information about nodes. The optional fields argument chooses what to return, for example T for text, R for resource ID, BP for Boolean properties, or all.
getPackageName, the same as getCurrentPackageName, returns the package of the running app. getOnScreenPackageNames returns the packages of all apps on the screen.
T:Controls&&TX returns the three nodes in the row. For each one, getUniqQuery builds a query that reaches it from the "Controls" text.
The row holds an icon, the text and an arrow.
>>device.sendAai({query:"T:Controls&&TX"}).retval.forEach(
x => print(device.sendAai({elements:[x],action:"getUniqQuery"}).retval)
)
T:Controls&&OX:-1
T:Controls
T:Controls&&OX:1
getSystemProp
Reads Android system properties, like getprop. A plain argument searches names and values. An argument starting with = returns the value of that exact key.
newQuery runs a new query and rebuilds the ML. addQuery searches within the current ML. waitQuery waits for a query to match without changing the ML. refreshQuery rereads the screen, and exists returns true or false.
Clicks on a node. click waits for the screen to start changing. nsClick clicks without waiting and also accepts x and y coordinates. clickDown and clickUp press and release separately.
push stores the ML of 227 nodes, filter(node.isEditable) reduces it to the single input field for setText, and pop restores the 227. The second command keeps only the nodes that have text.
getText runs once on four nodes and returns an array. forEach(getText) runs four times and returns four results. In the last command, setChecked(true) runs on each day button, and true marks the ones that changed.
saveImage saves a picture of the nodes in the ML on the device and returns the file path. saveImageSettings changes the format, folder, quality and resolution, and saveImageInfo returns the current settings.
The first image is saved as a JPEG in the shared Pictures folder. After the settings change, the same command saves a PNG in Total Control's private folder.
Scrolls until a node matching the query is found and fully visible, then puts it in the ML. The direction and the maximum number of pages are optional.
The first commands reopen the current activity by name. The others pass am start options: open a web page, launch YouTube, open the Wi-Fi settings and start an email.
Loops over all the sliders in the sound settings, reads each maximum and sets half of it.
>>device.sendAai({query:"C:.SeekBar"}).retval.forEach(
x => {
var max = device.sendAai({elements:[x], action:"getProgress"}).retval.max;
device.sendAai({elements:[x], action:`setProgress(${max/2})`});
}
)
setText (O)
Enters text into a text field directly, without an input method. By default it replaces the existing text; a leading + appends.
The first command finds the resumed activity. The second is refused with "Permission denied". The third chains several commands and returns their combined output.
function creates a new action from a list of actions. %1, %2 and so on stand for its arguments, and the values given at creation are the defaults. funcExists, funcDelete, funcList and funcReset manage the functions.
openApp starts an app or brings it to the front, closeApp force-closes it, and restartApp does both. The app is named by its package name or its display name.
With time measurement on, the first open takes about 3 seconds, opening it again when it is already in memory takes about 0.7 seconds, and a restart takes about 2.9 seconds.
After this text is copied on the device, getClipData returns the HTML in retval and the plain text in text.
The formatted text that was copied.
>>device.sendAai({action:"getClipData"})
{retval: '<p><b>Copying formatted text between Android and Windows.</b></p><p>•	Node(<i>getTextFormats</i>) or clipboard(<i>getClipData</i>)<br>
•	plain text or formatted HTML<br>
•	Copy from here, paste on box below</p>', text: 'Copying formatted text between Android and Windows.
•Node(getTextFormats) or clipboard(getClipData)
•plain text or formatted HTML
•Copy from here, paste on box below', type: 'html'}