{
  "name": "browser_control",
  "nodes": [
    {
      "parameters": {
        "workflowInputs": {
          "values": [
            {
              "name": "action"
            },
            {
              "name": "url"
            },
            {
              "name": "selector"
            },
            {
              "name": "direction"
            },
            {
              "name": "amount"
            },
            {
              "name": "toSelector"
            },
            {
              "name": "fullPage"
            },
            {
              "name": "waitAfter"
            }
          ]
        }
      },
      "id": "d6929b19-45aa-4fad-bc29-0a5c34480b18",
      "name": "When Executed by Another Workflow",
      "type": "n8n-nodes-base.executeWorkflowTrigger",
      "typeVersion": 1.1,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "const input = $input.first().json;\nconst action = input.action;\nconst url = input.url || '';\n\n// For goto_and_html, use /unblock endpoint\nif (action === 'goto_and_html') {\n  return [{\n    json: {\n      action,\n      url,\n      useUnblock: true,\n      unblockBody: {\n        url: url,\n        content: true,\n        cookies: false,\n        screenshot: false,\n        browserWSEndpoint: false\n      }\n    }\n  }];\n}\n\n// For other actions, use /function endpoint with code\nconst selector = input.selector || '';\nconst direction = input.direction || 'down';\nconst amount = input.amount || 500;\nconst toSelector = input.toSelector || '';\nconst fullPage = input.fullPage === true;\n\nconst helpers = `\n  const delay = ms => new Promise(r => setTimeout(r, ms + Math.random() * 500));\n  const waitForPageReady = async (page, maxWait = 15000) => {\n    const startTime = Date.now();\n    let lastLength = 0;\n    let stableCount = 0;\n    await delay(2000);\n    while (Date.now() - startTime < maxWait) {\n      await delay(500);\n      const currentLength = await page.evaluate(() => document.body?.innerHTML?.length || 0);\n      if (currentLength === lastLength && currentLength > 5000) {\n        stableCount++;\n        if (stableCount >= 3) break;\n      } else {\n        stableCount = 0;\n        lastLength = currentLength;\n      }\n    }\n  };\n`;\n\nlet code = '';\n\nif (action === 'goto_and_content') {\n  code = `export default async function ({ page }) {\n  ${helpers}\n  await page.setViewport({ width: 1920, height: 1080 });\n  await page.goto('${url}', { waitUntil: 'domcontentloaded', timeout: 30000 });\n  await waitForPageReady(page, 8000);\n  const currentUrl = page.url();\n  const title = await page.title();\n  const content = await page.evaluate(() => { document.querySelectorAll('script, style, noscript, iframe, [hidden]').forEach(el => el.remove()); return document.body.innerText; });\n  return { data: { success: true, currentUrl, title, content: content.substring(0, 50000), contentLength: content.length }, type: 'application/json' };\n}`;\n} else if (action === 'screenshot') {\n  code = `export default async function ({ page }) {\n  ${helpers}\n  await page.setViewport({ width: 1920, height: 1080 });\n  await page.goto('${url}', { waitUntil: 'domcontentloaded', timeout: 30000 });\n  await waitForPageReady(page, 8000);\n  const screenshot = await page.screenshot({ fullPage: ${fullPage}, encoding: 'base64' });\n  return { data: { success: true, screenshot: 'data:image/png;base64,' + screenshot }, type: 'application/json' };\n}`;\n} else {\n  code = `export default async function ({ page }) {\n  ${helpers}\n  await page.setViewport({ width: 1920, height: 1080 });\n  await page.goto('${url || 'about:blank'}', { waitUntil: 'domcontentloaded', timeout: 30000 });\n  await waitForPageReady(page, 8000);\n  const content = await page.evaluate(() => document.body.innerText);\n  return { data: { success: true, content: content.substring(0, 50000) }, type: 'application/json' };\n}`;\n}\n\nreturn [{ json: { code, action, url, useUnblock: false } }];"
      },
      "id": "build-request",
      "name": "Build Request",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        256,
        0
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $json.useUnblock ? 'https://production-sfo.browserless.io/unblock?token=YOUR_BROWSERLESS_TOKEN' : 'https://production-sfo.browserless.io/function?token=YOUR_BROWSERLESS_TOKEN' }}",
        "sendBody": true,
        "contentType": "={{ $json.useUnblock ? 'json' : 'raw' }}",
        "specifyBody": "={{ $json.useUnblock ? 'json' : 'string' }}",
        "bodyParameters": {
          "parameters": [
            {}
          ]
        },
        "jsonBody": "={{ $json.useUnblock ? JSON.stringify($json.unblockBody) : '{}' }}",
        "body": "={{ $json.useUnblock ? '' : $json.code }}",
        "rawContentType": "application/javascript",
        "options": {
          "timeout": 120000
        }
      },
      "id": "http-request",
      "name": "Browserless Function",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        512,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "const response = $input.first().json;\nconst buildData = $('Build Request').first().json;\n\n// Handle /unblock endpoint response\nif (buildData.useUnblock) {\n  // /unblock returns: { content: 'html...', cookies: [], screenshot: null, ttl: ... }\n  const html = response.content || '';\n  const hasCaptcha = html.includes('captcha-delivery.com') || html.includes('DataDome');\n  \n  return [{\n    json: {\n      data: {\n        success: !hasCaptcha,\n        blocked: hasCaptcha,\n        currentUrl: buildData.url,\n        title: '',\n        html: html,\n        htmlLength: html.length\n      },\n      type: 'application/json'\n    }\n  }];\n}\n\n// Handle /function endpoint response\nif (response && typeof response === 'object') {\n  return [{ json: response }];\n}\nif (typeof response === 'string') {\n  try {\n    return [{ json: JSON.parse(response) }];\n  } catch (e) {\n    return [{ json: { success: false, error: 'Failed to parse response', raw: response } }];\n  }\n}\nreturn [{ json: { success: false, error: 'Unexpected response format' } }];"
      },
      "id": "parse-response",
      "name": "Parse Response",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        752,
        0
      ]
    }
  ],
  "connections": {
    "When Executed by Another Workflow": {
      "main": [
        [
          {
            "node": "Build Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Request": {
      "main": [
        [
          {
            "node": "Browserless Function",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Browserless Function": {
      "main": [
        [
          {
            "node": "Parse Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "executionTimeout": 120,
    "callerPolicy": "workflowsFromSameOwner",
    "availableInMCP": false
  },
  "versionId": "af00b1cf-20ad-4453-943f-40180f635c24",
  "id": "55qv1MLCvPQ4Cph3",
  "tags": []
}