Selectors
Grammar
Section titled “Grammar”@login shorthand for id:loginid:login resource-id — matches full id, idShort, or a "/login" suffixtext:Sign in visible text (exact, case-insensitive, trimmed)desc:Submit content-desc / accessibility labelclass:Button simplified type ("Button") or full class ("android.widget.Button")"Sign in" a bare string is treated as text: (exact)Modifiers
Section titled “Modifiers”| Modifier | Effect |
|---|---|
--contains |
Make text/desc matches substring-based (drops the exact tier) |
--index N |
Select the Nth match, 0-based, when a selector intentionally matches several |
If a selector for an action matches more than one element and no --index is given, the
command exits 2 and lists the candidates. It never taps a guess, and it never waits for
ambiguity to resolve — the elements are already on screen.
Auto-healing
Section titled “Auto-healing”Matching is always case-insensitive and tries progressively looser tiers, stopping at the first that yields any match:
| Tier | Matches |
|---|---|
exact |
The value as written |
partial |
Substring |
normalized |
Case, punctuation, whitespace and emoji all stripped |
So text:sign up, text:SIGN UP and text:signup all find a “Sign up” button.
--contains drops the exact tier. --index N picks the Nth within the winning tier.
When a match heals — resolves at a tier other than exact — verikun appends
(healed: <tier> match) to stderr, so you can tighten the selector if you want to.
State modifiers
Section titled “State modifiers”A selector can also require an element’s accessibility state, in both polarities:
| Modifier | Matches | Negative form |
|---|---|---|
--enabled |
actionable right now | --not-enabled |
--selected |
the current option of a segmented control, tab bar or mode picker | --not-selected |
--checked |
a ticked checkbox, switch or radio | --not-checked |
--focused |
the element holding input focus | --not-focused |
Unset means don’t care. These never narrow a selector you did not ask them to. Passing
both --x and --not-x is a usage error (exit 2).
Why --enabled matters
Section titled “Why --enabled matters”A Submit or Check button the app disables until a form is valid is present long before it
is usable, so tapping on presence taps a dead control. Combined with
auto-wait, --enabled reads as “wait until it is pressable”:
vk tap @submit --enabledWhy the negative forms matter
Section titled “Why the negative forms matter”They are what make a toggle drivable. A segmented control whose options share one handler
flips on any tap, so “tap the option I want” lands on the other one whenever it was already
chosen — exit 0, nothing to notice, and the run exercises the wrong mode. Guard it instead:
vk find "@mode_video --not-selected" --no-wait && vk tap @mode_videoWriting a modifier into the selector string
Section titled “Writing a modifier into the selector string”A modifier can be written as a flag or appended to the selector string, as above. The
string form is what a vk ai control node (if-present,
when, repeat, while-present, read) uses, since it holds a bare selector with nowhere
to put a flag:
if-present "id:mode_video --not-selected" { tap id:mode_video }Platform support
Section titled “Platform support”--selected and --focused are Android-only. idb reports no such state for iOS, so
using them with --ios exits 3 rather than silently matching nothing. --enabled and
--checked work on both platforms. The matrix:
Platform support.
Which selector to reach for
Section titled “Which selector to reach for”@id first, text: second, desc: never. Not all four kinds travel equally well, and if a
flow has to run on both Android and iOS this ordering matters:
| selector | Android | iOS | portable? |
|---|---|---|---|
@id |
resource-id |
AXUniqueId |
yes — always prefer this |
text: |
visible text, falling back to content-desc |
AXLabel / title / AXValue |
yes |
desc: |
content-desc |
accessibilityHint only |
no — Android in practice |
class: |
widget class | element role | no — see below |
@id is also the only selector that is not text, so it survives localisation. A flow
pinned with text: breaks the moment the device is in a different language.
Two traps
Section titled “Two traps”desc:does not fall back.text:falls back todescwhen no text matches, so atext:Submitselector finds an element carrying only an accessibility label. The reverse is not true —desc:Submitwill never match visible text. On iOS an accessibility label arrives astext, so adesc:selector written against Android silently stops matching there.class:is mostly useless on a cross-platform UI toolkit. Flutter text inputs report asandroid.widget.EditText/TextField, but almost everything else isandroid.view.View— soclass:Buttoncannot match a Flutter button regardless of what the widget actually is.
Flutter
Section titled “Flutter”For a Flutter app, @id comes from Semantics(identifier:); Semantics(label:) gives you
desc on Android but text on iOS. Give every identified element a label too: an element
with an identifier but no label, value or action survives on Android yet vanishes from the
iOS tree. A worked example, with the cross-platform gotchas recorded per platform, is in
example/flutter-app.