Owl Browser Python SDK
Async Python client for the Owl Browser HTTP server: one method per browser tool, generated from the bundled OpenAPI schema.
Install
pip install owl-browser
Python 3.12+. Runtime dependencies: aiohttp, pyjwt[crypto], cryptography, beautifulsoup4.
Quick start
import asyncio
from owl_browser import OwlBrowser, RemoteConfig
async def main():
config = RemoteConfig(
url="http://localhost:8080",
token="your-token",
api_prefix="", # direct to http-server; keep the default "/api" behind nginx
)
async with OwlBrowser(config) as browser:
ctx = await browser.create_context()
await browser.navigate(context_id=ctx, url="https://example.com", wait_until="load")
text = await browser.extract_text(context_id=ctx)
print(text)
await browser.close_context(context_id=ctx)
asyncio.run(main())
async with calls connect() on entry and close() on exit. Without it, call them yourself.
create_context() returns the context id as a str. Every other tool takes context_id as a keyword argument.
Configuration
RemoteConfig is a dataclass in owl_browser.types (re-exported from owl_browser).
| Field | Type | Default | Meaning |
|---|---|---|---|
url |
str |
required | Server base URL. A trailing / is stripped. |
token |
str | None |
None |
Bearer token. Required when auth_mode is TOKEN. |
auth_mode |
AuthMode |
AuthMode.TOKEN |
TOKEN or JWT. |
jwt |
JWTConfig | None |
None |
Required when auth_mode is JWT. |
transport |
TransportMode |
TransportMode.HTTP |
HTTP or WEBSOCKET. |
timeout |
float |
30.0 |
Per-request timeout in seconds. |
max_concurrent |
int |
10 |
Concurrent in-flight requests (semaphore and aiohttp connector limit). |
retry |
RetryConfig |
RetryConfig() |
Retry policy, HTTP transport only. |
verify_ssl |
bool |
True |
TLS certificate verification. |
api_prefix |
str |
"/api" |
Path prefix for every request. Normalised to a leading / with no trailing /. |
__post_init__ raises ValueError if auth_mode is TOKEN with no token, or JWT with no jwt.
Paths are built as {url}{api_prefix}/execute/{tool}, so use the default "/api" behind the nginx proxy and api_prefix="" when talking to the http-server directly on port 8080.
RetryConfig: max_retries=3 (total attempts, not extra attempts), initial_delay_ms=100, max_delay_ms=10000, backoff_multiplier=2.0, jitter_factor=0.1.
from owl_browser import RemoteConfig, RetryConfig
config = RemoteConfig(
url="https://your-host",
token="your-token",
timeout=30.0,
max_concurrent=10,
retry=RetryConfig(max_retries=5, initial_delay_ms=200),
)
JWT (RS256, key read from disk, refreshed automatically before expiry):
from owl_browser import RemoteConfig, AuthMode, JWTConfig
config = RemoteConfig(
url="https://your-host",
auth_mode=AuthMode.JWT,
jwt=JWTConfig(
private_key_path="/path/to/private.pem",
expires_in=3600,
refresh_threshold=300,
issuer="my-app",
subject="user-123",
),
)
timeout applies per request, with one exception: over the HTTP transport a fixed set of slow tools (navigation, waits, content extraction, screenshots, CAPTCHA and the AI tools) uses max(120.0, timeout * 4) instead. Anything slower than that belongs in a job, see below.
Calling tools
execute() runs any tool by its server name:
result = await browser.execute(
"browser_navigate",
context_id=ctx,
url="https://example.com",
)
tool_name is positional-only, so tools that themselves take a tool_name parameter work:
await browser.execute(
"browser_webmcp_call_tool",
context_id=ctx,
tool_name="echo",
input='{"message":"hi"}',
)
At construction the client reads the bundled OpenAPI schema and builds one async method per tool, named without the browser_ prefix. These are equivalent:
await browser.execute("browser_click", context_id=ctx, selector="#submit")
await browser.click(context_id=ctx, selector="#submit")
Tools whose names do not start with browser_ (for example http_request, server_read_logs) keep their full name as the method name. All parameters are keyword arguments. Fields the schema declares as integers are coerced from float to int before the request is sent.
create_context is the one name that is not dynamic: it is a real method that unwraps the response and returns the context id string, raising OwlBrowserError if the server returns anything else.
Introspection is synchronous and offline (it reads the bundled schema, not the server):
browser.list_tools() # ['browser_navigate', 'browser_click', ...] server tool names
browser.list_methods() # ['navigate', 'click', ...] generated method names
browser.has_method("click") # True
browser.get_tool("browser_click") # ToolDefinition | None
ToolDefinition carries name, description, parameters (a dict of ParameterDef), required_params and integer_fields.
Two more client methods: health_check() (HTTP transport only) and the sync wrappers connect_sync(), execute_sync(), close_sync(). Each sync wrapper calls asyncio.run() on a fresh event loop, so they do not share a connection; prefer the async API.
Long running work
An agentic run (browser_nla) routinely takes minutes and will blow past the server's request timeout if you run it synchronously. The connection is dropped while the browser keeps working and the result is lost. Submit it as a background job instead.
run_task
This is the one to use. One call: it submits the job, polls it, and returns the unwrapped answer.
answer = await browser.run_task(ctx, "Find the cheapest book and report its price")
run_task(context_id, command, *, timeout=600.0, poll_interval=2.0, **params) -> Any
timeout and poll_interval are in seconds. Extra keyword arguments are passed through to browser_nla. It raises OwlBrowserError if the job fails, is cancelled, or does not finish within timeout (the job keeps running server-side in the timeout case).
Worked example. browser_nla needs an LLM on the context, either the built-in model or a third-party endpoint:
import asyncio
from owl_browser import OwlBrowser, RemoteConfig
URL = "https://books.toscrape.com/catalogue/category/books/travel_2/index.html"
async def main():
config = RemoteConfig(url="http://localhost:8080", token="your-token", api_prefix="")
async with OwlBrowser(config) as browser:
ctx = await browser.create_context(
render_mode="agent",
llm_use_builtin=False,
llm_is_third_party=True,
llm_endpoint="http://192.168.1.197:1234",
llm_model="qwen/qwen3.6-27b",
llm_api_key="lmstudio",
)
await browser.navigate(context_id=ctx, url=URL, wait_until="load")
answer = await browser.run_task(
ctx,
"Open the product page for 'The Great Railway Bazaar' and report how "
"many copies are in stock. Answer with just the number.",
)
print(answer) # -> the answer payload, envelopes already stripped
await browser.close_context(context_id=ctx)
asyncio.run(main())
The job API
Use these when you want the job id itself: to run several tasks at once, to report progress, or to cancel.
job_id = await browser.submit_job("browser_nla", context_id=ctx, command="...")
job = await browser.get_job(job_id) # dict for one job
jobs = await browser.list_jobs() # list of dicts, no result payloads
await browser.cancel_job(job_id) # dict, the job as it stands after the request
result = await browser.wait_for_job(job_id, timeout=600.0, poll_interval=2.0)
submit_job(tool_name, /, **params) -> strsetsasync=Trueon the payload and returns the server'sjob_id. It works for any tool, not justbrowser_nla. It raisesOwlBrowserErrorif the server does not return a job id.get_job(job_id)returns{"job_id", "state", "tool", "created_ms", "updated_ms"}plus"result"and"error"once they exist. An unknown id raisesOwlBrowserError, it does not hang or returnNone.list_jobs()returns the live jobs, newest first, without their result payloads.cancel_job(job_id)requests cancellation, see the state notes below.wait_for_job(job_id, *, timeout=600.0, poll_interval=2.0)polls until the job leaves the non-terminal states, then returns the unwrapped result. RaisesOwlBrowserErroronfailed, oncancelled, and on its own timeout.
run_task is exactly submit_job("browser_nla", ...) followed by wait_for_job(...).
Job states
queued, running, done, failed, cancelled, and cancelling.
Cancellation is cooperative. A queued job cancels immediately. A running job that is inside a browser call cannot be interrupted, so cancel_job only records the intent: the job reports cancelling until the in-flight call returns, and only then becomes cancelled. Do not treat cancelling as terminal.
Results
Job results are stored as the raw tool response, which means a REST envelope and, for browser_nla, a JSON string inside it. run_task and wait_for_job peel those layers off and hand back the payload. Reading job["result"] from get_job() yourself gives you the unpeeled string.
Requirements
Async jobs are plain REST endpoints under /jobs and exist only on the HTTP transport. A client built with transport=TransportMode.WEBSOCKET raises OwlBrowserError telling you so.
Jobs are held in memory per server process. They do not survive a server restart, and neither does the browser context they were driving.
Errors
Every exception derives from OwlBrowserError, importable from owl_browser.
| Exception | Raised when | Extra attributes |
|---|---|---|
OwlBrowserError |
Base class. Also raised directly for job failure, cancellation and polling timeouts, for job calls on a WebSocket client, for health_check() on a WebSocket client, for a bad create_context response, and for non-2xx replies from /jobs. |
|
ConnectionError |
Transport cannot connect, retries are exhausted, or the WebSocket is closed or not connected. | message, cause |
AuthenticationError |
HTTP 401, or a WebSocket handshake rejected with 401. | message, reason, status_code |
IPBlockedError |
HTTP 403. | message, ip_address, status_code |
RateLimitError |
HTTP 429. | message, retry_after, limit, remaining, status_code |
ToolExecutionError |
Any other HTTP status at or above 400, or a 2xx envelope with success: false. |
tool_name, message, status, result |
TimeoutError |
The request exceeds the transport timeout. | message, timeout_ms |
OpenAPISchemaError |
The bundled schema is missing or unparseable. | message, cause |
ConnectionError and TimeoutError shadow the Python builtins of the same name. Import them explicitly, or alias them, if you also catch the builtins.
from owl_browser import (
AuthenticationError,
ConnectionError,
OwlBrowserError,
RateLimitError,
TimeoutError,
ToolExecutionError,
)
async def click(browser, ctx):
try:
await browser.click(context_id=ctx, selector="#missing")
except ToolExecutionError as e:
print(f"{e.tool_name} failed: {e.message}")
except RateLimitError as e:
print(f"rate limited, retry after {e.retry_after}s")
except (AuthenticationError, ConnectionError, TimeoutError) as e:
print(f"transport problem: {e}")
except OwlBrowserError as e:
print(f"other SDK error: {e}")
401, 403 and 429 are never retried. Other connection and response errors are retried per RetryConfig.
owl_browser.exceptions also defines ElementNotFoundError, NavigationError, ContextLimitError, ExpectationError and FlowExecutionError. Apart from FlowExecutionError (used by the flow executor), the SDK does not raise these on its own: they come from the raise_for_action_result(result) helper in the same module, which you call on a tool result when you want a failed action to become an exception.
Transports
Default is HTTP. Select the WebSocket transport on the config:
from owl_browser import RemoteConfig, TransportMode
config = RemoteConfig(
url="http://localhost:8080",
token="your-token",
transport=TransportMode.WEBSOCKET,
)
Both transports expose the same execute() and the same generated methods. What differs:
| HTTP | WebSocket | |
|---|---|---|
| Endpoint | POST {api_prefix}/execute/{tool} |
one connection to {api_prefix}/ws, requests correlated by integer id |
| Retries | per RetryConfig |
none |
| Timeout | timeout, raised to max(120.0, timeout * 4) for known slow tools |
timeout for every call, no exception list |
| Concurrency | limited by max_concurrent |
multiplexed over the single connection, not limited by max_concurrent |
health_check() |
supported | raises OwlBrowserError |
| Async jobs | supported | raises OwlBrowserError |
| Non-JSON responses | returned as text | not applicable, frames are JSON |
Use HTTP unless you specifically need a persistent connection. It is the only transport with retries, extended timeouts for slow tools, and the job API.
Also in the package
Three optional layers ship alongside the tool client. Each is independent, and none of them is needed for the sections above.
| Import | What it is |
|---|---|
from owl_browser import FlowExecutor |
Runs declarative JSON flows: steps, captured variables, for_each, retries, expectations. See docs/FLOW_EXECUTOR.md. |
from owl_browser import Extractor |
Field specs to pull structured records out of a page. |
from owl_browser.playwright import chromium |
A Playwright-shaped facade (Browser, BrowserContext, Page) over the same client, for porting existing scripts. |
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file owl_browser-2.1.6.tar.gz.
File metadata
- Download URL: owl_browser-2.1.6.tar.gz
- Upload date:
- Size: 156.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.1.0 CPython/3.12.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d5044f2a8fe04a4a2e7a3ee8e5eb636efc48285b1baf45f79225b0c5799b79e9
|
|
| MD5 |
2540d9e572dcfe72be99f042d45724d7
|
|
| BLAKE2b-256 |
1a527f975df8a4c2a2e3123118e49300c65606e24940ab9e3ee88f2f937e159b
|
File details
Details for the file owl_browser-2.1.6-py3-none-any.whl.
File metadata
- Download URL: owl_browser-2.1.6-py3-none-any.whl
- Upload date:
- Size: 180.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.1.0 CPython/3.12.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ffbe0d9ba023d32758ebc2737c8b543399414cc7b22f2be7682634622f5e5050
|
|
| MD5 |
833b8736ae34a145920169c249b7f4bb
|
|
| BLAKE2b-256 |
2f205df21e01d9008367945e4a7d132231188382791ad61ece68d95ce4021e52
|