11 min read

How To Streamline Web Tasks by Integrating Browserless, Playwright and ChangeDetection.io with n8n

I recently self-hosted ChangeDetection.io using Browserless and Playwright in Docker. My initial goal was straightforward: set up basic website monitoring with ChangeDetection.io's GUI. I figured I might find it useful to have a way to monitor websites for changes and get alerts on restocks and price drops.

After setting it up, I realized that I had an environment capable of executing Browserless API calls and running Playwright scripts. This opened the door to advanced web automation tasks like generating PDFs, capturing screenshots, and more—all integrated seamlessly with n8n.

Why Integrate ChangeDetection.io, Browserless, Playwright and n8n?

  • ChangeDetection.io provides a simple frontend for monitoring website changes.
  • Browserless offers a hosted browser solution, enabling headless Chrome automation.
  • Playwright is a powerful library for automating browser interactions.
  • n8n is a workflow automation tool that connects various services and APIs.

By integrating these tools, it's trivial to have a self-hosted system for web monitoring and automation.

Setting Up ChangeDetection.io with Browserless and Playwright

I stumbled upon a Reddit post that linked to this Gist containing a useful docker-compose.yml file. This configuration integrates Browserless with Playwright, making it easy to get started.

Securing Your Setup with a Custom Token

Security is important when exposing services like Browserless and Playwright. I modified the Docker Compose file to include a custom token for authentication. Here's how you can do it:

  1. Generate a Token: Create a secure token that you'll use for authentication.
  2. Update docker-compose.yml: Replace YOUR_PLAYWRIGHT_TOKEN with your generated token in the environment variables.

Docker Compose Configuration

Below is the modified docker-compose.yml file with a with a Custom Token:

changedetection:
    image: ghcr.io/dgtlmoon/changedetection.io:latest
    container_name: changedetection
    hostname: changedetection
    volumes:
      - /home/path/files:/datastore  # Update path to your data volume
    environment:
      - PORT=20400  # Port for changedetection
      - PUID=1000  # Preferred user ID (avoid root/0 unless necessary in a managed environment)
      - PGID=1000  # Preferred group ID (avoid root/0 unless necessary in a managed environment)
      - PLAYWRIGHT_DRIVER_URL=ws://playwright-chrome:3000/chrome?token=YOUR_PLAYWRIGHT_TOKEN&launch={"headless":false}  # WebSocket connection for Playwright
    ports:
      - 20400:20400  # Port mapping for changedetection
    restart: unless-stopped
    depends_on:
      - playwright-chrome

  playwright-chrome:
    hostname: playwright-chrome
    image: ghcr.io/browserless/chrome
    restart: unless-stopped
    environment:
      - TOKEN=YOUR_PLAYWRIGHT_TOKEN # Update using a secure token
      - SCREEN_WIDTH=1920
      - SCREEN_HEIGHT=1024
      - SCREEN_DEPTH=16
      - ENABLE_DEBUGGER=true
      - TIMEOUT=600000
      - CONCURRENT=15
    ports:
      - 20450:3000  # Port mapping for Playwright WebSocket connection

Exploring Browserless API Capabilities

With Browserless and Playwright set up, I explored the Browserless API documentation to see what was possible. Here are some of the key endpoints and their capabilities:

  • Load and Render HTML: Use /content to load a URL or HTML content and retrieve the fully rendered HTML after JavaScript execution.
  • Download Files: Use /download to execute browser interactions and download files.
  • Execute Code in Browser: Use /function to run custom Playwright code in a browser context, perfect for web scraping tasks.
  • Generate PDFs: Use /pdf to create PDF documents from URLs or HTML content with customizable options.
  • Capture Screenshots: Use /screenshot to take screenshots of webpages, supporting full-page captures and customizations.
  • Scrape Data: Use /scrape to extract specific data using CSS or XPath selectors.

Integrating with n8n for Workflow Automation

In a previous post, I detailed how to self-host n8n on Google Cloud. Building on that, I connected n8n with a self-hosted Browserless and Playwright setup to automate web tasks.

Setting Up Credentials in n8n

To authenticate with your Browserless instance in n8n:

  1. Create New Credentials:
    • Go to Credentials in n8n.
    • Choose Header Auth.
    • Set the name to Authorization and the value to YOUR_PLAYWRIGHT_TOKEN.
  2. Use Credentials in Workflows:
    • In your HTTP Request nodes, select the Browserless credentials you just created.

