Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 66 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,68 @@
<div id="toc" align="center" style="margin-bottom: 0;">
<ul style="list-style: none; margin: 0; padding: 0;">
<a href="https://stagehand.dev">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="media/dark_logo.png" />
<img alt="Stagehand" src="media/light_logo.png" width="200" style="margin-right: 30px;" />
</picture>
</a>
</ul>
</div>
<p align="center">
<strong>The AI Browser Automation Framework</strong><br>
<a href="https://docs.stagehand.dev/v3/sdk/rust">Read the Docs</a>
</p>

<p align="center">
<a href="https://github.com/browserbase/stagehand/tree/main?tab=MIT-1-ov-file#MIT-1-ov-file">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="media/dark_license.svg" />
<img alt="MIT License" src="media/light_license.svg" />
</picture>
</a>
<a href="https://stagehand.dev/discord">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="media/dark_discord.svg" />
<img alt="Discord Community" src="media/light_discord.svg" />
</picture>
</a>
</p>

<p align="center">
<a href="https://trendshift.io/repositories/12122" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12122" alt="browserbase%2Fstagehand | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
</p>

<p align="center">
If you're looking for other languages, you can find them
<a href="https://docs.stagehand.dev/v3/first-steps/introduction"> here</a>
</p>

<div align="center" style="display: flex; align-items: center; justify-content: center; gap: 4px; margin-bottom: 0;">
<b>Vibe code</b>
<span style="font-size: 1.05em;"> Stagehand with </span>
<a href="https://director.ai" style="display: flex; align-items: center;">
<span>Director</span>
</a>
<span> </span>
<picture>
<img alt="Director" src="media/director_icon.svg" width="25" />
</picture>
</div>

## What is Stagehand?

Stagehand is a browser automation framework used to control web browsers with natural language and code. By combining the power of AI with the precision of code, Stagehand makes web automation flexible, maintainable, and actually reliable.

## Why Stagehand?

Most existing browser automation tools either require you to write low-level code in a framework like Selenium, Playwright, or Puppeteer, or use high-level agents that can be unpredictable in production. By letting developers choose what to write in code vs. natural language (and bridging the gap between the two) Stagehand is the natural choice for browser automations in production.

1. **Choose when to write code vs. natural language**: use AI when you want to navigate unfamiliar pages, and use code when you know exactly what you want to do.

2. **Go from AI-driven to repeatable workflows**: Stagehand lets you preview AI actions before running them, and also helps you easily cache repeatable actions to save time and tokens.

3. **Write once, run forever**: Stagehand's auto-caching combined with self-healing remembers previous actions, runs without LLM inference, and knows when to involve AI whenever the website changes and your automation breaks.

# Stagehand Rust SDK [ALPHA] <img height="40" alt="Stagehand logo" src="https://github.com/user-attachments/assets/0b264628-bf81-4130-b378-b9f6b7fcf76f" align="right"/><img height="40" alt="Rust logo" src="https://github.com/user-attachments/assets/ee66721b-25a3-4f85-ac1f-a5b2fd5d7013" align="right"/>


