@olib-ai/owl-browser-sdk
v2.1.6
Published
Node.js SDK for Owl Browser automation - Async-first with dynamic OpenAPI method generation
Maintainers
Readme
@olib-ai/owl-browser-sdk
Node.js / TypeScript client for the Owl Browser HTTP server: 185 browser tools, one class, ESM and async throughout.
Install
npm install @olib-ai/owl-browser-sdkNode 18 or newer. The package is ESM only ("type": "module").
Quick start
import { OwlBrowser } from '@olib-ai/owl-browser-sdk';
const browser = new OwlBrowser({
url: 'http://127.0.0.1:8080',
token: 'your-secret-token',
apiPrefix: '', // '' when talking to the server directly, '/api' behind nginx
});
await browser.connect();
const contextId = await browser.createContext();
await browser.navigate({ context_id: contextId, url: 'https://example.com' });
const markdown = await browser.getMarkdown({ context_id: contextId });
console.log(markdown);
await browser.closeContext({ context_id: contextId });
await browser.close();createContext() returns the context ID as a string. Every other tool takes it as
the context_id parameter.
Configuration
The OwlBrowser constructor takes one RemoteConfig object.
| Field | Type | Default | Notes |
|---|---|---|---|
| url | string | required | Server base URL. Trailing slashes are stripped. |
| token | string | required for token auth | Bearer token. The transport throws if it is missing and JWT auth is not configured. |
| authMode | AuthMode | AuthMode.TOKEN | AuthMode.JWT switches to RS256 JWT auth. jwt must also be set, otherwise the client falls back to token auth. |
| jwt | JWTConfig | none | See below. |
| transport | TransportMode | TransportMode.HTTP | TransportMode.WEBSOCKET for the WebSocket transport. |
| timeout | number | 30 | Request timeout in seconds. Long running tools get max(120s, timeout * 4) on the HTTP transport. |
| maxConcurrent | number | 10 | In-process cap on concurrent HTTP requests. Extra calls queue. |
| retry | RetryConfig | see below | HTTP transport only. |
| verifySsl | boolean | true | Accepted for parity with the Python SDK. The Node transports do not currently act on it. |
| apiPrefix | string | '/api' | Prefixed to every path. Pass '' for a direct connection to the browser server. |
RetryConfig defaults: maxRetries: 3 (total attempts, not extra attempts),
initialDelayMs: 100, maxDelayMs: 10000, backoffMultiplier: 2.0,
jitterFactor: 0.1. Retries apply to connection level failures only. Timeouts,
auth failures, rate limits and IP blocks are raised immediately.
JWT auth:
import { OwlBrowser, AuthMode } from '@olib-ai/owl-browser-sdk';
const browser = new OwlBrowser({
url: 'http://127.0.0.1:8080',
authMode: AuthMode.JWT,
jwt: {
privateKeyPath: '/path/to/private.pem', // path or an inline PEM string
expiresIn: 3600, // seconds, default 3600
refreshThreshold: 300, // re-sign this many seconds before expiry
issuer: 'my-app',
subject: 'automation',
audience: 'owl-browser',
claims: { team: 'growth' },
},
});Calling tools
execute(toolName, params, options?) calls any tool by its full name and returns
the unwrapped result field of the server response.
const result = await browser.execute('browser_navigate', {
context_id: contextId,
url: 'https://example.com',
wait_until: 'load',
});options.signal accepts an AbortSignal to cancel an in-flight request.
Every tool in the bundled OpenAPI schema also gets a generated method, under both
its camelCase and its snake_case name, with the browser_ prefix removed. These
are exactly equivalent to execute():
await browser.getMarkdown({ context_id: contextId });
await browser.get_markdown({ context_id: contextId });
await browser.execute('browser_get_markdown', { context_id: contextId });Parameters are passed through as the server names them (context_id, not
contextId). Fields typed as integer in the schema are floored before sending.
Introspection:
browser.listTools(); // 185 full tool names: 'browser_create_context', ...
browser.listMethods(); // 353 generated method names, camelCase and snake_case
browser.hasMethod('getMarkdown'); // true
const def = browser.getTool('browser_navigate');
def.name; // 'browser_navigate'
def.description;
def.requiredParams; // ['context_id', 'url']
def.parameters; // { context_id: {name, type, required, description, enumValues, default}, ... }listTools() and getTool() take full tool names (browser_navigate).
listMethods() and hasMethod() take generated method names (navigate).
Both read the schema bundled with the package, so they work before connect()
and without a server.
A handful of methods are hand written for better types and behaviour and take
precedence over the generated ones: createContext, create_context,
closeContext, closeAllContexts, navigate, click, type, screenshot,
waitForSelector, getHtml, getMarkdown, evaluate.
healthCheck() returns the server's health JSON. It is HTTP only.
Long running work
An agentic run (browser_nla) routinely takes minutes. Run synchronously it
exceeds the server's default 30 second request timeout, the connection is
dropped, and the result is lost even though the browser finished the work. The
SDK submits it as a background job and polls instead.
runTask
This is the one to use. One call submits the job, polls it, and returns the unwrapped answer.
import { OwlBrowser } from '@olib-ai/owl-browser-sdk';
const browser = new OwlBrowser({ url: 'http://127.0.0.1:8080', token: 't', apiPrefix: '' });
await browser.connect();
// browser_nla needs an LLM on the context: the built-in model, or your own.
const contextId = await browser.createContext({
render_mode: 'agent',
llm_is_third_party: true,
llm_use_builtin: false,
llm_endpoint: 'http://127.0.0.1:1234',
llm_model: 'qwen/qwen3.6-27b',
llm_api_key: process.env.LLM_API_KEY,
});
await browser.navigate({
context_id: contextId,
url: 'https://books.toscrape.com/catalogue/category/books/travel_2/index.html',
});
const answer = await browser.runTask({
contextId,
command: "Open the product page for 'The Great Railway Bazaar' and report how "
+ 'many copies are in stock. Answer with just the number.',
});
console.log(answer); // the tool's own result, envelopes already stripped
await browser.closeContext({ context_id: contextId });
await browser.close();runTask({ contextId, command, timeoutMs?, pollIntervalMs?, ...rest })
contextIdandcommandmap to thecontext_idandcommandparameters ofbrowser_nla.timeoutMsdefaults to600000(10 minutes). It bounds the client's polling, not the server. On expiryrunTaskrejects and the job keeps running server side.pollIntervalMsdefaults to2000.- Any other key is passed straight through to the tool.
- Rejects with
OwlBrowserErrorif the job fails or is cancelled.
The job API
Use these when you need the job ID itself, for example to report progress or to cancel from elsewhere.
const jobId = await browser.submitJob('browser_nla', {
context_id: contextId,
command: 'How many books are listed on this page? Answer with just the number.',
});
const job = await browser.getJob(jobId);
// { job_id, state, tool, created_ms, updated_ms, result?, error? }
const answer = await browser.waitForJob(jobId, { timeoutMs: 600000, pollIntervalMs: 2000 });submitJob(toolName, params)returns a job ID string. It works for any tool, not onlybrowser_nla.getJob(jobId)returns the raw job record. Rejects withOwlBrowserErrorif the ID is unknown.listJobs()returns every live job on the server as an array, without result payloads.cancelJob(jobId)requests cancellation and returns the job record.waitForJob(jobId, { timeoutMs, pollIntervalMs })polls until the job reaches a terminal state and returns the unwrapped result. Same defaults asrunTask.
waitForJob and runTask peel the transport envelopes off the stored result,
so you get the tool's answer rather than {success, result} wrapping a JSON
string.
Job states
| State | Meaning |
|---|---|
| queued | Accepted, not started. |
| running | Executing. |
| done | Finished, result is populated. |
| failed | Finished, error is populated. |
| cancelled | Stopped before completion. |
| cancelling | Cancel requested on a job that is already inside a browser call. |
A cancel cannot interrupt an in-flight browser call. Cancelling a queued job
moves it straight to cancelled. Cancelling a running job reports
cancelling until that call returns, then cancelled. Keep polling through
cancelling, it is not terminal.
Finished jobs are kept for 30 minutes, then reclaimed.
Transport requirement
The job endpoints are plain REST and exist only on the HTTP transport. A client
constructed with transport: TransportMode.WEBSOCKET rejects with
OwlBrowserError from getJob, listJobs, cancelJob, waitForJob and
runTask.
Errors
All errors extend OwlBrowserError, which extends Error.
| Error | Raised when |
|---|---|
| OwlBrowserError | Base class. Also raised directly for job failures, waitForJob client timeout, unknown job IDs, job calls on a WebSocket client, and a browser_create_context response with no usable ID. |
| ConnectionError | HTTP retries exhausted, a non-tool request failed at the network level, or a WebSocket connection failed or closed with requests pending. Carries cause. |
| AuthenticationError | HTTP 401, or a WebSocket handshake error mentioning 401. Carries reason and statusCode = 401. |
| ToolExecutionError | HTTP 4xx/5xx other than 401/403/429, a JSON body with success: false, or a WebSocket error frame. Carries toolName, status, result. |
| TimeoutError | The per-request transport timeout elapsed. Carries timeoutMs. |
| RateLimitError | HTTP 429. Carries retryAfter, limit, remaining, statusCode = 429. |
| IPBlockedError | HTTP 403. Carries ipAddress and statusCode = 403. |
| OpenAPISchemaError | The bundled schema is missing or unparseable. Carries cause. |
| RequestAbortedError | An options.signal you supplied was aborted. Not re-exported from the package root and not reachable through the package's exports map, so match on e.name === 'RequestAbortedError' or catch it as OwlBrowserError. |
import {
OwlBrowserError,
ToolExecutionError,
TimeoutError,
RateLimitError,
AuthenticationError,
} from '@olib-ai/owl-browser-sdk';
try {
await browser.click({ context_id: contextId, selector: '#nonexistent' });
} catch (e) {
if (e instanceof ToolExecutionError) {
console.log(e.toolName, e.status, e.message);
} else if (e instanceof TimeoutError) {
console.log('timed out after', e.timeoutMs, 'ms');
} else if (e instanceof RateLimitError) {
console.log('retry after', e.retryAfter, 'seconds');
} else if (e instanceof AuthenticationError) {
console.log(e.message);
}
}A failing tool surfaces as ToolExecutionError. ElementNotFoundError,
NavigationError, ContextLimitError, FlowExecutionError and
ExpectationError are exported but the transports never raise them. To get the
first two, run a result through the exported helper yourself:
import { raiseForActionResult, ElementNotFoundError } from '@olib-ai/owl-browser-sdk';
const result = await browser.click({ context_id: contextId, selector: '#maybe' });
try {
raiseForActionResult(result); // no-op unless result is a failed ActionResult
} catch (e) {
if (e instanceof ElementNotFoundError) console.log(e.selector);
}Transports
HTTP is the default and the fully featured one. WebSocket keeps a single connection open and multiplexes requests over it by numeric ID.
import { OwlBrowser, TransportMode } from '@olib-ai/owl-browser-sdk';
const browser = new OwlBrowser({
url: 'http://127.0.0.1:8080',
token: 'your-secret-token',
apiPrefix: '',
transport: TransportMode.WEBSOCKET, // connects to ws://127.0.0.1:8080/ws
});
await browser.connect();| | HTTP | WebSocket |
|---|---|---|
| Endpoint | POST {url}{apiPrefix}/execute/{tool} | ws(s)://{host}{apiPrefix}/ws |
| Connection | Per request, connect() only marks the client ready | One persistent socket, opened by connect() |
| Timeout | timeout, raised to max(120s, timeout * 4) for ~30 known long running tools | timeout for every call |
| Retry | Exponential backoff with jitter | None |
| Concurrency cap | maxConcurrent | Unbounded, limited by the server |
| healthCheck() | Supported | Throws OwlBrowserError |
| Async jobs | Supported | Throws OwlBrowserError |
| Error detail | 401 / 403 / 429 mapped to distinct classes | All server errors arrive as ToolExecutionError with toolName: 'unknown' |
Auth differs too: HTTP sends the bearer token on every request, so a JWT is re-signed as it approaches expiry. WebSocket sends it once in the handshake, so a long lived socket outlives its token unless you reconnect.
Pick WebSocket for many short calls where per-request overhead matters. Pick HTTP for everything else, and always for long running work.
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 |
|---|---|
| import { FlowExecutor } from '@olib-ai/owl-browser-sdk' | Runs declarative JSON flows: steps, captured variables, for_each, retries, expectations. See docs/FLOW_EXECUTOR.md. |
| import { Extractor } from '@olib-ai/owl-browser-sdk' | Field specs to pull structured records out of a page. |
| import { chromium } from '@olib-ai/owl-browser-sdk/playwright' | A Playwright-shaped facade (Browser, BrowserContext, Page) over the same client, for porting existing scripts. |
