Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "3.5.0"
".": "3.6.0"
}
6 changes: 3 additions & 3 deletions .stats.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
configured_endpoints: 8
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-43e6dd4ce19381de488d296e9036fea15bfea9a6f946cf8ccf4e02aecc8fb765.yml
openapi_spec_hash: f736e7a8acea0d73e1031c86ea803246
config_hash: b375728ccf7d33287335852f4f59c293
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-8fbb3fa8f3a37c1c7408de427fe125aadec49f705e8e30d191601a9b69c4cc41.yml
openapi_spec_hash: 8a36f79075102c63234ed06107deb8c9
config_hash: 4252fc025e947bc0fd6b2abd91a0cc8e
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
# Changelog

## 3.6.0 (2026-02-13)

Full Changelog: [v3.5.0...v3.6.0](https://github.com/browserbase/stagehand-python/compare/v3.5.0...v3.6.0)

### Features

* Add executionModel serialization to api client ([22dd688](https://github.com/browserbase/stagehand-python/commit/22dd68831f5b599dc070798bb991b349211631d9))
* **client:** add custom JSON encoder for extended type support ([f9017c8](https://github.com/browserbase/stagehand-python/commit/f9017c8fff8c58992739c6924ed6efbae552e027))


### Chores

* format all `api.md` files ([c22d22c](https://github.com/browserbase/stagehand-python/commit/c22d22cd79700c3c12462f20e3ebad54b925968f))
* **internal:** bump dependencies ([92d8393](https://github.com/browserbase/stagehand-python/commit/92d83930190c30b1d4653b78eb2a6e8d28225fa5))
* **internal:** codegen related update ([aa3fbd4](https://github.com/browserbase/stagehand-python/commit/aa3fbd46cfd8e3ad4f4db6724c14d43db52564b6))
* **internal:** codegen related update ([555a9c4](https://github.com/browserbase/stagehand-python/commit/555a9c44a902a6735e585e09ed974d9a7915a6bb))
* **internal:** fix lint error on Python 3.14 ([b0df744](https://github.com/browserbase/stagehand-python/commit/b0df7441a5a50cc8933d3f0edbc46561219d9fba))
* sync repo ([0c9bb8c](https://github.com/browserbase/stagehand-python/commit/0c9bb8cb3b791bf8c60ad0065fed9ad16b912b8e))

## 3.5.0 (2026-01-29)

Full Changelog: [v3.4.8...v3.5.0](https://github.com/browserbase/stagehand-python/compare/v3.4.8...v3.5.0)
Expand Down
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,15 @@ Python 3.9 or higher.

A complete working example is available at [`examples/full_example.py`](examples/full_example.py).

Before running examples, set up the example environment file:

```bash
cp examples/.env.example examples/.env
# Edit examples/.env with your credentials.
```

The examples load `examples/.env` automatically.

To run it, first export the required environment variables, then use Python:

```bash
Expand Down Expand Up @@ -132,6 +141,22 @@ See [`examples/logging_example.py`](examples/logging_example.py) for a remote-on
uv run python examples/logging_example.py
```

## Remote Playwright (SSE) example

See [`examples/remote_browser_playwright_example.py`](examples/remote_browser_playwright_example.py) for a remote Browserbase flow that attaches Playwright via CDP and streams SSE events for observe/act/extract/execute.

```bash
uv run python examples/remote_browser_playwright_example.py
```

## Local Playwright (SSE) example

See [`examples/local_browser_playwright_example.py`](examples/local_browser_playwright_example.py) for a local Stagehand flow that launches Playwright locally, shares its CDP URL with Stagehand, and streams SSE events for observe/act/extract/execute.

```bash
uv run python examples/local_browser_playwright_example.py
```

<details>
<summary><strong>Local development</strong></summary>

Expand Down
4 changes: 4 additions & 0 deletions examples/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
STAGEHAND_API_URL=https://api.stagehand.browserbase.com
MODEL_API_KEY=sk-proj-your-llm-api-key-here
BROWSERBASE_API_KEY=bb_live_your_api_key_here
BROWSERBASE_PROJECT_ID=your-bb-project-uuid-here
47 changes: 42 additions & 5 deletions examples/act_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,36 @@
- MODEL_API_KEY: Your OpenAI API key
"""

from __future__ import annotations

import os

from env import load_example_env

from stagehand import AsyncStagehand


async def _stream_to_result(stream, label: str) -> object | None:
result_payload: object | None = None
async for event in stream:
if event.type == "log":
print(f"[{label}][log] {event.data.message}")
continue

status = event.data.status
print(f"[{label}][system] status={status}")
if status == "finished":
result_payload = event.data.result
elif status == "error":
error_message = event.data.error or "unknown error"
raise RuntimeError(f"{label} stream reported error: {error_message}")

return result_payload


async def main() -> None:
load_example_env()
load_example_env()
# Create client using environment variables
# BROWSERBASE_API_KEY, BROWSERBASE_PROJECT_ID, MODEL_API_KEY
async with AsyncStagehand(
Expand All @@ -44,13 +68,26 @@ async def main() -> None:
# Call act() with a string instruction directly
# This is the key test - passing a string instead of an Action object
print("\nAttempting to call act() with string input...")
act_response = await session.act(
input="click the 'More information' link", # String instruction
act_stream = await session.act(
input="click the 'Learn more' link", # String instruction
stream_response=True,
x_stream_response="true",
)

print(f"Act completed successfully!")
print(f"Result: {act_response.data.result.message}")
print(f"Success: {act_response.data.result.success}")
act_result = await _stream_to_result(act_stream, "act")
result_message = (
act_result.get("message")
if isinstance(act_result, dict)
else act_result
)
result_success = (
act_result.get("success")
if isinstance(act_result, dict)
else None
)
print("Act completed successfully!")
print(f"Result: {result_message}")
print(f"Success: {result_success}")

except Exception as e:
print(f"Error: {e}")
Expand Down
34 changes: 31 additions & 3 deletions examples/agent_execute.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,37 @@
`PYTHONPATH=src .venv/bin/python examples/agent_execute_minimal.py`
"""

from __future__ import annotations

import os
import json

from env import load_example_env

from stagehand import AsyncStagehand, APIResponseValidationError


async def _stream_to_result(stream, label: str) -> object | None:
result_payload: object | None = None
async for event in stream:
if event.type == "log":
print(f"[{label}][log] {event.data.message}")
continue

status = event.data.status
print(f"[{label}][system] status={status}")
if status == "finished":
result_payload = event.data.result
elif status == "error":
error_message = event.data.error or "unknown error"
raise RuntimeError(f"{label} stream reported error: {error_message}")

return result_payload


async def main() -> None:
load_example_env()
load_example_env()
model_name = os.environ.get("STAGEHAND_MODEL", "openai/gpt-5-nano")

# Enable strict response validation so we fail fast if the API response
Expand All @@ -46,17 +70,21 @@ async def main() -> None:
options={"wait_until": "domcontentloaded"},
)

result = await session.execute(
stream = await session.execute(
agent_config={"model": model_name},
execute_options={
"instruction": "Go to Hacker News and return the titles of the first 3 articles.",
"max_steps": 5,
},
stream_response=True,
x_stream_response="true",
)

print("Agent message:", result.data.result.message)
result = await _stream_to_result(stream, "execute")
message = result.get("message") if isinstance(result, dict) else result
print("Agent message:", message)
print("\nFull result:")
print(json.dumps(result.data.result.to_dict(), indent=2, default=str))
print(json.dumps(result, indent=2, default=str))
finally:
await session.end()

Expand Down
32 changes: 28 additions & 4 deletions examples/byob_example.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
from __future__ import annotations

"""
Example showing how to bring your own browser driver while still using Stagehand.

Expand All @@ -20,15 +18,38 @@
```
"""

from __future__ import annotations

import os
import asyncio

from env import load_example_env
from playwright.async_api import async_playwright

from stagehand import AsyncStagehand


async def _stream_to_result(stream, label: str) -> object | None:
result_payload: object | None = None
async for event in stream:
if event.type == "log":
print(f"[{label}][log] {event.data.message}")
continue

status = event.data.status
print(f"[{label}][system] status={status}")
if status == "finished":
result_payload = event.data.result
elif status == "error":
error_message = event.data.error or "unknown error"
raise RuntimeError(f"{label} stream reported error: {error_message}")

return result_payload


async def main() -> None:
load_example_env()
load_example_env()
async with AsyncStagehand(
browserbase_api_key=os.environ.get("BROWSERBASE_API_KEY"),
browserbase_project_id=os.environ.get("BROWSERBASE_PROJECT_ID"),
Expand All @@ -55,16 +76,19 @@ async def main() -> None:
print("🔄 Syncing Stagehand to Playwright's current URL:", page.url)
await session.navigate(url=page.url)

extract_response = await session.extract(
extract_stream = await session.extract(
instruction="extract the text of the top comment on this page",
schema={
"type": "object",
"properties": {"comment": {"type": "string"}},
"required": ["comment"],
},
stream_response=True,
x_stream_response="true",
)

print("🧮 Stagehand extraction result:", extract_response.data.result)
extract_result = await _stream_to_result(extract_stream, "extract")
print("🧮 Stagehand extraction result:", extract_result)
finally:
await session.end()
await browser.close()
Expand Down
53 changes: 53 additions & 0 deletions examples/env.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from __future__ import annotations

import os
from pathlib import Path

REQUIRED_KEYS = {
"STAGEHAND_API_URL",
"MODEL_API_KEY",
"BROWSERBASE_API_KEY",
"BROWSERBASE_PROJECT_ID",
}


def _find_env_path() -> Path | None:
current = Path.cwd()
while True:
candidate = current / "examples" / ".env"
if candidate.exists():
return candidate
if current.parent == current:
return None
current = current.parent


def load_example_env() -> None:
env_path = _find_env_path()
if not env_path:
raise RuntimeError("Missing examples/.env (expected in repo examples/ directory).")

for line in env_path.read_text().splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
if "=" not in line:
continue
key, value = line.split("=", 1)
os.environ[key] = value

missing = [key for key in sorted(REQUIRED_KEYS) if not os.environ.get(key)]
if missing:
raise RuntimeError(
"Missing required env vars: "
+ ", ".join(missing)
+ " (from examples/.env)"
)

# Normalize for SDKs that expect STAGEHAND_BASE_URL
os.environ.setdefault("STAGEHAND_BASE_URL", os.environ["STAGEHAND_API_URL"])

# Use the repo-local SEA binary when available (avoid global installs).
sea_binary = env_path.parent.parent / "bin" / "sea" / "stagehand-darwin-arm64"
if sea_binary.exists():
os.environ.setdefault("STAGEHAND_SEA_BINARY", str(sea_binary))
Loading