Skip to main content

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) -> str sets async=True on the payload and returns the server's job_id. It works for any tool, not just browser_nla. It raises OwlBrowserError if 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 raises OwlBrowserError, it does not hang or return None.
  • 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. Raises OwlBrowserError on failed, on cancelled, 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

owl_browser-2.1.6.tar.gz (156.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

owl_browser-2.1.6-py3-none-any.whl (180.3 kB view details)

Uploaded Python 3

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

Hashes for owl_browser-2.1.6.tar.gz
Algorithm Hash digest
SHA256 d5044f2a8fe04a4a2e7a3ee8e5eb636efc48285b1baf45f79225b0c5799b79e9
MD5 2540d9e572dcfe72be99f042d45724d7
BLAKE2b-256 1a527f975df8a4c2a2e3123118e49300c65606e24940ab9e3ee88f2f937e159b

See more details on using hashes here.

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

Hashes for owl_browser-2.1.6-py3-none-any.whl
Algorithm Hash digest
SHA256 ffbe0d9ba023d32758ebc2737c8b543399414cc7b22f2be7682634622f5e5050
MD5 833b8736ae34a145920169c249b7f4bb
BLAKE2b-256 2f205df21e01d9008367945e4a7d132231188382791ad61ece68d95ce4021e52

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

2.1.6 This release

2 files

2.1.5

2 files

2.1.4

2 files

2.1.3

2 files

2.1.2

2 files

2.1.1

2 files

2.1.0

2 files

2.0.9

2 files

2.0.7

2 files

2.0.6

2 files

2.0.5

2 files

2.0.4

2 files

2.0.3

2 files

2.0.2

2 files

2.0.1

2 files

2.0.0

2 files

1.2.6

2 files

1.2.5

2 files

1.2.4

2 files

1.2.3

2 files

1.2.2

2 files

1.0.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page