feat: add batch command for multi-step workflows (#865)

Add `batch` command that reads a JSON array of commands from stdin
and executes them sequentially against the daemon. This avoids
per-command process startup overhead when AI agents run multi-step
browser workflows.

Supports --bail to stop on first error (default: continue all)
and --json for structured output as an array of results.

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Matt Van Horn
2026-03-17 08:48:50 -05:00
committed by GitHub
co-authored by Matt Van Horn Claude Opus 4.6
parent a865dd56e0
commit 7734bb2702
5 changed files with 234 additions and 1 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"agent-browser": minor
---
Add `batch` command for executing multiple commands from stdin in a single invocation. Accepts a JSON array of string arrays and returns results sequentially. Supports `--bail` to stop on first error and `--json` for structured output.
+19
View File
@@ -187,6 +187,25 @@ agent-browser wait "#spinner" --state hidden
**Load states:** `load`, `domcontentloaded`, `networkidle`
### Batch Execution
Execute multiple commands in a single invocation by piping a JSON array of
string arrays to `batch`. This avoids per-command process startup overhead
when running multi-step workflows.
```bash
# Pipe commands as JSON
echo '[
["open", "https://example.com"],
["snapshot", "-i"],
["click", "@e1"],
["screenshot", "result.png"]
]' | agent-browser batch --json
# Stop on first error
agent-browser batch --bail < commands.json
```
### Clipboard
```bash
+22
View File
@@ -1317,6 +1317,12 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
"diff" => parse_diff(&rest, &id, flags),
// === Batch ===
"batch" => {
let bail = rest.contains(&"--bail");
Ok(json!({ "id": id, "action": "batch", "bail": bail }))
}
_ => Err(ParseError::UnknownCommand {
command: cmd.to_string(),
}),
@@ -3961,4 +3967,20 @@ mod tests {
let cmd = parse_command(&args("get cdp-url"), &default_flags()).unwrap();
assert_eq!(cmd["action"], "cdp_url");
}
// === Batch Tests ===
#[test]
fn test_batch_default() {
let cmd = parse_command(&args("batch"), &default_flags()).unwrap();
assert_eq!(cmd["action"], "batch");
assert_eq!(cmd["bail"], false);
}
#[test]
fn test_batch_with_bail() {
let cmd = parse_command(&args("batch --bail"), &default_flags()).unwrap();
assert_eq!(cmd["action"], "batch");
assert_eq!(cmd["bail"], true);
}
}
+152 -1
View File
@@ -21,7 +21,7 @@ use windows_sys::Win32::System::Threading::{OpenProcess, PROCESS_QUERY_LIMITED_I
use commands::{gen_id, parse_command, ParseError};
use connection::{ensure_daemon, get_socket_dir, send_command, DaemonOptions};
use flags::{clean_args, parse_flags};
use flags::{clean_args, parse_flags, Flags};
use install::run_install;
use output::{
print_command_help, print_help, print_response_with_opts, print_version, OutputOptions,
@@ -729,6 +729,13 @@ fn main() {
}
}
// Handle batch command: read commands from stdin, execute sequentially
if cmd.get("action").and_then(|v| v.as_str()) == Some("batch") {
let bail = cmd.get("bail").and_then(|v| v.as_bool()).unwrap_or(false);
run_batch(&flags, bail);
return;
}
let output_opts = OutputOptions {
json: flags.json,
content_boundaries: flags.content_boundaries,
@@ -809,6 +816,150 @@ fn main() {
}
}
fn run_batch(flags: &Flags, bail: bool) {
use std::io::Read as _;
let mut input = String::new();
if let Err(e) = std::io::stdin().read_to_string(&mut input) {
if flags.json {
print_json_error(format!("Failed to read stdin: {}", e));
} else {
eprintln!("{} Failed to read stdin: {}", color::error_indicator(), e);
}
exit(1);
}
let commands: Vec<Vec<String>> = match serde_json::from_str(&input) {
Ok(c) => c,
Err(e) => {
if flags.json {
print_json_error(format!(
"Invalid JSON input: {}. Expected an array of string arrays, e.g. [[\"open\", \"https://example.com\"], [\"snapshot\"]]",
e
));
} else {
eprintln!(
"{} Invalid JSON input: {}. Expected an array of string arrays.",
color::error_indicator(),
e
);
}
exit(1);
}
};
if commands.is_empty() {
if flags.json {
println!("[]");
}
return;
}
let output_opts = OutputOptions {
json: flags.json,
content_boundaries: flags.content_boundaries,
max_output: flags.max_output,
};
let mut results: Vec<serde_json::Value> = Vec::new();
let mut had_error = false;
for (i, cmd_args) in commands.iter().enumerate() {
if cmd_args.is_empty() {
continue;
}
let parsed = match parse_command(cmd_args, flags) {
Ok(c) => c,
Err(e) => {
had_error = true;
if flags.json {
results.push(json!({
"command": cmd_args,
"success": false,
"error": e.format(),
}));
if bail {
break;
}
} else {
eprintln!(
"{} Command {}: {}",
color::error_indicator(),
i + 1,
e.format()
);
if bail {
exit(1);
}
}
continue;
}
};
let action = parsed
.get("action")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
match send_command(parsed, &flags.session) {
Ok(resp) => {
if flags.json {
results.push(json!({
"command": cmd_args,
"success": resp.success,
"result": resp.data,
"error": resp.error,
}));
} else {
if i > 0 {
println!();
}
print_response_with_opts(&resp, action.as_deref(), &output_opts);
}
if !resp.success {
had_error = true;
if bail {
if !flags.json {
exit(1);
}
break;
}
}
}
Err(e) => {
had_error = true;
if flags.json {
results.push(json!({
"command": cmd_args,
"success": false,
"error": e.to_string(),
}));
if bail {
break;
}
} else {
eprintln!("{} Command {}: {}", color::error_indicator(), i + 1, e);
if bail {
exit(1);
}
}
}
}
}
if flags.json {
println!(
"{}",
serde_json::to_string(&results).unwrap_or_else(|_| "[]".to_string())
);
}
if had_error {
exit(1);
}
}
#[cfg(test)]
mod tests {
use super::*;
+36
View File
@@ -2418,6 +2418,38 @@ Examples:
"##
}
"batch" => {
r##"
agent-browser batch - Execute multiple commands from stdin
Usage: echo '<json>' | agent-browser batch [options]
Reads a JSON array of commands from stdin and executes them sequentially.
Each command is an array of strings matching normal CLI arguments.
Results are printed in order, separated by blank lines (or as a JSON array
with --json).
Options:
--bail Stop on first error (default: continue all commands)
--json Output results as a JSON array
Input Format:
A JSON array of string arrays. Each inner array is one command:
[
["open", "https://example.com"],
["snapshot", "-i"],
["click", "@e1"],
["fill", "@e2", "test@example.com"],
["screenshot", "result.png"]
]
Examples:
echo '[["open", "https://example.com"], ["snapshot"]]' | agent-browser batch
echo '[["open", "https://example.com"], ["get", "title"]]' | agent-browser batch --json
agent-browser batch --bail < commands.json
"##
}
_ => return false,
};
println!("{}", help.trim());
@@ -2508,6 +2540,10 @@ Debug:
inspect Open Chrome DevTools for the active page
clipboard <op> [text] Read/write clipboard (read, write, copy, paste)
Batch:
batch [--bail] Execute commands from stdin (JSON array of string arrays)
--bail stops on first error (default: continue all)
Auth Vault:
auth save <name> [opts] Save auth profile (--url, --username, --password/--password-stdin)
auth login <name> Login using saved credentials