Selectors

Purpose

Define the NodeMatcher contract used across execution payloads, explain how CLI selector flags map into that contract, and document the mutual-exclusion rules that prevent ambiguous selector input.

Sources

  • Contract shape: apps/node/src/contracts/selectors.ts
  • Shared matcher limits: apps/node/src/contracts/limits.ts
  • Execution validation: apps/node/src/domain/executions/validateExecution.ts
  • CLI selector parsing: apps/node/src/cli/selectorFlags.ts
  • read-value label selector handling: apps/node/src/cli/registry.ts

NodeMatcher Contract

The raw selector object shared by matcher, container, expectedNode, and labelMatcher is:

{
  "resourceId": "optional string",
  "role": "optional string",
  "textEquals": "optional string",
  "textContains": "optional string",
  "contentDescEquals": "optional string",
  "contentDescContains": "optional string"
}

Meaning of each field:

Field Match behavior
resourceId exact Android resource ID match
role exact accessibility role match
textEquals exact visible-text match
textContains substring visible-text match
contentDescEquals exact content-description match
contentDescContains substring content-description match

Rules enforced by Node:

  • a matcher may contain one field or several fields
  • multiple fields combine into one object, so the runtime receives all of them together
  • empty matcher objects are invalid
  • each matcher string value must be at most 512 characters (LIMITS.MAX_MATCHER_VALUE_LENGTH)
  • blank strings are rejected before or during validation depending on how the matcher was built
  • selector objects are strict in execution validation, so unknown keys are rejected
  • common input aliases are normalized before that strict validation runs, including id/resource_id, text, text_contains, content_desc, description, and accessibility_label
  • when the shared CLI parser sees no selector flags at all, it returns an empty matcher object and the command decides whether selectors are required for that command

Accepted raw JSON matcher-field aliases:

Alias Canonical field
id, resource_id resourceId
text textEquals
text_contains textContains
content_desc, description, accessibility_label contentDescEquals
content_desc_equals contentDescEquals
content_desc_contains, description_contains, accessibility_label_contains contentDescContains

Concrete payload example:

{
  "matcher": {
    "role": "button",
    "textContains": "Settings"
  }
}

Success condition for that selector object:

  • the object uses only the six supported matcher keys
  • at least one value is non-empty

Where Selectors Appear

NodeMatcher is reused in several action parameters:

Action parameter Meaning
params.matcher primary target node for actions such as click, read_text, enter_text, wait_for_node, scroll_until, and scroll_and_click
params.container optional ancestor or scrollable container constraint
params.expectedNode navigation target for wait_for_navigation
params.labelMatcher label node for read_key_value_pair

Example execution fragment:

{
  "id": "read-1",
  "type": "read_text",
  "params": {
    "matcher": { "textEquals": "Battery" },
    "container": { "resourceId": "android:id/list" }
  }
}

CLI Selector Forms

For most commands, the CLI offers two equivalent ways to build a NodeMatcher:

  1. Shorthand flags such as --text, --text-contains, --id, --desc, --desc-contains, and --role
  2. Raw JSON via --selector '<json>'

Agent-friendly CLI aliases accepted for shorthand selectors:

  • --resource-id -> --id
  • --content-desc -> --desc
  • --content-desc-contains -> --desc-contains
  • --container-resource-id -> --container-id
  • --container-content-desc -> --container-desc
  • --container-content-desc-contains -> --container-desc-contains

Container selectors follow the same pattern:

  1. Shorthand flags such as --container-text, --container-id, and --container-role
  2. Raw JSON via --container-selector '<json>'

The parser resolves shorthand flags into the same NodeMatcher object used by raw JSON. For example:

clawperator wait --text "Done" --role button

becomes the matcher:

{
  "textEquals": "Done",
  "role": "button"
}

Selector Flags

Flag Description Notes
--selector Raw NodeMatcher JSON for an element. Mutually exclusive with shorthand element selector flags.
--text Match an element by exact text. May be combined with other shorthand selector flags.
--text-contains Match an element by partial text. May be combined with other shorthand selector flags.
--id Match an element by resource id. May be combined with other shorthand selector flags.
--desc Match an element by exact content description. May be combined with other shorthand selector flags.
--desc-contains Match an element by partial content description. May be combined with other shorthand selector flags.
--role Match an element by accessibility role. May be combined with other shorthand selector flags.
--container-selector Raw NodeMatcher JSON for a container. Mutually exclusive with raw selector JSON on the same matcher.
--container-text Match a container by exact text. Mutually exclusive with raw selector JSON on the same matcher.
--container-text-contains Match a container by partial text. Mutually exclusive with raw selector JSON on the same matcher.
--container-id Match a container by resource id. Mutually exclusive with raw selector JSON on the same matcher.
--container-desc Match a container by exact content description. Mutually exclusive with raw selector JSON on the same matcher.
--container-desc-contains Match a container by partial content description. Mutually exclusive with raw selector JSON on the same matcher.
--container-role Match a container by accessibility role. Mutually exclusive with raw selector JSON on the same matcher.

