Skip to content

Browser actions

REL uses one browser-action contract across the CLI, MCP server, RPC v1, and Rust SDK. Actions are JSON objects with an action discriminator. Arrays run in order and stop at the first failure.

Surface Multiple actions One action
CLI rel perform ACTIONS, rel capture URL --actions JSON Repeat --action JSON, or use rel page action PAGE_ID --action JSON
MCP rel_capture.actions rel_page_action.action
RPC v1 POST /v1/perform, POST /v1/captures POST /v1/pages/{page_id}/actions
Rust SDK PerformRequest.actions, CaptureRequest.actions PageActionRequest.action

The same object shapes and validation rules apply on every surface. The CLI and MCP server forward actions through RPC v1; neither has a separate browser implementation.

Action Required fields Purpose
click selector Click the first matching element.
wait-for selector Wait until a matching element is present, optionally with a local timeout.
type selector, nonempty text Append text through Chromium keyboard input.
clear selector Remove an editable control’s contents.
press selector, key Send one supported named key to a control.
select selector, value Select one enabled option by exact value.
wait nonnegative seconds Pause action execution for a bounded duration.
click-link link, match Click an anchor by resolved HTTP(S) URL.
{ "action": "click", "selector": "button.more" }
{ "action": "click", "selector": "button.more", "mouse_move": false, "scroll": false }
{ "action": "wait-for", "selector": "#loaded-content", "timeout": 10 }
{ "action": "type", "selector": "#search", "text": "Magickraft" }
{ "action": "clear", "selector": "#query" }
{ "action": "press", "selector": "#search", "key": "Enter" }
{ "action": "select", "selector": "#genre", "value": "disco" }
{ "action": "wait", "seconds": 0.5 }
{
"action": "click-link",
"link": "https://example.com/more",
"match": { "type": "fuzzy-link", "threshold": 0.9 }
}

Function-style strings and legacy object shapes are rejected. Action objects are closed contracts: fields not listed for that action are rejected by the agent.

Selector actions use CEF’s read-only renderer DOM snapshot. Supported selectors are comma-separated lists composed of tag, universal, ID, class, presence or value attribute selectors, plus descendant, child (>), adjacent-sibling (+), and general-sibling (~) combinators. Pseudo-classes, pseudo-elements, namespaces, and CSS escapes are rejected.

wait-for checks only for presence and does not require layout bounds. Its optional positive timeout is measured from the start of that action and is capped by the enclosing operation’s remaining deadline. Omitting it preserves the enclosing deadline. If its own timeout expires first, REL returns ACTION_TIMEOUT; if the enclosing deadline expires first, REL returns TIMEOUT. click reads the first match’s bounds and dispatches CEF mouse input. A missing click target returns ACTION_TARGET_NOT_FOUND without polling, so put wait-for immediately before click when a page renders the target asynchronously.

click and click-link accept two optional booleans:

Field Default Behavior
mouse_move true Send a Chromium-local mouse-move event before button-down and button-up.
scroll true Use bounded Chromium wheel input and re-read bounds until an offscreen target is visible.

Setting mouse_move to false sends only button-down and button-up. Neither mode moves the macOS cursor. Setting scroll to false requires the target to already be visible.

click-link resolves anchor href values in the same read-only snapshot, normalizes the requested HTTP(S) URL, and applies its fuzzy-link threshold. The threshold must be between 0 and 1; 1 requires an exact normalized URL match.

Click targeting and dispatch never execute page JavaScript, mutate the DOM, invoke accessibility activation, or use Chrome DevTools Protocol. Missing, unreachable, and unsupported targets fail without a fallback.

type focuses an editable control and appends nonempty text through Chromium’s keyboard input path. Each character emits key-down, character/input, and key-up events, so keyboard-event handlers observe ordinary typing. clear explicitly empties the control; use clear followed by type to replace existing text. Text actions reject missing, disabled, read-only, or non-editable targets.

press focuses its target and accepts exactly these named keys:

Enter Tab Escape Backspace Delete
ArrowUp ArrowDown ArrowLeft ArrowRight
Home End PageUp PageDown Space

select targets a <select> element and chooses one enabled <option> by its exact value. Form updates dispatch ordinary DOM events so page state remains synchronized. These actions use fixed renderer operations and never accept caller-supplied JavaScript.

Pass an ordered array to perform:

Terminal window
rel perform '[
{"action":"wait-for","selector":"#disco_search","timeout":10},
{"action":"clear","selector":"#disco_search"},
{"action":"type","selector":"#disco_search","text":"Magickraft"},
{"action":"press","selector":"#disco_search","key":"Enter"}
]'

For URL capture, repeat --action or pass an array with --actions. The two options may be combined and preserve command-line order:

Terminal window
rel https://example.com \
--action '{"action":"wait-for","selector":"#disco_search","timeout":10}' \
--actions '[{"action":"type","selector":"#disco_search","text":"Magickraft"}]'

rel_capture accepts the same ordered array:

{
"url": "https://example.com",
"actions": [
{ "action": "clear", "selector": "#disco_search" },
{ "action": "type", "selector": "#disco_search", "text": "Magickraft" },
{ "action": "press", "selector": "#disco_search", "key": "Enter" }
]
}

rel_page_action accepts one of the same objects in its action field. MCP’s input schemas enumerate all eight action kinds and forward the validated objects through the corresponding RPC operations.

POST /v1/perform and POST /v1/captures accept an actions array. POST /v1/pages/{page_id}/actions accepts one object under action:

{
"action": { "action": "select", "selector": "#genre", "value": "disco" },
"timeout": 90,
"wait": 1
}

The agent validates every object before sending it through the private Chromium bridge. A page action remains pinned to the page, session, URL, and proxy chosen when that page was attached.

The public Action enum serializes directly to the JSON objects above:

use rel_client::{Action, FuzzyLinkMatch};
let actions = vec![
Action::WaitFor {
selector: "#disco_search".into(),
timeout: Some(10.0),
},
Action::Clear {
selector: "#disco_search".into(),
},
Action::Type {
selector: "#disco_search".into(),
text: "Magickraft".into(),
},
Action::Press {
selector: "#disco_search".into(),
key: "Enter".into(),
},
Action::ClickLink {
link: "https://example.com/more".into(),
match_rule: FuzzyLinkMatch::new(0.9),
mouse_move: None,
scroll: None,
},
];

None uses the default true behavior for mouse_move and scroll; use Some(false) to disable either behavior.

Browser sessions controlled while not visible use the Background Browser Size preset in REL → Settings… → General, which defaults to 1,920 × 947 CSS pixels. Visible sessions follow the resizable REL window. The viewport is a global app setting rather than an action field.

Actions stop at the first failure and return the standard structured RPC error. No action falls back to a different element, alternate browser backend, page script, or undocumented compatibility shape.