diff --git a/README.md b/README.md index 78c733d..eb660bf 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,10 @@ # n8n-nodes-browserbase -This is an n8n community node that lets you automate browsers using [Browserbase](https://browserbase.com) powered by [Stagehand](https://stagehand.dev) in your n8n workflows. +This is an n8n community node for [Browserbase](https://browserbase.com). It gives your workflows three Browserbase capabilities in one node: + +1. `Agent`: run Stagehand-powered browser automation +2. `Search`: find relevant URLs without creating a browser session +3. `Fetch`: retrieve page content without creating a browser session [n8n](https://n8n.io/) is a [fair-code licensed](https://docs.n8n.io/sustainable-use-license/) workflow automation platform. @@ -11,145 +15,122 @@ Follow the [installation guide](https://docs.n8n.io/integrations/community-nodes ## Development / Testing with Docker ```bash -# Install dependencies npm install - -# Build the node npm run build - -# Run n8n with your node in Docker docker-compose up --build - -# Open http://localhost:5678 and search for "Browserbase" node ``` +Open `http://localhost:5678` and search for `Browserbase`. + To rebuild after changes: + ```bash npm run build && docker-compose up --build ``` -## How It Works +## Capabilities + +### Agent + +Use Browserbase browser sessions plus Stagehand to complete browser tasks from a prompt. + +Required fields: + +| Field | Description | +| --- | --- | +| `Starting URL` | The page where the agent begins | +| `Instruction` | Natural language task for the agent to complete | + +The Agent resource supports: -The Browserbase Agent node is a single, self-contained node that: -1. Creates a browser session -2. Navigates to your starting URL -3. Executes an AI agent to complete your task -4. Closes the session automatically +- CUA, DOM, and Hybrid modes +- Browserbase Model Gateway or your own model API key +- Session options like region, proxies, context reuse, and keep-alive +- Variables for DOM and Hybrid flows -Just provide a URL and an instruction - the node handles everything else. +### Search -## Configuration +Use Browserbase Search to find relevant URLs quickly without launching a browser. -### Required Fields +Required fields: | Field | Description | -|-------|-------------| -| **Starting URL** | The page where the agent begins (e.g., `https://example.com`) | -| **Instruction** | Natural language task for the agent to complete | +| --- | --- | +| `Query` | Search query to execute | -### Driver Model +Optional fields: -The driver model powers the browser session (navigation, DOM interactions). Choose from: +| Field | Description | +| --- | --- | +| `Number of Results` | How many results to return, from 1 to 25 | + +Search output includes: + +- `requestId` +- `query` +- `results` +- `resultCount` -- `google/gemini-2.5-flash` (Recommended - fast & cheap) -- `google/gemini-2.5-pro` -- `openai/gpt-4o` -- `openai/gpt-4o-mini` -- `anthropic/claude-sonnet-4-5-20250929` +### Fetch -### Agent Mode +Use Browserbase Fetch to retrieve raw page content without launching a browser. -| Mode | Description | Best For | -|------|-------------|----------| -| **CUA** | Computer Use Agent - uses vision and coordinates | Complex UIs, visual interactions | -| **DOM** | Uses DOM selectors - works with any LLM | Speed, simple pages | -| **Hybrid** | Combines both approaches | Fallback reliability | +Required fields: -### Agent Models +| Field | Description | +| --- | --- | +| `URL` | URL to fetch | -Models available depend on the selected mode: +Optional fetch settings: -**CUA Mode:** -- `google/gemini-2.5-computer-use-preview-10-2025` -- `openai/computer-use-preview` -- `anthropic/claude-sonnet-4-20250514` -- `anthropic/claude-sonnet-4-5-20250929` -- `anthropic/claude-haiku-4-5-20251001` +- Follow redirects +- Allow insecure SSL +- Use Browserbase proxies -**DOM Mode:** -- `google/gemini-2.5-flash` -- `google/gemini-2.5-pro` -- `openai/gpt-4o` -- `openai/gpt-4o-mini` -- `anthropic/claude-sonnet-4-5-20250929` +Fetch output includes: -**Hybrid Mode:** -- `google/gemini-3-flash-preview` -- `anthropic/claude-sonnet-4-20250514` -- `anthropic/claude-haiku-4-5-20251001` +- `statusCode` +- `headers` +- `content` +- `contentType` +- `encoding` ## Credentials -You need three credentials: +The node uses one Browserbase credential: | Credential | Description | -|------------|-------------| -| **Browserbase API Key** | Your Browserbase API key | -| **Browserbase Project ID** | Your Browserbase project ID | -| **Model API Key** | API key for your chosen model provider | +| --- | --- | +| `Browserbase API Key` | Required for all resources | +| `Browserbase Project ID (Deprecated)` | Optional legacy header | +| `Model API Key` | Optional. Only needed for Agent when using your own model provider key | -> **Important:** The Model API Key must match the provider of your models. If using Google models, provide a Google API key. If using OpenAI, provide an OpenAI key. +## Example Usage -### Getting Credentials +### Search -1. Sign up at [Browserbase](https://browserbase.com) -2. Navigate to your dashboard for API key and Project ID -3. Get an API key from your model provider: - - [Google AI Studio](https://aistudio.google.com/apikey) - - [OpenAI](https://platform.openai.com/api-keys) - - [Anthropic](https://console.anthropic.com/) +- Resource: `Search` +- Query: `browserbase documentation` -## Example Usage +### Fetch -**Simple extraction:** -- URL: `https://news.ycombinator.com` -- Instruction: `Find the top 3 stories and return their titles and URLs` - -**Form filling:** -- URL: `https://example.com/contact` -- Instruction: `Fill out the contact form with name "John Doe" and email "john@example.com", then submit` - -**Navigation + action:** -- URL: `https://github.com` -- Instruction: `Search for "stagehand" and click on the first repository result` - -## Output - -The node returns an `AgentResult` object: - -```json -{ - "success": true, - "message": "Task completed successfully", - "actions": [ - { "type": "act", "action": "clicked submit button" } - ], - "completed": true, - "usage": { - "input_tokens": 1250, - "output_tokens": 340, - "inference_time_ms": 2500 - }, - "sessionId": "abc-123" -} -``` +- Resource: `Fetch` +- URL: `https://browserbase.com` + +### Agent + +- Resource: `Agent` +- Starting URL: `https://github.com` +- Instruction: `Search for "stagehand" and open the first repository result` ## Compatibility -Compatible with n8n@1.60.0 or later +Compatible with n8n `1.60.0` or later. ## Resources -- [n8n community nodes documentation](https://docs.n8n.io/integrations/#community-nodes) - [Browserbase Documentation](https://docs.browserbase.com) +- [Fetch Overview](https://docs.browserbase.com/platform/fetch/overview) +- [Search Overview](https://docs.browserbase.com/platform/search/overview) - [Stagehand Documentation](https://docs.stagehand.dev) +- [n8n community nodes documentation](https://docs.n8n.io/integrations/#community-nodes) diff --git a/icons/browserbase.svg b/icons/browserbase.svg index e10699d..5a168aa 100644 --- a/icons/browserbase.svg +++ b/icons/browserbase.svg @@ -1,11 +1,6 @@ - - - - - - - - - - + + + + + diff --git a/nodes/Browserbase/Browserbase.node.json b/nodes/Browserbase/Browserbase.node.json index a1601d1..3687340 100644 --- a/nodes/Browserbase/Browserbase.node.json +++ b/nodes/Browserbase/Browserbase.node.json @@ -10,5 +10,5 @@ } ] }, - "alias": ["browser", "automation", "web", "scrape", "ai agent", "stagehand"] + "alias": ["browser", "automation", "web", "scrape", "ai agent", "stagehand", "search", "fetch"] } diff --git a/nodes/Browserbase/Browserbase.node.ts b/nodes/Browserbase/Browserbase.node.ts index a77ef07..78cd529 100644 --- a/nodes/Browserbase/Browserbase.node.ts +++ b/nodes/Browserbase/Browserbase.node.ts @@ -6,614 +6,809 @@ import { type IExecuteFunctions, type INodeCredentialTestResult, type INodeExecutionData, + type INodeProperties, type INodeType, type INodeTypeDescription, type IHttpRequestMethods, NodeOperationError, } from 'n8n-workflow'; -const BASE_URL = 'https://api.stagehand.browserbase.com'; +const STAGEHAND_BASE_URL = 'https://api.stagehand.browserbase.com'; +const API_BASE_URL = 'https://api.browserbase.com'; -export class Browserbase implements INodeType { - description: INodeTypeDescription = { - displayName: 'Browserbase Agent', - name: 'browserbase', - icon: 'file:../../icons/browserbase.svg', - group: ['transform'], - version: 2, - subtitle: '={{$parameter["operation"] + ": " + $parameter["mode"]}}', - description: - 'AI-powered browser automation. Provide a URL and instruction, get results. Supports CUA (vision), DOM (selectors), and Hybrid modes.', - defaults: { - name: 'Browserbase Agent', +type BrowserbaseHeaders = Record; + +type BrowserOptions = { + recordSession?: boolean; + solveCaptchas?: boolean; + blockAds?: boolean; + advancedStealth?: boolean; + viewportWidth?: number; + viewportHeight?: number; + logSession?: boolean; + os?: string; +}; + +type SessionOptions = { + region?: string; + timeout?: number; + proxies?: boolean; + contextId?: string; + persistContext?: boolean; + keepAlive?: boolean; + userMetadata?: string; +}; + +function normalizeUrl(url: string): string { + if (!url) { + return url; + } + + if (url.startsWith('http://') || url.startsWith('https://')) { + return url; + } + + return `https://${url}`; +} + +function getSessionId(response: Record): string | undefined { + const data = response.data as Record | undefined; + return (data?.sessionId ?? response.sessionId ?? response.id) as + | string + | undefined; +} + +function getHeaders( + credentials: ICredentialDataDecryptedObject, + options?: { + includeModelApiKey?: boolean; + }, +): BrowserbaseHeaders { + const headers: BrowserbaseHeaders = { + Accept: 'application/json', + 'Content-Type': 'application/json', + 'x-bb-api-key': credentials.browserbaseApiKey as string, + }; + + if (options?.includeModelApiKey) { + headers['x-model-api-key'] = credentials.modelApiKey as string; + } + + const projectId = (credentials.browserbaseProjectId as string)?.trim(); + if (projectId) { + headers['x-bb-project-id'] = projectId; + } + + return headers; +} + +function buildProperties(): INodeProperties[] { + return [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'Agent', + value: 'agent', + }, + { + name: 'Fetch', + value: 'fetch', + }, + { + name: 'Search', + value: 'search', + }, + ], + default: 'agent', }, - inputs: [NodeConnectionTypes.Main], - outputs: [NodeConnectionTypes.Main], - usableAsTool: true, - credentials: [ - { - name: 'browserbaseApi', - required: true, - testedBy: 'browserbaseApiTest', + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['agent'], + }, }, - ], - properties: [ - // Resource - { - displayName: 'Resource', - name: 'resource', - type: 'options', - noDataExpression: true, - options: [ - { - name: 'Agent', - value: 'agent', - }, - ], - default: 'agent', + options: [ + { + name: 'Execute', + value: 'execute', + description: 'Execute an AI agent to perform browser automation tasks', + action: 'Execute an agent', + }, + ], + default: 'execute', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['fetch'], + }, }, - // Operation - { - displayName: 'Operation', - name: 'operation', - type: 'options', - noDataExpression: true, - displayOptions: { - show: { - resource: ['agent'], - }, + options: [ + { + name: 'Fetch', + value: 'fetch', + description: 'Fetch a page without starting a browser session', + action: 'Fetch a page', + }, + ], + default: 'fetch', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['search'], }, - options: [ - { - name: 'Execute', - value: 'execute', - description: 'Execute an AI agent to perform browser automation tasks', - action: 'Execute an agent', - }, - ], - default: 'execute', }, - // Notice about modes - { - displayName: 'Mode Info', - name: 'modeNotice', - type: 'notice', - default: '', - displayOptions: { - show: { - resource: ['agent'], - operation: ['execute'], - }, + options: [ + { + name: 'Search', + value: 'search', + description: 'Search the web and return structured results', + action: 'Search the web', + }, + ], + default: 'search', + }, + { + displayName: 'Mode Info', + name: 'modeNotice', + type: 'notice', + default: '', + displayOptions: { + show: { + resource: ['agent'], + operation: ['execute'], }, - description: - 'CUA uses vision/coordinates (best for complex UIs). DOM uses selectors (faster, any LLM). Hybrid combines both.', }, - // Primary fields - { - displayName: 'Starting URL', - name: 'url', - type: 'string', - default: '', - required: true, - placeholder: 'e.g. https://example.com', - description: 'The starting page URL for the agent', - displayOptions: { - show: { - resource: ['agent'], - operation: ['execute'], - }, + description: + 'CUA uses vision/coordinates (best for complex UIs). DOM uses selectors (faster, any LLM). Hybrid combines both.', + }, + { + displayName: 'Starting URL', + name: 'url', + type: 'string', + default: '', + required: true, + placeholder: 'e.g. https://example.com', + description: 'The starting page URL for the agent', + displayOptions: { + show: { + resource: ['agent'], + operation: ['execute'], }, }, - { - displayName: 'Instruction', - name: 'instruction', - type: 'string', - typeOptions: { - rows: 4, + }, + { + displayName: 'Instruction', + name: 'instruction', + type: 'string', + typeOptions: { + rows: 4, + }, + default: '', + required: true, + placeholder: 'e.g. Find the pricing page and extract all plan names and prices', + description: 'The task for the agent to complete', + displayOptions: { + show: { + resource: ['agent'], + operation: ['execute'], }, - default: '', - required: true, - placeholder: 'e.g. Find the pricing page and extract all plan names and prices', - description: 'The task for the agent to complete', - displayOptions: { - show: { - resource: ['agent'], - operation: ['execute'], - }, + }, + }, + { + displayName: 'Model Source', + name: 'modelSource', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['agent'], + operation: ['execute'], }, }, - { - displayName: 'Model Source', - name: 'modelSource', - type: 'options', - noDataExpression: true, - displayOptions: { - show: { - resource: ['agent'], - operation: ['execute'], - }, + options: [ + { + name: 'Model Gateway (Browserbase)', + value: 'gateway', + description: 'Use Browserbase-managed model routing. Mix any providers freely.', + }, + { + name: 'User-Provided API Key', + value: 'userProvidedKey', + description: 'Use your own model API key from credentials. Same provider required for both models.', + }, + ], + default: 'gateway', + description: + 'Choose how model calls are routed. Model Gateway lets you mix providers; User-provided API key requires both models from the same provider.', + }, + { + displayName: 'Model Info', + name: 'modelNoticeGateway', + type: 'notice', + default: '', + displayOptions: { + show: { + resource: ['agent'], + operation: ['execute'], + modelSource: ['gateway'], }, - options: [ - { - name: 'Model Gateway (Browserbase)', - value: 'gateway', - description: 'Use Browserbase-managed model routing. Mix any providers freely.', - }, - { - name: 'User-Provided API Key', - value: 'userProvidedKey', - description: 'Use your own model API key from credentials. Same provider required for both models.', - }, - ], - default: 'gateway', - description: 'Choose how model calls are routed. Model Gateway lets you mix providers; User-provided API key requires both models from the same provider.', }, - { - displayName: 'Model Info', - name: 'modelNoticeGateway', - type: 'notice', - default: '', - displayOptions: { - show: { - resource: ['agent'], - operation: ['execute'], - modelSource: ['gateway'], - }, + description: + 'Using the Browserbase Model Gateway. You can freely mix models from different providers for Driver and Agent.', + }, + { + displayName: 'Model Info', + name: 'modelNoticeBYOK', + type: 'notice', + default: '', + displayOptions: { + show: { + resource: ['agent'], + operation: ['execute'], + modelSource: ['userProvidedKey'], }, - description: - 'Using the Browserbase Model Gateway. You can freely mix models from different providers for Driver and Agent.', }, - { - displayName: 'Model Info', - name: 'modelNoticeBYOK', - type: 'notice', - default: '', - displayOptions: { - show: { - resource: ['agent'], - operation: ['execute'], - modelSource: ['userProvidedKey'], - }, + description: + 'Using your own API key from credentials. Both Driver and Agent models MUST be from the same provider.', + }, + { + displayName: 'Driver Model', + name: 'driverModel', + type: 'options', + displayOptions: { + show: { + resource: ['agent'], + operation: ['execute'], }, - description: - 'Using your own API key from credentials. Both Driver and Agent models MUST be from the same provider.', }, - // Driver Model for session start - { - displayName: 'Driver Model', - name: 'driverModel', - type: 'options', - displayOptions: { - show: { - resource: ['agent'], - operation: ['execute'], - }, + options: [ + { + name: 'Claude Haiku 4.5 (Anthropic)', + value: 'anthropic/claude-haiku-4-5', + }, + { + name: 'Claude Opus 4.6 (Anthropic)', + value: 'anthropic/claude-opus-4-6', + }, + { + name: 'Claude Sonnet 4.6 (Anthropic)', + value: 'anthropic/claude-sonnet-4-6', + }, + { + name: 'Gemini 3 Flash (Google)', + value: 'google/gemini-3-flash', + }, + { + name: 'Gemini 3 Pro (Google)', + value: 'google/gemini-3-pro', + }, + { + name: 'GPT-4o (OpenAI)', + value: 'openai/gpt-4o', + }, + { + name: 'GPT-4o Mini (OpenAI)', + value: 'openai/gpt-4o-mini', + }, + ], + default: 'anthropic/claude-sonnet-4-6', + description: 'Model for browser session (DOM-based, used for navigation)', + }, + { + displayName: 'Mode', + name: 'mode', + type: 'options', + displayOptions: { + show: { + resource: ['agent'], + operation: ['execute'], }, - options: [ - { - name: 'Claude Haiku 4.5 (Anthropic)', - value: 'anthropic/claude-haiku-4-5', - }, - { - name: 'Claude Opus 4.6 (Anthropic)', - value: 'anthropic/claude-opus-4-6', - }, - { - name: 'Claude Sonnet 4.6 (Anthropic)', - value: 'anthropic/claude-sonnet-4-6', - }, - { - name: 'Gemini 3 Flash (Google)', - value: 'google/gemini-3-flash', - }, - { - name: 'Gemini 3 Pro (Google)', - value: 'google/gemini-3-pro', - }, - { - name: 'GPT-4o (OpenAI)', - value: 'openai/gpt-4o', - }, - { - name: 'GPT-4o Mini (OpenAI)', - value: 'openai/gpt-4o-mini', - }, - ], - default: 'anthropic/claude-sonnet-4-6', - description: 'Model for browser session (DOM-based, used for navigation)', }, - // Mode selection - { - displayName: 'Mode', - name: 'mode', - type: 'options', - displayOptions: { - show: { - resource: ['agent'], - operation: ['execute'], - }, + options: [ + { + name: 'CUA (Computer Use Agent)', + value: 'cua', + description: 'Uses vision and coordinates. Works with CUA-specific models.', + }, + { + name: 'DOM', + value: 'dom', + description: 'Uses DOM selectors. Works with any LLM. Faster.', + }, + { + name: 'Hybrid (Experimental)', + value: 'hybrid', + description: 'Combines vision and DOM. Requires specific models.', + }, + ], + default: 'cua', + description: 'Agent mode determines how the agent interacts with pages', + }, + { + displayName: 'Agent Model', + name: 'modelCua', + type: 'options', + displayOptions: { + show: { + resource: ['agent'], + operation: ['execute'], + mode: ['cua'], }, - options: [ - { - name: 'CUA (Computer Use Agent)', - value: 'cua', - description: - 'Uses vision and coordinates. Works with CUA-specific models.', - }, - { - name: 'DOM', - value: 'dom', - description: 'Uses DOM selectors. Works with any LLM. Faster.', - }, - { - name: 'Hybrid (Experimental)', - value: 'hybrid', - description: - 'Combines vision and DOM. Requires specific models.', - }, - ], - default: 'cua', - description: 'Agent mode determines how the agent interacts with pages', }, - // CUA Models - { - displayName: 'Agent Model', - name: 'modelCua', - type: 'options', - displayOptions: { - show: { - resource: ['agent'], - operation: ['execute'], - mode: ['cua'], - }, + options: [ + { + name: 'Claude Haiku 4.5 (Anthropic)', + value: 'anthropic/claude-haiku-4-5', + }, + { + name: 'Claude Opus 4.6 (Anthropic)', + value: 'anthropic/claude-opus-4-6', + }, + { + name: 'Claude Sonnet 4.6 (Anthropic)', + value: 'anthropic/claude-sonnet-4-6', + }, + { + name: 'Computer Use Preview (2025-03-11, OpenAI)', + value: 'openai/computer-use-preview-2025-03-11', + }, + { + name: 'Computer Use Preview (OpenAI)', + value: 'openai/computer-use-preview', + }, + { + name: 'Gemini 2.5 CUA (Google)', + value: 'google/gemini-2.5-computer-use-preview-10-2025', + }, + { + name: 'Gemini 3 Flash (Google)', + value: 'google/gemini-3-flash-preview', + }, + { + name: 'Gemini 3 Pro (Google)', + value: 'google/gemini-3-pro-preview', + }, + ], + default: 'anthropic/claude-sonnet-4-6', + description: 'CUA model for vision-based browser control', + }, + { + displayName: 'Agent Model', + name: 'modelDom', + type: 'options', + displayOptions: { + show: { + resource: ['agent'], + operation: ['execute'], + mode: ['dom'], }, - options: [ - { - name: 'Claude Haiku 4.5 (Anthropic)', - value: 'anthropic/claude-haiku-4-5', - }, - { - name: 'Claude Opus 4.6 (Anthropic)', - value: 'anthropic/claude-opus-4-6', - }, - { - name: 'Claude Sonnet 4.6 (Anthropic)', - value: 'anthropic/claude-sonnet-4-6', - }, - { - name: 'Computer Use Preview (2025-03-11, OpenAI)', - value: 'openai/computer-use-preview-2025-03-11', - }, - { - name: 'Computer Use Preview (OpenAI)', - value: 'openai/computer-use-preview', - }, - { - name: 'Gemini 2.5 CUA (Google)', - value: 'google/gemini-2.5-computer-use-preview-10-2025', - }, - { - name: 'Gemini 3 Flash (Google)', - value: 'google/gemini-3-flash-preview', - }, - { - name: 'Gemini 3 Pro (Google)', - value: 'google/gemini-3-pro-preview', - }, - ], - default: 'anthropic/claude-sonnet-4-6', - description: 'CUA model for vision-based browser control', }, - // DOM Models - { - displayName: 'Agent Model', - name: 'modelDom', - type: 'options', - displayOptions: { - show: { - resource: ['agent'], - operation: ['execute'], - mode: ['dom'], - }, + options: [ + { + name: 'Claude Sonnet 4.6 (Anthropic)', + value: 'anthropic/claude-sonnet-4-6', + }, + { + name: 'Gemini 3 Flash (Google)', + value: 'google/gemini-3-flash-preview', + }, + { + name: 'Gemini 3 Pro (Google)', + value: 'google/gemini-3-pro-preview', + }, + { + name: 'GPT-4.1 (OpenAI)', + value: 'openai/gpt-4.1', + }, + { + name: 'GPT-4o (OpenAI)', + value: 'openai/gpt-4o', + }, + { + name: 'GPT-4o Mini (OpenAI) - Budget', + value: 'openai/gpt-4o-mini', + }, + ], + default: 'anthropic/claude-sonnet-4-6', + description: 'LLM for DOM-based browser control', + }, + { + displayName: 'Agent Model', + name: 'modelHybrid', + type: 'options', + displayOptions: { + show: { + resource: ['agent'], + operation: ['execute'], + mode: ['hybrid'], }, - options: [ - { - name: 'Claude Sonnet 4.6 (Anthropic)', - value: 'anthropic/claude-sonnet-4-6', - }, - { - name: 'Gemini 3 Flash (Google)', - value: 'google/gemini-3-flash-preview', - }, - { - name: 'Gemini 3 Pro (Google)', - value: 'google/gemini-3-pro-preview', - }, - { - name: 'GPT-4.1 (OpenAI)', - value: 'openai/gpt-4.1', - }, - { - name: 'GPT-4o (OpenAI)', - value: 'openai/gpt-4o', - }, - { - name: 'GPT-4o Mini (OpenAI) - Budget', - value: 'openai/gpt-4o-mini', - }, - ], - default: 'anthropic/claude-sonnet-4-6', - description: 'LLM for DOM-based browser control', }, - // Hybrid Models - { - displayName: 'Agent Model', - name: 'modelHybrid', - type: 'options', - displayOptions: { - show: { - resource: ['agent'], - operation: ['execute'], - mode: ['hybrid'], - }, + options: [ + { + name: 'Gemini 3 Flash (Google)', + value: 'google/gemini-3-flash-preview', + }, + { + name: 'Claude Sonnet 4.6 (Anthropic)', + value: 'anthropic/claude-sonnet-4-6', + }, + { + name: 'Claude Haiku 4.5 (Anthropic)', + value: 'anthropic/claude-haiku-4-5-20251001', + }, + ], + default: 'anthropic/claude-sonnet-4-6', + description: 'Model for hybrid mode (must support coordinate actions)', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + resource: ['agent'], + operation: ['execute'], }, - options: [ - { - name: 'Gemini 3 Flash (Google)', - value: 'google/gemini-3-flash-preview', - }, - { - name: 'Claude Sonnet 4.6 (Anthropic)', - value: 'anthropic/claude-sonnet-4-6', - }, - { - name: 'Claude Haiku 4.5 (Anthropic)', - value: 'anthropic/claude-haiku-4-5-20251001', - }, - ], - default: 'anthropic/claude-sonnet-4-6', - description: 'Model for hybrid mode (must support coordinate actions)', }, - // Options collection (combines agent options) - { - displayName: 'Options', - name: 'options', - type: 'collection', - placeholder: 'Add Option', - default: {}, - displayOptions: { - show: { - resource: ['agent'], - operation: ['execute'], - }, + options: [ + { + displayName: 'Highlight Cursor', + name: 'highlightCursor', + type: 'boolean', + default: true, + description: + 'Whether to highlight the cursor during execution (CUA/Hybrid only)', }, - options: [ - { - displayName: 'Highlight Cursor', - name: 'highlightCursor', - type: 'boolean', - default: true, - description: - 'Whether to highlight the cursor during execution (CUA/Hybrid only)', - }, - { - displayName: 'Max Steps', - name: 'maxSteps', - type: 'number', - default: 20, - description: 'Maximum number of steps the agent can take', - }, - { - displayName: 'System Prompt', - name: 'systemPrompt', - type: 'string', - typeOptions: { - rows: 4, + { + displayName: 'Max Steps', + name: 'maxSteps', + type: 'number', + default: 20, + description: 'Maximum number of steps the agent can take', + }, + { + displayName: 'System Prompt', + name: 'systemPrompt', + type: 'string', + typeOptions: { + rows: 4, + }, + default: '', + placeholder: 'e.g. You are a helpful assistant that extracts data from websites', + description: 'Custom system prompt for the agent', + }, + ], + }, + { + displayName: 'Variables', + name: 'variables', + type: 'fixedCollection', + typeOptions: { multipleValues: true }, + default: {}, + placeholder: 'Add Variable', + description: + 'Pass sensitive data to the agent. The LLM sees %variableName% placeholders and descriptions, but never the actual values.', + displayOptions: { + show: { + resource: ['agent'], + operation: ['execute'], + mode: ['dom', 'hybrid'], + }, + }, + options: [ + { + name: 'variableValues', + displayName: 'Variable', + values: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + placeholder: 'e.g. username', + description: 'Variable name (used as %name% in instructions)', }, - default: '', - placeholder: 'e.g. You are a helpful assistant that extracts data from websites', - description: 'Custom system prompt for the agent', - }, - ], + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + typeOptions: { password: true }, + placeholder: 'e.g. john@example.com', + description: 'The actual value (never shown to the LLM)', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + placeholder: 'e.g. The login email address', + description: + 'Optional description visible to the LLM to understand what this variable is for', + }, + ], + }, + ], + }, + { + displayName: 'Browser Options', + name: 'browserOptions', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + resource: ['agent'], + operation: ['execute'], + }, }, - // Variables - { - displayName: 'Variables', - name: 'variables', - type: 'fixedCollection', - typeOptions: { multipleValues: true }, - default: {}, - placeholder: 'Add Variable', - description: - 'Pass sensitive data to the agent. The LLM sees %variableName% placeholders and descriptions, but never the actual values.', - displayOptions: { - show: { - resource: ['agent'], - operation: ['execute'], - mode: ['dom', 'hybrid'], - }, + options: [ + { + displayName: 'Advanced Stealth', + name: 'advancedStealth', + type: 'boolean', + default: false, + description: 'Whether to enable advanced stealth mode to avoid bot detection', + }, + { + displayName: 'Block Ads', + name: 'blockAds', + type: 'boolean', + default: true, + description: 'Whether to block ads during browsing', + }, + { + displayName: 'Log Session', + name: 'logSession', + type: 'boolean', + default: true, + description: 'Whether to enable session logging', + }, + { + displayName: 'Record Session', + name: 'recordSession', + type: 'boolean', + default: true, + description: 'Whether to record the browser session for replay', + }, + { + displayName: 'Solve Captchas', + name: 'solveCaptchas', + type: 'boolean', + default: false, + description: 'Whether to automatically solve captchas encountered during execution', + }, + { + displayName: 'Viewport Height', + name: 'viewportHeight', + type: 'number', + default: 711, + description: 'Browser viewport height in pixels (711 recommended for CUA)', + }, + { + displayName: 'Viewport Width', + name: 'viewportWidth', + type: 'number', + default: 1288, + description: 'Browser viewport width in pixels (1288 recommended for CUA)', + }, + ], + }, + { + displayName: 'Session Options', + name: 'sessionOptions', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + resource: ['agent'], + operation: ['execute'], }, - options: [ - { - name: 'variableValues', - displayName: 'Variable', - values: [ - { - displayName: 'Name', - name: 'name', - type: 'string', - default: '', - placeholder: 'e.g. username', - description: 'Variable name (used as %name% in instructions)', - }, - { - displayName: 'Value', - name: 'value', - type: 'string', - default: '', - typeOptions: { password: true }, - placeholder: 'e.g. john@example.com', - description: 'The actual value (never shown to the LLM)', - }, - { - displayName: 'Description', - name: 'description', - type: 'string', - default: '', - placeholder: 'e.g. The login email address', - description: - 'Optional description visible to the LLM to understand what this variable is for', - }, - ], - }, - ], }, - // Browser Options - { - displayName: 'Browser Options', - name: 'browserOptions', - type: 'collection', - placeholder: 'Add Option', - default: {}, - displayOptions: { - show: { - resource: ['agent'], - operation: ['execute'], - }, + options: [ + { + displayName: 'Context ID', + name: 'contextId', + type: 'string', + default: '', + placeholder: 'e.g. ctx_abc123', + description: + 'Reuse cookies, auth, and cached data across sessions. Create a context via the Browserbase Contexts API first.', }, - options: [ - { - displayName: 'Advanced Stealth', - name: 'advancedStealth', - type: 'boolean', - default: false, - description: 'Whether to enable advanced stealth mode to avoid bot detection', - }, - { - displayName: 'Block Ads', - name: 'blockAds', - type: 'boolean', - default: true, - description: 'Whether to block ads during browsing', - }, - { - displayName: 'Log Session', - name: 'logSession', - type: 'boolean', - default: true, - description: 'Whether to enable session logging', - }, - { - displayName: 'Record Session', - name: 'recordSession', - type: 'boolean', - default: true, - description: 'Whether to record the browser session for replay', - }, - { - displayName: 'Solve Captchas', - name: 'solveCaptchas', - type: 'boolean', - default: false, - description: 'Whether to automatically solve captchas encountered during execution', - }, - { - displayName: 'Viewport Height', - name: 'viewportHeight', - type: 'number', - default: 711, - description: 'Browser viewport height in pixels (711 recommended for CUA)', - }, - { - displayName: 'Viewport Width', - name: 'viewportWidth', - type: 'number', - default: 1288, - description: 'Browser viewport width in pixels (1288 recommended for CUA)', - }, - ], + { + displayName: 'Keep Alive', + name: 'keepAlive', + type: 'boolean', + default: false, + description: 'Whether to keep the session alive even after disconnections. Available on Hobby plan and above.', + }, + { + displayName: 'Persist Context', + name: 'persistContext', + type: 'boolean', + default: true, + description: + 'Whether to save session changes (cookies, auth tokens, cache) back to the context when the session ends. Only used when Context ID is set.', + }, + { + displayName: 'Region', + name: 'region', + type: 'options', + options: [ + { name: 'AP Southeast 1 (Singapore)', value: 'ap-southeast-1' }, + { name: 'EU Central 1 (Frankfurt)', value: 'eu-central-1' }, + { name: 'US East 1 (Virginia)', value: 'us-east-1' }, + { name: 'US West 2 (Oregon)', value: 'us-west-2' }, + ], + default: 'us-west-2', + description: 'Region where the browser session will run', + }, + { + displayName: 'Timeout', + name: 'timeout', + type: 'number', + default: 300, + typeOptions: { + minValue: 60, + maxValue: 21600, + }, + description: 'Session timeout in seconds (60-21600)', + }, + { + displayName: 'Use Proxies', + name: 'proxies', + type: 'boolean', + default: true, + description: 'Whether to route traffic through proxies', + }, + { + displayName: 'User Metadata', + name: 'userMetadata', + type: 'string', + typeOptions: { + rows: 3, + }, + default: '', + placeholder: '{"key": "value"}', + description: 'Arbitrary JSON metadata to attach to the session', + }, + ], + }, + { + displayName: 'Query', + name: 'query', + type: 'string', + typeOptions: { + rows: 3, }, - // Session Options - { - displayName: 'Session Options', - name: 'sessionOptions', - type: 'collection', - placeholder: 'Add Option', - default: {}, - displayOptions: { - show: { - resource: ['agent'], - operation: ['execute'], - }, + default: '', + required: true, + placeholder: 'e.g. browserbase documentation', + description: 'The search query to run', + displayOptions: { + show: { + resource: ['search'], + operation: ['search'], + }, + }, + }, + { + displayName: 'Number of Results', + name: 'numResults', + type: 'number', + typeOptions: { + minValue: 1, + maxValue: 25, + }, + default: 10, + description: 'How many search results to return (1-25)', + displayOptions: { + show: { + resource: ['search'], + operation: ['search'], + }, + }, + }, + { + displayName: 'URL', + name: 'fetchUrl', + type: 'string', + default: '', + required: true, + placeholder: 'e.g. https://example.com', + description: 'The URL to fetch', + displayOptions: { + show: { + resource: ['fetch'], + operation: ['fetch'], + }, + }, + }, + { + displayName: 'Fetch Options', + name: 'fetchOptions', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + resource: ['fetch'], + operation: ['fetch'], }, - options: [ - { - displayName: 'Context ID', - name: 'contextId', - type: 'string', - default: '', - placeholder: 'e.g. ctx_abc123', - description: 'Reuse cookies, auth, and cached data across sessions. Create a context via the Browserbase Contexts API first.', - }, - { - displayName: 'Keep Alive', - name: 'keepAlive', - type: 'boolean', - default: false, - description: 'Whether to keep the session alive even after disconnections. Available on Hobby plan and above.', - }, - { - displayName: 'Persist Context', - name: 'persistContext', - type: 'boolean', - default: true, - description: 'Whether to save session changes (cookies, auth tokens, cache) back to the context when the session ends. Only used when Context ID is set.', - }, - { - displayName: 'Region', - name: 'region', - type: 'options', - options: [ - { name: 'AP Southeast 1 (Singapore)', value: 'ap-southeast-1' }, - { name: 'EU Central 1 (Frankfurt)', value: 'eu-central-1' }, - { name: 'US East 1 (Virginia)', value: 'us-east-1' }, - { name: 'US West 2 (Oregon)', value: 'us-west-2' }, - ], - default: 'us-west-2', - description: 'Region where the browser session will run', - }, - { - displayName: 'Timeout', - name: 'timeout', - type: 'number', - default: 300, - typeOptions: { - minValue: 60, - maxValue: 21600, - }, - description: 'Session timeout in seconds (60-21600)', - }, - { - displayName: 'Use Proxies', - name: 'proxies', - type: 'boolean', - default: true, - description: 'Whether to route traffic through proxies', - }, - { - displayName: 'User Metadata', - name: 'userMetadata', - type: 'string', - typeOptions: { - rows: 3, - }, - default: '', - placeholder: '{"key": "value"}', - description: 'Arbitrary JSON metadata to attach to the session', - }, - ], + }, + options: [ + { + displayName: 'Allow Insecure SSL', + name: 'allowInsecureSsl', + type: 'boolean', + default: false, + description: 'Whether to bypass TLS certificate verification', + }, + { + displayName: 'Allow Redirects', + name: 'allowRedirects', + type: 'boolean', + default: false, + description: 'Whether to follow HTTP redirects', + }, + { + displayName: 'Use Proxies', + name: 'proxies', + type: 'boolean', + default: false, + description: 'Whether to route the request through Browserbase proxies', + }, + ], + }, + ]; +} + +export class Browserbase implements INodeType { + description: INodeTypeDescription = { + displayName: 'Browserbase', + name: 'browserbase', + icon: 'file:../../icons/browserbase.svg', + group: ['transform'], + version: 2, + subtitle: + '={{$parameter["resource"] === "agent" ? $parameter["operation"] + ": " + $parameter["mode"] : $parameter["operation"]}}', + description: + 'Browser automation, web search, and page fetches with Browserbase.', + defaults: { + name: 'Browserbase', + }, + inputs: [NodeConnectionTypes.Main], + outputs: [NodeConnectionTypes.Main], + usableAsTool: true, + credentials: [ + { + name: 'browserbaseApi', + required: true, + testedBy: 'browserbaseApiTest', }, ], + properties: buildProperties(), }; methods = { @@ -622,364 +817,439 @@ export class Browserbase implements INodeType { this: ICredentialTestFunctions, credential: ICredentialsDecrypted, ): Promise { - const { browserbaseApiKey, browserbaseProjectId, modelApiKey } = - credential.data!; - - const headers: Record = { - Accept: 'application/json', - 'Content-Type': 'application/json', - 'x-bb-api-key': browserbaseApiKey as string, - }; - if (modelApiKey) { - headers['x-model-api-key'] = modelApiKey as string; - } - const projectId = (browserbaseProjectId as string)?.trim(); - if (projectId) { - headers['x-bb-project-id'] = projectId; - } - - let sessionId: string | undefined; - - const httpRequest = this.helpers[ - 'request' as keyof typeof this.helpers - ] as (opts: object) => Promise>; - try { - const startResponse = await httpRequest({ + const headers = getHeaders(credential.data!); + const httpRequest = this.helpers[ + 'request' as keyof typeof this.helpers + ] as (opts: object) => Promise>; + + await httpRequest({ method: 'POST', - uri: `${BASE_URL}/v1/sessions/start`, + uri: `${API_BASE_URL}/v1/fetch`, headers, - body: { modelName: 'openai/gpt-4o' }, + body: { url: 'https://browserbase.com/' }, json: true, }); - const data = startResponse.data as Record | undefined; - sessionId = (data?.sessionId ?? - startResponse.sessionId ?? - startResponse.id) as string | undefined; - - if (sessionId) { - await httpRequest({ - method: 'POST', - uri: `${BASE_URL}/v1/sessions/${sessionId}/end`, - headers, - body: {}, - json: true, - }); - } - return { status: 'OK', message: 'Connection successful' }; } catch (error) { - if (sessionId) { - try { - await httpRequest({ - method: 'POST', - uri: `${BASE_URL}/v1/sessions/${sessionId}/end`, - headers, - body: {}, - json: true, - }); - } catch (cleanupError) { - // Best-effort cleanup; ignore failures so the original test error surfaces. - void cleanupError; - } - } - - const msg = - error instanceof Error ? error.message : String(error); + const msg = error instanceof Error ? error.message : String(error); return { status: 'Error', message: msg }; } }, }, }; - async execute(this: IExecuteFunctions): Promise { - const items = this.getInputData(); - const returnData: INodeExecutionData[] = []; + private async apiCall( + executeFunctions: IExecuteFunctions, + method: IHttpRequestMethods, + baseUrl: string, + endpoint: string, + headers: BrowserbaseHeaders, + body?: object, + ): Promise> { + try { + return await executeFunctions.helpers.httpRequest({ + method, + url: `${baseUrl}${endpoint}`, + headers, + body, + json: true, + }); + } catch (error: unknown) { + const err = error as { + response?: { data?: unknown }; + message?: string; + }; + const detail = err.response?.data + ? JSON.stringify(err.response.data) + : err.message ?? 'Unknown error'; + throw new NodeOperationError( + executeFunctions.getNode(), + `API call to ${endpoint} failed: ${detail}`, + ); + } + } - for (let i = 0; i < items.length; i++) { - try { - // Get parameters - let url = this.getNodeParameter('url', i) as string; - // Ensure URL has protocol - if (url && !url.startsWith('http://') && !url.startsWith('https://')) { - url = `https://${url}`; - } - const instruction = this.getNodeParameter('instruction', i) as string; - const modelSource = this.getNodeParameter('modelSource', i) as string; - const driverModel = this.getNodeParameter('driverModel', i) as string; - const mode = this.getNodeParameter('mode', i) as string; - - let agentModel: string; - if (mode === 'cua') { - agentModel = this.getNodeParameter('modelCua', i) as string; - } else if (mode === 'dom') { - agentModel = this.getNodeParameter('modelDom', i) as string; - } else { - agentModel = this.getNodeParameter('modelHybrid', i) as string; - } + private async executeSearch( + executeFunctions: IExecuteFunctions, + itemIndex: number, + headers: BrowserbaseHeaders, + ): Promise { + const query = executeFunctions.getNodeParameter('query', itemIndex) as string; + const numResults = executeFunctions.getNodeParameter( + 'numResults', + itemIndex, + ) as number; - if (modelSource === 'userProvidedKey') { - const driverProvider = driverModel.split('/')[0]; - const agentProvider = agentModel.split('/')[0]; - if (driverProvider !== agentProvider) { - throw new NodeOperationError( - this.getNode(), - `When using your own model API key, both Driver and Agent models must be from the same provider. Driver is "${driverProvider}", Agent is "${agentProvider}".`, - ); - } - } + const response = await this.apiCall( + executeFunctions, + 'POST', + API_BASE_URL, + '/v1/search', + headers, + { + query, + numResults, + }, + ); - const options = this.getNodeParameter('options', i, {}) as { - maxSteps?: number; - systemPrompt?: string; - highlightCursor?: boolean; - }; - const browserOptions = this.getNodeParameter( - 'browserOptions', - i, - {}, - ) as { - recordSession?: boolean; - solveCaptchas?: boolean; - blockAds?: boolean; - advancedStealth?: boolean; - viewportWidth?: number; - viewportHeight?: number; - logSession?: boolean; - os?: string; - }; - const sessionOptions = this.getNodeParameter( - 'sessionOptions', - i, - {}, - ) as { - region?: string; - timeout?: number; - proxies?: boolean; - contextId?: string; - persistContext?: boolean; - keepAlive?: boolean; - userMetadata?: string; - }; + const results = + (response.results as Array> | undefined) ?? []; - const credentials = await this.getCredentials('browserbaseApi'); - const headers: Record = { - Accept: 'application/json', - 'Content-Type': 'application/json', - 'x-bb-api-key': credentials.browserbaseApiKey as string, - }; - if (modelSource === 'userProvidedKey') { - const modelApiKey = credentials.modelApiKey as string; - if (!modelApiKey) { - throw new NodeOperationError( - this.getNode(), - 'Model Source is set to "User-provided API key" but no Model API Key is configured in the Browserbase credentials.', - ); - } - headers['x-model-api-key'] = modelApiKey; - } - const projectId = (credentials.browserbaseProjectId as string)?.trim(); - if (projectId) { - headers['x-bb-project-id'] = projectId; - } + return { + json: { + requestId: response.requestId as string | undefined, + query: (response.query as string | undefined) ?? query, + results, + resultCount: results.length, + }, + pairedItem: { item: itemIndex }, + }; + } - // Helper function to make API calls - const apiCall = async ( - method: IHttpRequestMethods, - endpoint: string, - body?: object, - ) => { - const fullUrl = `${BASE_URL}${endpoint}`; - - try { - const response = await this.helpers.httpRequest({ - method, - url: fullUrl, - headers, - body, - json: true, - }); - return response; - } catch (error: unknown) { - const err = error as { response?: { data?: unknown }; message?: string }; - const detail = err.response?.data - ? JSON.stringify(err.response.data) - : err.message ?? 'Unknown error'; - throw new NodeOperationError( - this.getNode(), - `API call to ${endpoint} failed: ${detail}`, - ); - } - }; + private async executeFetch( + executeFunctions: IExecuteFunctions, + itemIndex: number, + headers: BrowserbaseHeaders, + ): Promise { + const url = normalizeUrl( + executeFunctions.getNodeParameter('fetchUrl', itemIndex) as string, + ); + const fetchOptions = executeFunctions.getNodeParameter( + 'fetchOptions', + itemIndex, + {}, + ) as { + allowRedirects?: boolean; + allowInsecureSsl?: boolean; + proxies?: boolean; + }; - let sessionId: string | undefined; + const response = await this.apiCall( + executeFunctions, + 'POST', + API_BASE_URL, + '/v1/fetch', + headers, + { + url, + allowRedirects: fetchOptions.allowRedirects ?? false, + allowInsecureSsl: fetchOptions.allowInsecureSsl ?? false, + proxies: fetchOptions.proxies ?? false, + }, + ); - try { - // 1. Start session - const browserSettings: Record = { - recordSession: browserOptions.recordSession ?? true, - solveCaptchas: browserOptions.solveCaptchas ?? false, - blockAds: browserOptions.blockAds ?? true, - advancedStealth: browserOptions.advancedStealth ?? false, - logSession: browserOptions.logSession ?? true, - viewport: { - width: browserOptions.viewportWidth ?? 1288, - height: browserOptions.viewportHeight ?? 711, - }, - }; + return { + json: { + url, + statusCode: response.statusCode as number | undefined, + headers: + (response.headers as Record | undefined) ?? {}, + content: response.content as string | undefined, + contentType: response.contentType as string | undefined, + encoding: response.encoding as string | undefined, + }, + pairedItem: { item: itemIndex }, + }; + } - if (sessionOptions.contextId) { - browserSettings.context = { - id: sessionOptions.contextId, - persist: sessionOptions.persistContext ?? true, - }; - } + private async executeAgent( + executeFunctions: IExecuteFunctions, + itemIndex: number, + headers: BrowserbaseHeaders, + ): Promise { + let url = executeFunctions.getNodeParameter('url', itemIndex) as string; + url = normalizeUrl(url); - if (browserOptions.os) { - browserSettings.os = browserOptions.os; - } + const instruction = executeFunctions.getNodeParameter( + 'instruction', + itemIndex, + ) as string; + const modelSource = executeFunctions.getNodeParameter( + 'modelSource', + itemIndex, + ) as string; + const driverModel = executeFunctions.getNodeParameter( + 'driverModel', + itemIndex, + ) as string; + const mode = executeFunctions.getNodeParameter('mode', itemIndex) as string; - const sessionCreateParams: Record = { - browserSettings, - region: sessionOptions.region ?? 'us-west-2', - timeout: sessionOptions.timeout ?? 300, - ...(sessionOptions.proxies !== false ? { proxies: true } : {}), - }; + let agentModel: string; + if (mode === 'cua') { + agentModel = executeFunctions.getNodeParameter('modelCua', itemIndex) as string; + } else if (mode === 'dom') { + agentModel = executeFunctions.getNodeParameter('modelDom', itemIndex) as string; + } else { + agentModel = executeFunctions.getNodeParameter( + 'modelHybrid', + itemIndex, + ) as string; + } - if (sessionOptions.keepAlive) { - sessionCreateParams.keepAlive = true; - } + if (modelSource === 'userProvidedKey') { + const driverProvider = driverModel.split('/')[0]; + const agentProvider = agentModel.split('/')[0]; + if (driverProvider !== agentProvider) { + throw new NodeOperationError( + executeFunctions.getNode(), + `When using your own model API key, both Driver and Agent models must be from the same provider. Driver is "${driverProvider}", Agent is "${agentProvider}".`, + ); + } + } - if (sessionOptions.userMetadata) { - try { - sessionCreateParams.userMetadata = { n8n: 'true', ...JSON.parse(sessionOptions.userMetadata) }; - } catch (parseError) { - void parseError; - sessionCreateParams.userMetadata = { n8n: 'true', note: sessionOptions.userMetadata }; - } - } else { - sessionCreateParams.userMetadata = { n8n: 'true' }; - } + const options = executeFunctions.getNodeParameter( + 'options', + itemIndex, + {}, + ) as { + maxSteps?: number; + systemPrompt?: string; + highlightCursor?: boolean; + }; + const browserOptions = executeFunctions.getNodeParameter( + 'browserOptions', + itemIndex, + {}, + ) as BrowserOptions; + const sessionOptions = executeFunctions.getNodeParameter( + 'sessionOptions', + itemIndex, + {}, + ) as SessionOptions; - const startBody: Record = { - modelName: driverModel, - browserbaseSessionCreateParams: sessionCreateParams, - }; + let sessionId: string | undefined; - const startResponse = await apiCall( - 'POST', - '/v1/sessions/start', - startBody, - ); - sessionId = - startResponse.data?.sessionId ?? - startResponse.sessionId ?? - startResponse.id; - - if (!sessionId) { - throw new NodeOperationError( - this.getNode(), - 'Failed to get session ID from start response', - ); - } + try { + const browserSettings: Record = { + recordSession: browserOptions.recordSession ?? true, + solveCaptchas: browserOptions.solveCaptchas ?? false, + blockAds: browserOptions.blockAds ?? true, + advancedStealth: browserOptions.advancedStealth ?? false, + logSession: browserOptions.logSession ?? true, + viewport: { + width: browserOptions.viewportWidth ?? 1288, + height: browserOptions.viewportHeight ?? 711, + }, + }; - // 2. Navigate to URL - await apiCall('POST', `/v1/sessions/${sessionId}/navigate`, { - url, - options: { - waitUntil: 'domcontentloaded', - }, - }); + if (sessionOptions.contextId) { + browserSettings.context = { + id: sessionOptions.contextId, + persist: sessionOptions.persistContext ?? true, + }; + } - // 3. Execute agent - const agentConfigBody: Record = { - model: agentModel, - }; + if (browserOptions.os) { + browserSettings.os = browserOptions.os; + } - if (options.systemPrompt) { - agentConfigBody.systemPrompt = options.systemPrompt; - } + const sessionCreateParams: Record = { + browserSettings, + region: sessionOptions.region ?? 'us-west-2', + timeout: sessionOptions.timeout ?? 300, + ...(sessionOptions.proxies !== false ? { proxies: true } : {}), + }; + + if (sessionOptions.keepAlive) { + sessionCreateParams.keepAlive = true; + } - const executeOpts: Record = { - instruction, - maxSteps: options.maxSteps ?? 20, + if (sessionOptions.userMetadata) { + try { + sessionCreateParams.userMetadata = { + n8n: 'true', + ...JSON.parse(sessionOptions.userMetadata), + }; + } catch (parseError) { + void parseError; + sessionCreateParams.userMetadata = { + n8n: 'true', + note: sessionOptions.userMetadata, }; + } + } else { + sessionCreateParams.userMetadata = { n8n: 'true' }; + } - if ( - (mode === 'cua' || mode === 'hybrid') && - options.highlightCursor !== false - ) { - executeOpts.highlightCursor = options.highlightCursor ?? true; - } + const startResponse = await this.apiCall( + executeFunctions, + 'POST', + STAGEHAND_BASE_URL, + '/v1/sessions/start', + headers, + { + modelName: driverModel, + browserbaseSessionCreateParams: sessionCreateParams, + }, + ); + sessionId = getSessionId(startResponse); + + if (!sessionId) { + throw new NodeOperationError( + executeFunctions.getNode(), + 'Failed to get session ID from start response', + ); + } + + await this.apiCall( + executeFunctions, + 'POST', + STAGEHAND_BASE_URL, + `/v1/sessions/${sessionId}/navigate`, + headers, + { + url, + options: { + waitUntil: 'domcontentloaded', + }, + }, + ); - if (mode === 'dom' || mode === 'hybrid') { - const variablesParam = this.getNodeParameter('variables', i, {}) as { - variableValues?: Array<{ - name: string; - value: string; - description?: string; - }>; - }; - - if (variablesParam.variableValues?.length) { - const variables: Record = {}; - for (const v of variablesParam.variableValues) { - if (v.name) { - variables[v.name] = v.description - ? { value: v.value, description: v.description } - : { value: v.value }; - } - } - if (Object.keys(variables).length > 0) { - executeOpts.variables = variables; - } + const agentConfigBody: Record = { + model: agentModel, + }; + + if (options.systemPrompt) { + agentConfigBody.systemPrompt = options.systemPrompt; + } + + const executeOptions: Record = { + instruction, + maxSteps: options.maxSteps ?? 20, + }; + + if ( + (mode === 'cua' || mode === 'hybrid') && + options.highlightCursor !== false + ) { + executeOptions.highlightCursor = options.highlightCursor ?? true; + } + + if (mode === 'dom' || mode === 'hybrid') { + const variablesParam = executeFunctions.getNodeParameter( + 'variables', + itemIndex, + {}, + ) as { + variableValues?: Array<{ + name: string; + value: string; + description?: string; + }>; + }; + + if (variablesParam.variableValues?.length) { + const variables: Record = + {}; + for (const variable of variablesParam.variableValues) { + if (variable.name) { + variables[variable.name] = variable.description + ? { value: variable.value, description: variable.description } + : { value: variable.value }; } } + if (Object.keys(variables).length > 0) { + executeOptions.variables = variables; + } + } + } + + const executeResponse = await this.apiCall( + executeFunctions, + 'POST', + STAGEHAND_BASE_URL, + `/v1/sessions/${sessionId}/agentExecute`, + headers, + { + agentConfig: agentConfigBody, + executeOptions, + }, + ); - const executeResponse = await apiCall( + await this.apiCall( + executeFunctions, + 'POST', + STAGEHAND_BASE_URL, + `/v1/sessions/${sessionId}/end`, + headers, + {}, + ); + + const responseData = executeResponse.data as + | Record + | undefined; + const result = (responseData?.result as Record | undefined) ?? executeResponse; + + return { + json: { + success: result.success ?? true, + message: result.message ?? 'Task completed', + actions: result.actions ?? [], + completed: result.completed ?? true, + usage: result.usage ?? {}, + sessionId, + ...(sessionOptions.contextId ? { contextId: sessionOptions.contextId } : {}), + }, + pairedItem: { item: itemIndex }, + }; + } catch (error) { + if (sessionId) { + try { + await this.apiCall( + executeFunctions, 'POST', - `/v1/sessions/${sessionId}/agentExecute`, - { - agentConfig: agentConfigBody, - executeOptions: executeOpts, - }, + STAGEHAND_BASE_URL, + `/v1/sessions/${sessionId}/end`, + headers, + {}, ); + } catch (cleanupError) { + void cleanupError; + } + } + throw error; + } + } - // 4. End session - await apiCall('POST', `/v1/sessions/${sessionId}/end`, {}); + async execute(this: IExecuteFunctions & Browserbase): Promise { + const items = this.getInputData(); + const returnData: INodeExecutionData[] = []; - // Return agent result - const result = executeResponse.data?.result ?? executeResponse; - returnData.push({ - json: { - success: result.success ?? true, - message: result.message ?? 'Task completed', - actions: result.actions ?? [], - completed: result.completed ?? true, - usage: result.usage ?? {}, - sessionId, - ...(sessionOptions.contextId ? { contextId: sessionOptions.contextId } : {}), - }, - pairedItem: { item: i }, - }); - } catch (error) { - // Try to end session if it was created - if (sessionId) { - try { - await apiCall('POST', `/v1/sessions/${sessionId}/end`, {}); - } catch (cleanupError) { - // Best-effort session close after an error; ignore cleanup failures. - void cleanupError; - } - } - throw error; + for (let i = 0; i < items.length; i++) { + try { + const resource = this.getNodeParameter('resource', i) as string; + const modelSource = this.getNodeParameter('modelSource', i, 'gateway') as string; + const credentials = await this.getCredentials('browserbaseApi'); + + if ( + resource === 'agent' && + modelSource === 'userProvidedKey' && + !credentials.modelApiKey + ) { + throw new NodeOperationError( + this.getNode(), + 'Model Source is set to "User-provided API key" but no Model API Key is configured in the Browserbase credentials.', + ); + } + + const headers = getHeaders(credentials, { + includeModelApiKey: + resource === 'agent' && modelSource === 'userProvidedKey', + }); + + if (resource === 'search') { + returnData.push(await this.executeSearch(this, i, headers)); + } else if (resource === 'fetch') { + returnData.push(await this.executeFetch(this, i, headers)); + } else { + returnData.push(await this.executeAgent(this, i, headers)); } } catch (error) { if (this.continueOnFail()) { returnData.push({ json: { - success: false, error: error instanceof Error ? error.message : String(error), }, pairedItem: { item: i }, diff --git a/package-lock.json b/package-lock.json index 1daaa72..fd7c2d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "n8n-nodes-browserbase", - "version": "1.2.4", + "version": "1.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "n8n-nodes-browserbase", - "version": "1.2.4", + "version": "1.3.0", "license": "MIT", "devDependencies": { "@n8n/node-cli": "*", diff --git a/package.json b/package.json index d52c37d..af93681 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "n8n-nodes-browserbase", - "version": "1.2.4", - "description": "n8n community node for Browserbase", + "version": "1.3.0", + "description": "n8n community node for Browserbase agent, search, and fetch workflows", "license": "MIT", "homepage": "https://github.com/browserbase/n8n-node", "keywords": [