Mutual Exclusion And Validation Rules

Element selector rules:

  • --selector is mutually exclusive with all shorthand element selector flags
  • duplicate value flags such as repeating --text or --id are rejected
  • --selector must be valid JSON
  • --selector must parse to a JSON object, not an array or scalar
  • blank values such as --text "" or --selector "" are rejected
  • click --coordinate <x> <y> is mutually exclusive with every element selector flag
  • click --coordinate ... --focus is invalid because coordinate clicks do not support clickType = "focus"

Container selector rules:

  • --container-selector is mutually exclusive with all --container-* shorthand flags
  • duplicate container flags such as repeating --container-id are rejected
  • --container-selector must be valid JSON
  • --container-selector must parse to a JSON object
  • blank values such as --container-text "" are rejected
  • if no container flags are present, Node omits params.container

Validation examples:

Valid:

clawperator read --text "Price" --container-id "android:id/list" --json

Invalid:

clawperator read --text "Price" --selector '{"textEquals":"Price"}'

Why invalid:

  • the CLI parser rejects mixing --selector with shorthand element flags

Structured validation example:

{
  "code": "EXECUTION_VALIDATION_FAILED",
  "message": "use --selector OR the simple flags, not both"
}

Command-Specific Notes

Most commands

click, read, wait, scroll-until, scroll-and-click, and wait-for-nav all use the shared selector parser from selectorFlags.ts. That means they share the same shorthand-to-JSON mapping and the same mutual-exclusion rules.

Required-vs-optional behavior is decided by the command after parsing:

  • click, read, and wait require an element selector unless click is using --coordinate
  • wait-for-nav accepts either --app or a selector, but still requires at least one of them
  • scroll has no target selector and uses only optional container selector flags
  • scroll-until and scroll-and-click require a target selector

type

type is slightly different because --text means “text to enter”, not “textEquals selector”. For element targeting, type uses:

  • --id
  • --desc
  • --desc-contains
  • --role
  • --text-contains
  • --selector

Example:

clawperator type "hello world" --role textfield

read-value

read-value does not use the general selector parser. It builds labelMatcher from three dedicated flags:

CLI flag labelMatcher field
--label textEquals
--label-id resourceId
--label-desc contentDescEquals

Accepted CLI aliases for those read-value label flags:

  • --text and --label-text -> --label
  • --id and --resource-id -> --label-id
  • --desc and --content-desc -> --label-desc

At least one of those flags is required.

Blank label values are rejected, and if you provide none of the three label flags the command returns a usage error before execution is built.

Raw JSON aliases for read_key_value_pair.params.labelMatcher follow the same NodeMatcher alias table shown above. Raw payload aliases label_matcher and label_selector are normalized to labelMatcher before validation.

Concrete execution fragment:

{
  "id": "read-value-1",
  "type": "read_key_value_pair",
  "params": {
    "labelMatcher": {
      "textEquals": "Battery"
    }
  }
}

Container Matching Semantics

Container selectors narrow an action to a matched ancestor or scrollable region instead of searching the full screen.

Current uses:

  • read attaches the matcher as params.container
  • scroll attaches the matcher as params.container
  • scroll_until attaches the matcher as params.container
  • scroll_and_click attaches the matcher as params.container

If no container selector is provided:

  • read_text searches without container scoping
  • scroll actions let Android choose the relevant on-screen scrollable container

Example:

clawperator scroll-until --text "About phone" --container-id "android:id/list"

becomes:

{
  "type": "scroll_until",
  "params": {
    "matcher": { "textEquals": "About phone" },
    "container": { "resourceId": "android:id/list" }
  }
}

JSON Examples

Exact text:

{ "textEquals": "Wi-Fi" }

Partial text plus role:

{ "textContains": "Sign", "role": "button" }

Raw container selector:

{ "resourceId": "android:id/list" }

Navigation target:

{
  "expectedPackage": "com.android.settings",
  "expectedNode": { "textEquals": "Settings" }
}

CLI Examples

clawperator click --text "Wi-Fi"
clawperator read --selector '{"resourceId":"android:id/title"}' --json
clawperator wait --text-contains "Done" --timeout 10000 --json
clawperator scroll-until --text "About phone" --container-id "android:id/list"
clawperator read-value --label "Battery" --json