@render-lab/tasks-browserbase
v0.1.2
Published
> ⚠️ **Experimental: proof of concept.** This package is part of the [Render Tasks](https://github.com/render-lab/render-tasks) POC and is published for testing only. It is not fully tested or production ready. Task names, inputs, outputs, and behavior ca
Readme
@render-lab/tasks-browserbase
⚠️ Experimental: proof of concept. This package is part of the Render Tasks POC and is published for testing only. It is not fully tested or production ready. Task names, inputs, outputs, and behavior can change or break in any release. Pin exact versions and expect breaking changes.
Durable Browserbase tasks for Render Workflows.
This pack drives Browserbase's managed Chrome sessions from durable workflow tasks. It uses the official @browserbasehq/sdk for the control plane (contexts, sessions, uploads, downloads, logs, replay) and playwright-core for browser automation over CDP.
Reconnect-per-operation. Nothing holds a live browser between tasks. Each browser operation (runActions, captureScreenshot) retrieves the session's debug WebSocket URL, calls chromium.connectOverCDP, drives the first page, and always closes the local CDP connection in finally. Closing that local connection never releases a keepAlive session — so the next task reconnects to the same session — and closeSession is the only thing that sends REQUEST_RELEASE. This keeps every task a short-lived, restartable, observable durable run.
import {
createContext,
deleteContext,
createSession,
getSession,
closeSession,
runActions,
captureScreenshot,
uploadFiles,
listDownloads,
getDownload,
getSessionLogs,
getSessionReplay,
} from "@render-lab/tasks-browserbase";Tasks
| Task | Input | Output | Retry |
| --- | --- | --- | --- |
| browserbase.createContext | CreateContextInput | CreateContextResult | none (0 retries) |
| browserbase.deleteContext | DeleteContextInput | DeleteContextResult | convergent (3× backoff) |
| browserbase.createSession | CreateSessionInput | CreateSessionResult | convergent (3× backoff) |
| browserbase.getSession | SessionIdInput | SessionDTO | read (3× backoff) |
| browserbase.closeSession | SessionIdInput | CloseSessionResult | convergent (3× backoff) |
| browserbase.runActions | RunActionsInput | RunActionsResult | none (0 retries) |
| browserbase.captureScreenshot | CaptureScreenshotInput | CaptureScreenshotResult | none (0 retries) |
| browserbase.uploadFiles | UploadFilesInput | UploadFilesResult | none (0 retries) |
| browserbase.listDownloads | ListDownloadsInput | ListDownloadsResult | read (3× backoff) |
| browserbase.getDownload | GetDownloadInput | GetDownloadResult | read (3× backoff) |
| browserbase.getSessionLogs | GetSessionLogsInput | GetSessionLogsResult | read (3× backoff) |
| browserbase.getSessionReplay | GetSessionReplayInput | GetSessionReplayResult | read (3× backoff) |
Retry classes (from retry.ts; the SDK is built with maxRetries: 0, so the durable task retry is the single source of truth):
- none —
createContext,runActions,captureScreenshot, anduploadFiles. A context create has no find-or-create, and browser actions, screenshots, and uploads are side effects; a silent replay after an ambiguous failure could leak a paid context, re-run a click, or double-upload. Every attempt is one observable run. - convergent —
createSession(find-or-create by reserved metadata),closeSession(reads first, releases an active session once), anddeleteContext(a repeated/404 delete normalizes todeleted: false). A retry settles on the same terminal state instead of duplicating work. - read —
getSession,listDownloads,getDownload,getSessionLogs, andgetSessionReplay. Safe to repeat, so transient network/rate-limit/5xx failures retry with backoff over ~1s, 2s, 4s.
Idempotent sessions
createSession requires a caller idempotencyKey. The key is stored in the reserved _render_tasks_idempotency_key user-metadata field (the port overwrites any caller attempt to set it). Before creating, the task queries for an existing session with that key and returns it with created: false — even if it is COMPLETED, ERROR, or TIMED_OUT — so a retried create never starts a second paid session. A fresh key returns created: true.
const ctx = await createContext({});
const session = await createSession({
idempotencyKey: `crawl:${jobId}`,
contextId: ctx.contextId,
keepAlive: true, // let each task reconnect to the same session
});
await runActions({
sessionId: session.sessionId,
actions: [
{ type: "goto", url: "https://example.com" },
{ type: "fill", selector: "#q", value: "render workflows" },
{ type: "press", key: "Enter" },
{ type: "waitForSelector", selector: ".results", state: "visible" },
{ type: "extractText", selector: ".results", maxBytes: 262_144 },
],
});
const shot = await captureScreenshot({
sessionId: session.sessionId,
fullPage: true,
maxBytes: 524_288,
});
// Session and context IDs are the only handles that cross task boundaries.
await closeSession({ sessionId: session.sessionId }); // sends REQUEST_RELEASE
await deleteContext({ contextId: ctx.contextId });Session and context IDs are the only handles that cross a task boundary. No connectUrl, debug URL, Playwright object, Buffer, stream, or Date is ever returned — only JSON-serializable DTOs (ADR-0003).
Browser actions
runActions accepts 1 to 50 actions and runs them in order against the first page, returning the resulting URL and title after each so partial progress is diagnosable. Supported action types: goto, click, fill, press, select, waitForSelector, and extractText (bounded to maxBytes, default 256 KiB). The action switch is exhaustive with a compile-time never check — an unknown action type is a type error, not a permissive default. actionTimeoutMs defaults to 30,000 and caps at 120,000. Actions get zero retries: an ambiguous connection failure never silently replays a side-effecting action.
Payload limits and the 4 MB cap
Every artifact-bearing task returns a bounded shape from @render-lab/tasks-core (ADR-0020) so a task result can never exceed the Render Workflows 4 MB argument/result cap. Binary is all-or-nothing (a partial screenshot or download is misleading) and rejects before returning; text truncates on a UTF-8 boundary and reports the original byte count.
| Task | Field | Default | Maximum |
| --- | --- | --- | --- |
| runActions (extractText) | maxBytes | 262,144 | 1,048,576 |
| captureScreenshot | maxBytes | 524,288 | 2,097,152 |
| uploadFiles | maxTotalBytes (decoded, aggregate) | 1,048,576 | 2,097,152 |
| getDownload | maxBytes | 1,048,576 | 2,097,152 |
| listDownloads | limit | 20 | 100 |
| getSessionLogs | limit / maxBytes (aggregate) | 100 / 262,144 | 500 / 1,048,576 |
| getSessionReplay | limit | 20 | 100 |
getDownload reads download metadata first and rejects an oversized file before fetching its bytes. getSessionLogs serializes each log's request/response to JSON, bounds them independently, and stops adding records once the aggregate returned bytes reach maxBytes. getSessionReplay returns page metadata and signed playlist URLs only — never HLS manifests or media segments. When a payload is too large, narrow the request, lower the limit, or retrieve the artifact from the Browserbase-managed download/replay URL directly.
No webhooks
This wave ships no webhook adapter. Browserbase session state is observed by polling getSession and released with an explicit closeSession; there is no general session-completion webhook subpath.
Install
pnpm add @render-lab/tasks-browserbase @renderinc/sdk@renderinc/sdk is a peer dependency, pinned to an exact version. @browserbasehq/sdk and playwright-core are regular dependencies encapsulated inside the tasks; playwright-core does not download a browser — it only connects to Browserbase over CDP.
Environment contract
| Variable | Required | Purpose |
| --- | --- | --- |
| BROWSERBASE_API_KEY | for all browserbase.* tasks | Browserbase API key. Read lazily at the first API call, never at import; the SDK client is constructed once, on first use, with maxRetries: 0. |
| BROWSERBASE_PROJECT_ID | optional | Default project for createContext / createSession when the operation does not name one and the key cannot infer it. Read lazily. |
Credentials are read lazily at first use. Constructing the port (or importing the pack) never reads them, so importing this pack for one task never requires another's secret.
Testing
pnpm -C packages/tasks-browserbase build
pnpm -C packages/tasks-browserbase test # hermetic Tier 1 (no secrets, no network)
pnpm -C packages/tasks-browserbase typecheck
RUN_LIVE=1 BROWSERBASE_API_KEY=bb_... pnpm -C packages/tasks-browserbase test:live # opt-in Tier 2The raw *Impl functions take an injected BrowserbaseDeps, so they unit-test against a fake BrowserbasePort with no network. The port itself is tested against structural SDK/CDP fakes in test/client.test.ts.