Example n8n Workflows

I've created several workflows utilizing different Browserless API endpoints:

  • Screenshotting: Capture a screenshot of a URL.
  • PDF Generation: Convert a web page into a PDF.
  • Content Retrieval: Extract the HTML content from a specific URL.
  • Downloading Files: Download files (e.g., ZIPs) from provided links.
  • Scraping: Extract elements using CSS or XPath selectors.

You can copy and paste the following workflow template to import all the examples into your n8n instance (also shared on the n8n forum).

{
	"name": "Browserless Examples [Shared]",
	"nodes": [
		{
			"parameters": {
				"content": "## /Screenshot URL\nhttps://docs.browserless.io/HTTP-APIs/screenshot\n\nHardcoded URL Example.com\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nPassing URL Example.com as a variable",
				"height": 552.0382521592817,
				"width": 414.07950814059393
			},
			"id": "3b89c6bb-b9cd-4bcd-8958-4dfbf87e39aa",
			"name": "Sticky Note1",
			"type": "n8n-nodes-base.stickyNote",
			"typeVersion": 1,
			"position": [
				-53.09026656387687,
				-60
			]
		},
		{
			"parameters": {
				"content": "## /content return HTML\nhttps://docs.browserless.io/HTTP-APIs/content\n\nHardcoded URL Example.com\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nPassing URL Example.com as a variable",
				"height": 549.8248303020775,
				"width": 415.7157914610786
			},
			"id": "e069f9c9-a524-4da0-b0e7-9ddb8f7f2317",
			"name": "Sticky Note",
			"type": "n8n-nodes-base.stickyNote",
			"typeVersion": 1,
			"position": [
				400,
				-60
			]
		},
		{
			"parameters": {
				"content": "## /download example\n\nHardcoded URL https://getsamplefiles.com/sample-archive-files/zip and downloads the first ZIP file\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nPassing URL, file type and first/last/all as a variable",
				"height": 543.2718166583338,
				"width": 436.9874746273785
			},
			"id": "5b5bef89-7cb2-4932-86cf-1cb695e1e46c",
			"name": "Sticky Note2",
			"type": "n8n-nodes-base.stickyNote",
			"typeVersion": 1,
			"position": [
				860,
				-60
			]
		},
		{
			"parameters": {
				"content": "## /function examples",
				"height": 543.1338634894774,
				"width": 404.2618082176863
			},
			"id": "f2a40685-047a-4a71-b314-7409ac83cfa3",
			"name": "Sticky Note3",
			"type": "n8n-nodes-base.stickyNote",
			"typeVersion": 1,
			"position": [
				-56.54071590859172,
				580
			]
		},
		{
			"parameters": {
				"content": "## /scrape example\nhttps://docs.browserless.io/HTTP-APIs/scrape\n\nHardcoded URL Example.com\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nPassing URL Example.com as a variable",
				"height": 549.7523374008159,
				"width": 454.9865911527092
			},
			"id": "1bbb57a1-87cc-48bb-8aa4-e7705d6d434f",
			"name": "Sticky Note5",
			"type": "n8n-nodes-base.stickyNote",
			"typeVersion": 1,
			"position": [
				860,
				580
			]
		},
		{
			"parameters": {
				"method": "POST",
				"url": "http://yourdomain:port/chrome/function",
				"authentication": "genericCredentialType",
				"genericAuthType": "httpHeaderAuth",
				"sendHeaders": true,
				"headerParameters": {
					"parameters": [
						{
							"name": "Content-Type",
							"value": "application/json"
						}
					]
				},
				"sendBody": true,
				"contentType": "raw",
				"rawContentType": "application/json",
				"body": "{\n  \"code\": \"export default async function ({ page }) {\\n    try {\\n      await page.goto('https://www.example.com', { \\n        waitUntil: 'networkidle2',\\n        timeout: 60000 // 60 seconds timeout\\n      });\\n      const screenshot = await page.screenshot({ fullPage: true });\\n      return {\\n        data: screenshot.toString('base64'),\\n        type: 'image/png'\\n      };\\n    } catch (error) {\\n      console.error('Error:', error);\\n      return {\\n        data: `Error: ${error.message}`,\\n        type: 'text/plain'\\n      };\\n    }\\n  }\\n\"\n}",
				"options": {
				}
			},
			"id": "8a91be2b-03f6-4060-93c7-bcd4e3c0e06f",
			"name": "HTTP Request5",
			"type": "n8n-nodes-base.httpRequest",
			"typeVersion": 4.2,
			"position": [
				120,
				860
			],
			"credentials": {
				"httpHeaderAuth": {
					"id": "NXo0Bj93cIusdtnD",
					"name": "Browserless: Header Auth Account"
				}
			}
		},
		{
			"parameters": {
				"jsCode": "// Set the URL in a variable\nconst url = 'https://www.example.com';\n\n// Loop over input items and add the URL to the JSON of each one\nfor (const item of $input.all()) {\n  item.json.url = url;\n}\n\n// Return the updated items\nreturn $input.all();"
			},
			"id": "1233d7c8-6218-4e3c-92ad-b3ed33fde0ba",
			"name": "Code",
			"type": "n8n-nodes-base.code",
			"typeVersion": 2,
			"position": [
				0,
				320
			]
		},
		{
			"parameters": {
				"jsCode": "// Set the URL in a variable\nconst url = 'https://www.example.com';\n\n// Loop over input items and add the URL to the JSON of each one\nfor (const item of $input.all()) {\n  item.json.url = url;\n}\n\n// Return the updated items\nreturn $input.all();"
			},
			"id": "be478e2d-0964-416d-9641-9deda9d92dfb",
			"name": "Code1",
			"type": "n8n-nodes-base.code",
			"typeVersion": 2,
			"position": [
				440,
				320
			]
		},
		{
			"parameters": {
				"jsCode": "// Set the URL in a variable\nconst url = 'https://www.example.com/';\n\n// Loop over input items and add the URL to the JSON of each one\nfor (const item of $input.all()) {\n  item.json.url = url;\n}\n\n// Return the updated items\nreturn $input.all();"
			},
			"id": "cc78803b-e207-4fd1-b650-1ff8f92054f1",
			"name": "Set URL",
			"type": "n8n-nodes-base.code",
			"typeVersion": 2,
			"position": [
				920,
				960
			]
		},
		{
			"parameters": {
				"jsCode": "// Set the URL in a variable\nconst url = 'https://www.example.com';\n\n// Loop over input items and add the URL to the JSON of each one\nfor (const item of $input.all()) {\n  item.json.url = url;\n}\n\n// Return the updated items\nreturn $input.all();"
			},
			"id": "31a1b79b-b10f-48c4-8f43-c30658976ae8",
			"name": "Set URL1",
			"type": "n8n-nodes-base.code",
			"typeVersion": 2,
			"position": [
				420,
				960
			]
		},
		{
			"parameters": {
				"content": "## /pdf example\nhttps://docs.browserless.io/HTTP-APIs/pdf\n\nHardcoded URL Example.com\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nPassing URL Example.com as a variable",
				"height": 549.7523374008159,
				"width": 454.9865911527092
			},
			"id": "5bd19d9b-258b-4a9d-9664-f648d9cbb8af",
			"name": "Sticky Note6",
			"type": "n8n-nodes-base.stickyNote",
			"typeVersion": 1,
			"position": [
				380,
				580
			]
		},
		{
			"parameters": {
				"method": "POST",
				"url": "http://yourdomain:port/chrome/screenshot",
				"authentication": "genericCredentialType",
				"genericAuthType": "httpHeaderAuth",
				"sendHeaders": true,
				"headerParameters": {
					"parameters": [
						{
							"name": "Content-Type",
							"value": "application/json"
						}
					]
				},
				"sendBody": true,
				"contentType": "raw",
				"rawContentType": "application/json",
				"body": "{   \"url\": \"https://www.example.com\",   \"options\": {     \"fullPage\": true   } }",
				"options": {
				}
			},
			"id": "48668f58-fed4-4a74-8b50-57d8dec68eef",
			"name": "HTTP Request: Screenshot",
			"type": "n8n-nodes-base.httpRequest",
			"typeVersion": 4.2,
			"position": [
				0,
				80
			],
			"credentials": {
				"httpHeaderAuth": {
					"id": "NXo0Bj93cIusdtnD",
					"name": "Browserless: Header Auth Account"
				}
			}
		},
		{
			"parameters": {
				"method": "POST",
				"url": "http://yourdomain:port/chrome/content",
				"authentication": "genericCredentialType",
				"genericAuthType": "httpHeaderAuth",
				"sendHeaders": true,
				"headerParameters": {
					"parameters": [
						{
							"name": "Content-Type",
							"value": "application/json"
						}
					]
				},
				"sendBody": true,
				"contentType": "raw",
				"rawContentType": "application/json",
				"body": "{\n  \"url\": \"https://www.example.com\"\n}",
				"options": {
				}
			},
			"id": "c473afb5-3b38-4d8a-9bee-4fbb95584a26",
			"name": "HTTP Request: Content",
			"type": "n8n-nodes-base.httpRequest",
			"typeVersion": 4.2,
			"position": [
				440,
				80
			],
			"credentials": {
				"httpHeaderAuth": {
					"id": "NXo0Bj93cIusdtnD",
					"name": "Browserless: Header Auth Account"
				}
			}
		},
		{
			"parameters": {
				"method": "POST",
				"url": "http://yourdomain:port/chrome/download",
				"authentication": "genericCredentialType",
				"genericAuthType": "httpHeaderAuth",
				"sendHeaders": true,
				"headerParameters": {
					"parameters": [
						{
							"name": "Content-Type",
							"value": "application/json"
						}
					]
				},
				"sendBody": true,
				"contentType": "raw",
				"rawContentType": "application/json",
				"body": "{\n  \"code\": \"export default async function ({ page }) {\\n  try {\\n    await page.goto('https://getsamplefiles.com/sample-archive-files/zip', { waitUntil: 'networkidle2', timeout: 120000 });\\n    console.log('Page loaded successfully');\\n    const zipLink = await page.$eval('a[href$=\\\\\\\".zip\\\\\\\"]', link => link.href);\\n    console.log(`Processing first ZIP link: ${zipLink}`);\\n    const response = await page.goto(zipLink, { waitUntil: 'networkidle2', timeout: 120000 });\\n    if (response.ok()) {\\n      const buffer = await response.buffer();\\n      return [{\\n        filename: zipLink.split('/').pop(),\\n        data: buffer.toString('base64'),\\n        type: 'application/zip'\\n      }];\\n    } else {\\n      console.error(`Failed to download: ${response.status()} ${response.statusText()}`);\\n      return {\\\"error\\\": `Failed to download: ${response.status()} ${response.statusText()}` };\\n    }\\n  } catch (error) {\\n    console.error('No matching ZIP file found or an error occurred:', error);\\n    return {\\\"error\\\": 'No matching ZIP file found or an error occurred' };\\n  }\\n}\\n\",\n  \"context\": {}\n}",
				"options": {
				}
			},
			"id": "15cf6c0b-458c-4a7a-8fc3-6f7cb666b307",
			"name": "HTTP Request: Download",
			"type": "n8n-nodes-base.httpRequest",
			"typeVersion": 4.2,
			"position": [
				920,
				80
			],
			"credentials": {
				"httpHeaderAuth": {
					"id": "NXo0Bj93cIusdtnD",
					"name": "Browserless: Header Auth Account"
				}
			}
		},
		{
			"parameters": {
				"method": "POST",
				"url": "http://yourdomain:port/chrome/function",
				"authentication": "genericCredentialType",
				"genericAuthType": "httpHeaderAuth",
				"sendHeaders": true,
				"headerParameters": {
					"parameters": [
						{
							"name": "Content-Type",
							"value": "application/json"
						}
					]
				},
				"sendBody": true,
				"contentType": "raw",
				"rawContentType": "application/json",
				"body": "{\n  \"code\": \"export default async function ({ page }) {\\n  try {\\n    const response = await page.goto('https://www.dundeecity.gov.uk/sites/default/files/publications/civic_renewal_forms.zip', { \\n      waitUntil: 'networkidle2',\\n      timeout: 60000 // 60 seconds timeout\\n    });\\n\\n    if (response.ok()) {\\n      const buffer = await response.buffer();\\n      return {\\n        data: buffer.toString('base64'),\\n        type: 'application/zip'\\n      };\\n    } else {\\n      throw new Error(`Failed to download: ${response.status()} ${response.statusText()}`);\\n    }\\n  } catch (error) {\\n    console.error('Error:', error);\\n    return {\\n      data: `Error: ${error.message}`,\\n      type: 'text/plain'\\n    };\\n  }\\n}\\n\"\n}",
				"options": {
				}
			},
			"id": "92c67358-e23b-4954-bd21-01430b255183",
			"name": "HTTP Request: Function",
			"type": "n8n-nodes-base.httpRequest",
			"typeVersion": 4.2,
			"position": [
				120,
				680
			],
			"credentials": {
				"httpHeaderAuth": {
					"id": "NXo0Bj93cIusdtnD",
					"name": "Browserless: Header Auth Account"
				}
			}
		},
		{
			"parameters": {
				"method": "POST",
				"url": "http://yourdomain:port/chrome/pdf",
				"authentication": "genericCredentialType",
				"genericAuthType": "httpHeaderAuth",
				"sendHeaders": true,
				"headerParameters": {
					"parameters": [
						{
							"name": "Content-Type",
							"value": "application/json"
						}
					]
				},
				"sendBody": true,
				"contentType": "raw",
				"rawContentType": "application/json",
				"body": "{\n  \"url\": \"https://www.example.com\",\n  \"options\": {\n    \"displayHeaderFooter\": true,\n    \"printBackground\": false,\n    \"format\": \"A4\"\n  }\n}",
				"options": {
				}
			},
			"id": "2996a1d2-70c1-469f-8610-c122f0cfc95c",
			"name": "HTTP Request: PDF",
			"type": "n8n-nodes-base.httpRequest",
			"typeVersion": 4.2,
			"position": [
				420,
				720
			],
			"credentials": {
				"httpHeaderAuth": {
					"id": "NXo0Bj93cIusdtnD",
					"name": "Browserless: Header Auth Account"
				}
			}
		},
		{
			"parameters": {
				"method": "POST",
				"url": "http://yourdomain:port/chrome/scrape",
				"authentication": "genericCredentialType",
				"genericAuthType": "httpHeaderAuth",
				"sendHeaders": true,
				"headerParameters": {
					"parameters": [
						{
							"name": "Content-Type",
							"value": "application/json"
						}
					]
				},
				"sendBody": true,
				"contentType": "raw",
				"rawContentType": "application/json",
				"body": "{\n  \"url\": \"https://www.example.com/\",\n  \"elements\": [\n    { \"selector\": \"h1\" }\n  ],\n  \"gotoOptions\": {\n    \"timeout\": 10000,\n    \"waitUntil\": \"networkidle2\"\n  }\n}",
				"options": {
				}
			},
			"id": "e6b2daae-b3ba-4cc4-b1f5-b58577c3b4f3",
			"name": "HTTP Request: Scrape",
			"type": "n8n-nodes-base.httpRequest",
			"typeVersion": 4.2,
			"position": [
				920,
				720
			],
			"credentials": {
				"httpHeaderAuth": {
					"id": "NXo0Bj93cIusdtnD",
					"name": "Browserless: Header Auth Account"
				}
			}
		},
		{
			"parameters": {
				"method": "POST",
				"url": "http://yourdomain:port/chrome/screenshot",
				"authentication": "genericCredentialType",
				"genericAuthType": "httpHeaderAuth",
				"sendHeaders": true,
				"headerParameters": {
					"parameters": [
						{
							"name": "Content-Type",
							"value": "application/json"
						}
					]
				},
				"sendBody": true,
				"contentType": "raw",
				"rawContentType": "application/json",
				"body": "={\n  \"url\": \"{{$json[\"url\"]}}\",\n  \"options\": { \"fullPage\": true }\n}",
				"options": {
				}
			},
			"id": "9513a03e-e37c-4d4d-80cb-076e212fcdb9",
			"name": "HTTP Request: Screenshot1",
			"type": "n8n-nodes-base.httpRequest",
			"typeVersion": 4.2,
			"position": [
				160,
				320
			],
			"credentials": {
				"httpHeaderAuth": {
					"id": "NXo0Bj93cIusdtnD",
					"name": "Browserless: Header Auth Account"
				}
			}
		},
		{
			"parameters": {
				"method": "POST",
				"url": "http://yourdomain:port/chrome/pdf",
				"authentication": "genericCredentialType",
				"genericAuthType": "httpHeaderAuth",
				"sendHeaders": true,
				"headerParameters": {
					"parameters": [
						{
							"name": "Content-Type",
							"value": "application/json"
						}
					]
				},
				"sendBody": true,
				"contentType": "raw",
				"rawContentType": "application/json",
				"body": "={\n  \"url\": \"{{$json[\"url\"]}}\",\n  \"options\": {\n    \"displayHeaderFooter\": true,\n    \"printBackground\": false,\n    \"format\": \"A4\"\n  }\n}",
				"options": {
				}
			},
			"id": "4bed427f-0c4e-4c5e-8fb5-611f483e86ed",
			"name": "HTTP Request: PDF1",
			"type": "n8n-nodes-base.httpRequest",
			"typeVersion": 4.2,
			"position": [
				580,
				960
			],
			"credentials": {
				"httpHeaderAuth": {
					"id": "NXo0Bj93cIusdtnD",
					"name": "Browserless: Header Auth Account"
				}
			}
		},
		{
			"parameters": {
				"method": "POST",
				"url": "http://yourdomain:port/chrome/content",
				"authentication": "genericCredentialType",
				"genericAuthType": "httpHeaderAuth",
				"sendHeaders": true,
				"headerParameters": {
					"parameters": [
						{
							"name": "Content-Type",
							"value": "application/json"
						}
					]
				},
				"sendBody": true,
				"contentType": "raw",
				"rawContentType": "application/json",
				"body": "={\n  \"url\": \"{{$json[\"url\"]}}\"\n}",
				"options": {
				}
			},
			"id": "73836d1e-638c-4937-a89a-b0c923bbcd52",
			"name": "HTTP Request: Content1",
			"type": "n8n-nodes-base.httpRequest",
			"typeVersion": 4.2,
			"position": [
				600,
				320
			],
			"credentials": {
				"httpHeaderAuth": {
					"id": "NXo0Bj93cIusdtnD",
					"name": "Browserless: Header Auth Account"
				}
			}
		},
		{
			"parameters": {
				"method": "POST",
				"url": "http://yourdomain:port/chrome/scrape",
				"authentication": "genericCredentialType",
				"genericAuthType": "httpHeaderAuth",
				"sendHeaders": true,
				"headerParameters": {
					"parameters": [
						{
							"name": "Content-Type",
							"value": "application/json"
						}
					]
				},
				"sendBody": true,
				"contentType": "raw",
				"rawContentType": "application/json",
				"body": "={\n  \"url\": \"{{$json[\"url\"]}}\",\n  \"elements\": [\n    { \"selector\": \"h1\" }\n  ],\n  \"gotoOptions\": {\n    \"timeout\": 10000,\n    \"waitUntil\": \"networkidle2\"\n  }\n}",
				"options": {
				}
			},
			"id": "bd0e9889-68d5-4ae0-95b1-96fb937b592d",
			"name": "HTTP Request: Scrape1",
			"type": "n8n-nodes-base.httpRequest",
			"typeVersion": 4.2,
			"position": [
				1080,
				960
			],
			"credentials": {
				"httpHeaderAuth": {
					"id": "NXo0Bj93cIusdtnD",
					"name": "Browserless: Header Auth Account"
				}
			}
		},
		{
			"parameters": {
				"method": "POST",
				"url": "http://yourdomain:port/chrome/download",
				"authentication": "genericCredentialType",
				"genericAuthType": "httpHeaderAuth",
				"sendHeaders": true,
				"headerParameters": {
					"parameters": [
						{
							"name": "Content-Type",
							"value": "application/json"
						}
					]
				},
				"sendBody": true,
				"contentType": "raw",
				"rawContentType": "application/json",
				"body": "={\n  \"code\": \"export default async function ({ page }) {\\n  try {\\n    await page.goto('{{ $json[\"url\"] }}', { waitUntil: 'networkidle2', timeout: 120000 });\\n    console.log('Page loaded successfully');\\n    const links = await page.$eval('a[href$=\\\\\\\".{{ $json[\"fileType\"] }}\\\\\\\"]', links => links.map(link => link.href));\\n    let selectedLinks = [];\\n    if ('{{ $json[\"downloadOption\"] }}' === 'all') {\\n      selectedLinks = links;\\n    } else if ('{{ $json[\"downloadOption\"] }}' === 'first') {\\n      selectedLinks = links.length > 0 ? [links[0]] : [];\\n    } else if ('{{ $json[\"downloadOption\"] }}' === 'last') {\\n      selectedLinks = links.length > 0 ? [links[links.length - 1]] : [];\\n    }\\n    const results = [];\\n    for (const zipLink of selectedLinks) {\\n      console.log(`Processing ${zipLink}`);\\n      const response = await page.goto(zipLink, { waitUntil: 'networkidle2', timeout: 120000 });\\n      if (response.ok()) {\\n        const buffer = await response.buffer();\\n        results.push({\\n          filename: zipLink.split('/').pop(),\\n          data: buffer.toString('base64'),\\n          type: 'application/{{ $json[\"fileType\"] }}'\\n        });\\n      } else {\\n        console.error(`Failed to download: ${response.status()} ${response.statusText()}`);\\n        results.push({\\\"error\\\": `Failed to download: ${response.status()} ${response.statusText()}` });\\n      }\\n    }\\n    return results;\\n  } catch (error) {\\n    console.error('No matching {{ $json[\"fileType\"] }} file found or an error occurred:', error);\\n    return {\\\"error\\\": 'No matching {{ $json[\"fileType\"] }} file found or an error occurred' };\\n  }\\n}\\n\",\n  \"context\": {}\n}",
				"options": {
				}
			},
			"name": "HTTP Request: Download4",
			"type": "n8n-nodes-base.httpRequest",
			"typeVersion": 4.2,
			"position": [
				1120,
				320
			],
			"id": "8ba0010f-fa97-45cd-928e-01751688705d",
			"credentials": {
				"httpHeaderAuth": {
					"id": "NXo0Bj93cIusdtnD",
					"name": "Browserless: Header Auth Account"
				}
			}
		},
		{
			"parameters": {
				"jsCode": "return [\n  {\n    json: {\n      url: 'https://getsamplefiles.com/sample-archive-files/zip',\n      fileType: 'zip',\n      downloadOption: 'first' // Options: 'all', 'first', 'last'\n    }\n  }\n];"
			},
			"id": "8ecdcfac-2d65-45ec-a604-5f49ae66383f",
			"name": "Code4",
			"type": "n8n-nodes-base.code",
			"typeVersion": 2,
			"position": [
				920,
				320
			]
		},
		{
			"parameters": {
				"content": "## Note about the URL field\nReplace yourdomain:port with your actual domain.com:portt# like something.com:24000",
				"height": 80,
				"width": 1341.4849931447006
			},
			"id": "e4e9705d-4b08-447c-85fa-57d260697ad8",
			"name": "Sticky Note4",
			"type": "n8n-nodes-base.stickyNote",
			"typeVersion": 1,
			"position": [
				-60,
				-180
			]
		},
		{
			"parameters": {
			},
			"id": "2e4045a9-cb47-4a65-af34-aabaa47ba16c",
			"name": "When clicking ‘Test workflow’",
			"type": "n8n-nodes-base.manualTrigger",
			"typeVersion": 1,
			"position": [
				1440,
				940
			]
		}
	],
	"pinData": {
	},
	"connections": {
		"Code": {
			"main": [
				[
					{
						"node": "HTTP Request: Screenshot1",
						"type": "main",
						"index": 0
					}
				]
			]
		},
		"Code1": {
			"main": [
				[
					{
						"node": "HTTP Request: Content1",
						"type": "main",
						"index": 0
					}
				]
			]
		},
		"Set URL": {
			"main": [
				[
					{
						"node": "HTTP Request: Scrape1",
						"type": "main",
						"index": 0
					}
				]
			]
		},
		"Set URL1": {
			"main": [
				[
					{
						"node": "HTTP Request: PDF1",
						"type": "main",
						"index": 0
					}
				]
			]
		},
		"Code4": {
			"main": [
				[
					{
						"node": "HTTP Request: Download4",
						"type": "main",
						"index": 0
					}
				]
			]
		}
	},
	"active": false,
	"settings": {
		"executionOrder": "v1"
	},
	"versionId": "186e16fa-c115-441f-8517-1ce16f3f2695",
	"meta": {
		"templateCredsSetupCompleted": true,
		"instanceId": "84c8cadeffb0e45ffb93507bd03ee1ba65b1274dc2bab04cc058f9e6a2a130e1"
	},
	"id": "cqCiCDGyUb37qXGd",
	"tags": [

	]
}

Note: Aside from adding your header authentication credentials, the only other change you’ll need to make to the above workflow is to update the domain and port of your Changedetection.io (or Browserless/Playwright) instance in each node, as mentioned in the sticky note within the template.

Benefits of This Setup

For those relying on third-party services for web automation tasks, this setup offers a self-hosted, cost-effective solution to capture screenshots, generate PDFs, extract HTML content, automate file downloads, and scrape elements from web pages, reducing reliance on third-party services. I hope these tips are helpful for others looking to automate mundane tasks possible with Browserless, Playwright, and n8n.