Expand All @@ -10,7 +75,7 @@
A Rust client library for [Stagehand](https://stagehand.dev), the AI-powered browser automation framework. This SDK provides an async-first, type-safe interface for controlling [Browserbase browsers](https://browserbase.com/) and performing AI-driven web interactions.

> [!CAUTION]
> This is an ALPHA release and is not production-ready.
> This is an ALPHA release and is not production-ready.
> Please provide feedback and let us know if you have feature requests / bug reports!

## Features
Expand Down
186 changes: 186 additions & 0 deletions examples/basic.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
use stagehand_sdk::{
ActResponseEvent, AgentConfig, AgentExecuteOptions, Env, ExecuteResponseEvent,
ExtractResponseEvent, Model, ModelConfiguration, ObserveResponseEvent, Stagehand,
TransportChoice, V3Options,
};
use futures::StreamExt;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

#[derive(Serialize, Deserialize, Debug)]
struct Comment {
text: String,
author: String,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// Load environment variables from .env file
dotenvy::dotenv().ok();

println!("=== Stagehand Rust SDK Example ===\n");

// Environment variables required:
// - BROWSERBASE_API_KEY
// - BROWSERBASE_PROJECT_ID
// - A model API key (OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_GENERATIVE_AI_API_KEY, etc.)

// 1. Initialize client with API keys (from environment variables)
println!("1. Connecting to Stagehand...");
let mut stagehand = Stagehand::connect(TransportChoice::default_rest()).await?;
println!(" Connected!\n");

// 2. Start session with model_name (NO deprecated headers)
println!("2. Starting browser session...");
let opts = V3Options {
env: Some(Env::Browserbase),
model: Some(Model::String("openai/gpt-5-nano".into())),
verbose: Some(2),
..Default::default()
};

stagehand.start(opts).await?;
println!(" Session ID: {:?}\n", stagehand.session_id());

// 3. Navigate to https://news.ycombinator.com
println!("3. Navigating to Hacker News...");
let mut act_stream = stagehand
.act(
"Navigate to https://news.ycombinator.com",
None,
HashMap::new(),
Some(60_000),
None,
)
.await?;

while let Some(res) = act_stream.next().await {
if let Ok(response) = res {
if let Some(ActResponseEvent::Success(success)) = response.event {
println!(" Navigation success: {}\n", success);
}
}
}

// 4. Observe to find "link to view comments for the top post"
println!("4. Finding link to view comments for the top post...");
let mut observe_stream = stagehand
.observe(
Some("Find the link to view comments for the top post".to_string()),
None,
Some(60_000),
None,
None,
)
.await?;

let mut elements_json = String::new();
while let Some(res) = observe_stream.next().await {
if let Ok(response) = res {
if let Some(ObserveResponseEvent::ElementsJson(json)) = response.event {
elements_json = json;
println!(" Found elements!\n");
}
}
}

// 5. Act on the first action from observe results
println!("5. Clicking on the comments link...");
let mut act_stream = stagehand
.act(
"Click on the comments link for the top post",
None,
HashMap::new(),
Some(60_000),
None,
)
.await?;

while let Some(res) = act_stream.next().await {
if let Ok(response) = res {
if let Some(ActResponseEvent::Success(success)) = response.event {
println!(" Click success: {}\n", success);
}
}
}

// 6. Extract top comment text + author using JSON schema
println!("6. Extracting top comment and author...");
let schema = serde_json::json!({
"type": "object",
"properties": {
"text": { "type": "string", "description": "The text content of the top comment" },
"author": { "type": "string", "description": "The username of the comment author" }
},
"required": ["text", "author"]
});

let mut extract_stream = stagehand
.extract(
"Extract the text and author of the top comment on the page",
schema,
None,
Some(60_000),
None,
None,
)
.await?;

let mut comment_data = String::new();
while let Some(res) = extract_stream.next().await {
if let Ok(response) = res {
if let Some(ExtractResponseEvent::DataJson(json)) = response.event {
comment_data = json.clone();
if let Ok(comment) = serde_json::from_str::<Comment>(&json) {
println!(" Top comment by {}: {}\n", comment.author, comment.text);
}
}
}
}

// 7. Execute autonomous agent to find author's profile (GitHub/LinkedIn/website)
println!("7. Finding author's profile using autonomous agent...");
let agent_config = AgentConfig {
provider: None,
model: Some(ModelConfiguration::String("openai/gpt-5-nano".into())),
system_prompt: None,
cua: None,
};

let comment: Comment = serde_json::from_str(&comment_data)?;
let execute_options = AgentExecuteOptions {
instruction: format!(
"Find the profile page for the Hacker News user '{}'. Look for links to their GitHub, LinkedIn, or personal website.",
comment.author
),
max_steps: Some(10),
highlight_cursor: None,
};

let mut execute_stream = stagehand
.execute(agent_config, execute_options, None)
.await?;

while let Some(res) = execute_stream.next().await {
if let Ok(response) = res {
match response.event {
Some(ExecuteResponseEvent::Log(log)) => {
println!(" [Agent Log] {:?}", log);
}
Some(ExecuteResponseEvent::ResultJson(result)) => {
println!(" Agent result: {}\n", result);
}
_ => {}
}
}
}

// 8. End session
println!("8. Closing session...");
stagehand.end().await?;
println!(" Session closed successfully!\n");

println!("=== Example completed! ===");

Ok(())
}