UiPath desktop automation, rewritten as a selector expression language with operator precedence
Most explainers about driving Windows desktop applications describe selectors as XML fragments. A <wnd> for the window, a <ctrl> for the control, attribute key=value pairs inside each tag, a Strict / Fuzzy toggle in a designer. That is the surface UiPath ships. Underneath both UiPath and Terminator sit on the same Microsoft UI Automation tree, the same roles (Pane, Edit, Button, ListItem), the same AutomationIds. Terminator changes the surface. The selector is one string. The grammar has &&, ||, !, ( ), descendant chaining via >>, spatial operators like rightof:, and a has: predicate. It is parsed by a Shunting Yard algorithm into a 23-variant typed AST. The whole point is so an AI coding assistant can compose, edit, and reason about selectors as expressions, not as XML it has to template.
What every UiPath explainer leaves on the table
Open ten guides about driving desktop apps from a script. They will all describe the same XML form: one tag per element in the path, attributes as flat key=value pairs, a Strict mode that requires every attribute to match exactly, a Fuzzy mode that lets attributes drift with matching:aaname='fuzzy' and fuzzylevel:aaname='0.4'. They will describe Studio's Selector Editor (Ctrl+E) and the UI Explorer overlay. They will mention anchor-based matching for elements with no useful accessibility name. None of them present the option that this page is about: a selector grammar with parentheses, operator precedence, and a real parser. That option exists. It is the surface Terminator chose because the operator writing the workflow is now often an LLM, and an LLM is much better at composing an expression than at hand-editing XML fragments inside a designer.
The anchor: one file, one Shunting Yard pass, 23 AST variants
The implementation lives in crates/terminator/src/selector.rs. The first thing the file declares is a Selector enum with twenty-three variants. Eight cover atomic match types (Role, Id, Name, Text, Path, NativeId, Attributes, ClassName). Five are spatial (RightOf, LeftOf, Above, Below, Near). Three are predicates and traversal (Has, Parent, Nth). Three are logical (And, Or, Not). One is the descendant Chain. The remaining variants cover process scoping, visibility filtering, localized roles, and an explicit Invalid(reason) return that the parser produces when the input string does not parse, so a malformed selector never silently turns into an unintended match.
The selector AST, top of selector.rs
Twenty-three variants. The interesting ones for a reader coming from a UiPath background are the five spatial variants and the boolean group. UiPath has spatial matching through anchors, but it is an activity that wraps two selectors. Here it is a single selector token (rightof:) that takes another selector as its inner argument and parses the same way as everything else.
The parser. Shunting Yard, three precedence levels, no surprises.
Or has precedence 1, And has precedence 2, Not has precedence 3. The output queue holds Selector values, the operator stack holds tokens. Same algorithm Dijkstra wrote for arithmetic expressions in 1961, applied to a tiny boolean algebra over UI matchers. The full implementation is fewer than seventy lines.
Why >> is parsed before everything else
Descendant chaining is not a boolean operator. (role:Window && name:Notepad) >> role:Edit means: find a Window whose name is Notepad, and inside that, find an Edit. Each side of >> is itself a complete selector expression. The From<&str> implementation splits on >> first and recursively parses each segment, producing a Selector::Chain. Inside each segment, the boolean operators are then resolved with the Shunting Yard pass.
Side by side: same target, two surfaces
Notepad's text area is a single element in the Windows UIA tree. UiPath addresses it with an XML fragment configured in Studio. Terminator addresses it with a single string parsed into a chained boolean expression. The runtime does the same thing on both sides; the authoring surface is the part that differs.
UiPath XML selector vs. Terminator selector grammar
<!-- A typical UiPath selector. Edited in Studio's Selector Editor (Ctrl+E).
Attributes inside one tag are an implicit AND.
There is no OR, no NOT, no parentheses, no precedence. -->
<wnd app='notepad.exe' cls='Notepad' />
<ctrl name='Text Editor' role='editable text' />Six recipes that exercise the grammar
Each one is a single string. Each one parses through parse_atomic_selector for the leaves and parse_boolean_expression for the operators. None of them require a designer or a manifest file.
What happens between desktop.locator(...) and the click
Four actors, eight messages, one round-trip through the parser, one walk of the live UIA tree. The assistant calls the SDK, the SDK constructs a Selector via From<&str>, the engine walks the chain, and a UIElement comes back wrapped in a Locator. After that point, .click(), .typeText(), and friends are all that remains.
Locator string -> Selector AST -> UIA element
With the grammar vs. without it
The same intent, two authoring loops. One ends with a person in a designer dragging a UI Explorer crosshair and toggling Strict / Fuzzy on a per-attribute basis. The other ends with a string in a file an AI assistant just wrote, parsed in microseconds, and either an executable element or anInvalid variant the runtime can refuse to act on.
XML selector in a designer vs. selector grammar in code
Open Studio. Drag the UI Explorer crosshair onto the target element. Read back an XML fragment with one tag per ancestor and key=value attributes inside each tag. Toggle Strict or Fuzzy attribute by attribute. Save as part of an activity inside a XAML file. Selector behaviour is implicit AND across attributes; for OR, NOT, or composition you escape into Anchor Base, Find Children, or Conditional activities.
- One tag per ancestor, attributes as flat pairs
- Strict / Fuzzy toggle inside Studio
- No AND/OR/NOT between attributes, no parentheses
- Composition lives in surrounding activities, not the selector
What a real MCP turn looks like on stdio
One valid selector chains, walks the UIA tree, finds the element, and clicks. One malformed selector parses into Selector::Invalid and never reaches the engine. The assistant sees both responses on the same channel and can react in the next turn.
The four steps that get this wired into your assistant
Nothing extra to install for the grammar specifically. It comes with the Rust core, which the Node and Python SDKs both link against, and it is the same parser the MCP agent uses when an AI assistant emits a selector.
Install the framework
npm i @mediar-ai/terminator on Windows for the TypeScript SDK, or cargo add terminator-rs for Rust. The selector grammar lives in the Rust core and is shared by every SDK. Python (terminator.py) is partial today.
Wire it into your AI assistant
claude mcp add terminator 'npx -y terminator-mcp-agent@latest'. The MCP server exposes ~35 tools: get_window_tree, click_element, execute_sequence, typecheck_workflow, and more. The assistant calls them with selector strings.
Inspect the live UI tree
Use Accessibility Insights for Windows or inspect.exe to see what UIA reports for the app you want to drive. Names, roles, AutomationIDs, ClassNames are exactly what you put after the colons in the selector grammar (name:, role:, nativeid:, classname:).
Compose, run, refine
Write the workflow as a TypeScript file using @mediar-ai/workflow's createStep / createWorkflow. Each step takes a desktop instance and a Locator with a selector string. If a selector is wrong, the parser returns a Selector::Invalid(reason) before any click runs.
The comparison row that belongs in every roundup
Most comparisons of UiPath alternatives stop at license type, supported operating system, and recorder capability. The selector representation is the row that decides whether an LLM can author a workflow without a designer.
| Feature | UiPath desktop automation | Terminator |
|---|---|---|
| Selector representation | XML fragment, one tag per ancestor (e.g. <wnd app='notepad.exe' /><ctrl name='Edit' />) | Single-string boolean expression (e.g. (role:Window && name:Notepad) >> role:Edit) |
| Logical operators between attributes | Implicit AND only (every attribute on a tag must match) | Explicit && (AND), || (OR), , (also OR), ! (NOT), with parentheses for grouping |
| Operator precedence | Not applicable, attributes are flat key=value pairs | Shunting Yard with operator_precedence (Or=1, And=2, Not=3) |
| Descendant chaining | Implied by tag order in the XML fragment | >> operator splits into a Selector::Chain of independent expressions |
| Spatial selectors | Anchor-based with the Anchor Base activity | rightof:, leftof:, above:, below:, near: as first-class atomic selectors |
| Has-descendant predicate | Workaround via Find Children + filtering | has:<inner-selector> as an atomic selector, parsed into Selector::Has |
| Parent navigation | GetParent activity | .. as a literal selector token, parsed into Selector::Parent |
| Invalid input behaviour | Selector Editor flags the field, run-time exception otherwise | Selector::Invalid(reason) variant returned by the parser, surfaced before execution |
| Author | Human in Studio's Selector Editor (Ctrl+E) | Code, or an AI coding assistant emitting a string into a TypeScript file |
| License of the engine | Vendor-proprietary, requires a UiPath licence to run at scale | MIT, Rust crate terminator-rs on crates.io |
The numbers that describe the surface
0 file (selector.rs), 0 Selector AST variants, 0 precedence levels (Or=1, And=2, Not=3), 0 spatial operators (rightof, leftof, above, below, near), 0 descendant chain operator (>>), and 0 typed Invalid variant the parser returns instead of letting a malformed selector match anything.
“The selector should be an expression an AI can compose. Not an XML fragment a person edits in a designer.”
selector.rs, terminator-rs
Where this leaves UiPath, and where it does not
UiPath remains the right tool for an organisation that wants a designer-first authoring loop, a hosted control plane (Orchestrator), formal enterprise support, and a marketplace of pre-built activities. Terminator does not replace any of that. What Terminator replaces is the part of the workflow where a developer or an AI assistant is going to write code anyway. For that part, an XML fragment in a designer is a strange interchange format, and a parsed expression language is a more honest one.
Install with claude mcp add terminator "npx -y terminator-mcp-agent@latest". Ask your assistant to call get_window_tree on a running app. Watch what it emits as a selector string. The string will be a single line that parses cleanly through selector.rs. That is the entire pitch.
Walk through your hardest UiPath selector and rewrite it as a Terminator expression
Bring one selector you fight with regularly. We will open a live MCP session, get the live UIA tree, and rewrite the selector as a single string with the boolean grammar. You will see the AST it parses into, and the element it finds, on the same call.
Frequently asked
Frequently asked questions
Is Terminator a UiPath replacement?
Not as a like-for-like swap. UiPath is a closed-source platform with Studio (a visual designer), Orchestrator (a job scheduler), and a robot license model. Terminator is a developer framework: an MIT-licensed Rust core, an MCP server (terminator-mcp-agent on npm), and SDKs for TypeScript, Python, and Rust. Both sit on top of Windows UI Automation, so the underlying selectors target the same elements (Pane, Edit, Button, ListItem, etc.). The difference is the authoring surface. UiPath workflows are XAML files with XML selector strings dragged together in Studio. Terminator workflows are TypeScript files an AI coding assistant or a developer writes directly, with selectors expressed as boolean expressions parsed by a real grammar.
What does the selector grammar actually look like?
A single string. Examples: role:Button|name:Save (legacy single-pipe role + name). (role:Window && name:Best Plan Pro) >> nativeid:dob (a chained, AND-grouped expression). role:Edit && !visible:false (NOT operator). rightof:role:Label|name:Email (spatial, find an element to the right of an anchor). has:role:Image (descendant predicate, like Playwright's :has()). All of those parse through the same code path in crates/terminator/src/selector.rs. The tokenizer handles &&, ||, ',' (also OR), !, (, ), and >>. parse_boolean_expression then runs a Shunting Yard pass with operator_precedence Or=1, And=2, Not=3.
How is that different from UiPath's strict and fuzzy selectors?
UiPath stores a selector as an XML fragment, e.g. <wnd app='notepad.exe' cls='Notepad' /><ctrl name='Text Editor' role='editable text' />. Each tag is one element in the path, attributes inside the tag are matched as flat key=value pairs, and you toggle between strict matching and fuzzy (with attributes like matching:aaname='fuzzy' and fuzzylevel:aaname='0.4') in Studio's Selector Editor. There is no AND/OR/NOT between attributes, no parentheses, no operator precedence. Terminator's expression grammar gives you those primitives. The tradeoff is honest: UiPath's editor is friendlier for someone hand-clicking through a UI; the grammar is friendlier for code that an AI generates and edits, and for selectors that need composition.
Where exactly does the parser live?
crates/terminator/src/selector.rs in the Terminator repository. The Selector enum is at the top of the file with 23 variants (Role, Id, Name, Text, Path, NativeId, Attributes, Filter, Chain, ClassName, Visible, LocalizedRole, Process, RightOf, LeftOf, Above, Below, Near, Nth, Has, Parent, And, Or, Not, plus Invalid for parse failures). The tokenizer is fn tokenize starting around line 94. The Shunting Yard parser is fn parse_boolean_expression starting around line 215. The descendant chain split on >> happens earlier, in the From<&str> impl at line 478, because >> has priority over the boolean operators and produces a Selector::Chain.
What spatial selectors are supported?
Five: rightof:, leftof:, above:, below:, and near:. Each takes another selector as its inner expression. So rightof:role:Label|name:Email finds elements that are rightward of the labelled Email field. These are useful when an input has no native accessibility name but always sits next to a label that does. They are listed as variants in the Selector enum (RightOf, LeftOf, Above, Below, Near) and are dispatched by parse_atomic_selector in the same file.
Does Terminator support attended automation, the way UiPath Assistant does?
Terminator runs from your own session. It uses your existing browser session, so cookies and logins survive. It does not take over your cursor and keyboard the way a foreground RDA bot does. You can wire that into an attended-style trigger (a hotkey, an MCP tool call from your assistant, a CLI command) or into an unattended job runner. The framework is neutral about whether a human is at the keyboard; what it gives you is the underlying selector engine, the action API (click, type, press, drag), the workflow recorder, and an MCP server. Composition is up to you.
Can an AI coding assistant actually use this without a designer?
Yes, that is the design point. Add the MCP agent with claude mcp add terminator 'npx -y terminator-mcp-agent@latest'. Your assistant gets ~35 tools registered, including get_window_tree (returns the live UIA tree as JSON), click_element (takes a selector string), and execute_sequence (runs a YAML or TS workflow). The assistant composes selector strings the same way it composes Playwright selectors for the web. Because the grammar has operator precedence and a typed AST, the assistant can reason about a selector as an expression, not as a stringly-typed XML blob it has to template.
What about the things UiPath gives you that this does not?
Several real gaps. UiPath ships Studio, a visual designer that beginners can use to build a workflow without writing code; Terminator does not. UiPath has Orchestrator, a hosted control plane for scheduling jobs across many robots; Terminator does not (you bring your own runner). UiPath has a marketplace of pre-built activities and connectors; Terminator has SDKs and examples but no marketplace. UiPath has formal enterprise support contracts; Terminator is community-supported under MIT. If your team's workflow assumes a designer-first authoring loop and a centralised job board, UiPath is a different category of tool. If your workflow is 'an LLM writes the automation', Terminator's surface is the one shaped for that.