This is an n8n community node. It lets you automate browser actions using Playwright in your n8n workflows.
n8n is a fair-code licensed workflow automation platform.
Installation
Operations
Custom Scripts
Compatibility
Resources
Version history
Follow the installation guide in the n8n community nodes documentation.
pnpm install n8n-nodes-playwrightNote: The package will automatically download and set up the required browser binaries during installation. This requires approximately 1GB of disk space.
If you need to manually trigger the browser setup:
pnpm rebuild n8n-nodes-playwrightThis node supports the following operations:
- Navigate: Go to a specified URL and retrieve page content
- Take Screenshot: Capture a screenshot of a webpage (full page or viewport)
- Get Text: Extract text from an element using CSS selector or XPath
- Click Element: Click on an element using CSS selector or XPath
- Fill Form: Fill a form field using CSS selector or XPath
- Run Custom Script: Execute custom JavaScript code with full Playwright API access
- Choose between Chromium, Firefox, or WebKit
- Configure headless mode
- Adjust operation speed with slow motion option
For Get Text, Click Element, and Fill Form operations, you can choose between:
- CSS Selector: Standard CSS selectors (e.g.,
#submit-button,.my-class,button[type="submit"]) - XPath: XPath expressions (e.g.,
//button[@id="submit"],//div[contains(@class, "content")])
- Full page capture
- Custom save path
- Base64 output
The Run Custom Script operation gives you complete control over Playwright to automate complex browser interactions, scrape data, generate PDFs/screenshots, and more. Scripts run in a sandboxed environment with access to the full Playwright API and n8n's Code node features.
Access Playwright-specific objects using:
$page- Current page instance$browser- Browser instance$playwright- Playwright library$helpers- n8n helper methods (includingprepareBinaryData)
Plus all special variables and methods from the Code node are available:
$json- Current item's JSON data$input- Access to input data$getNodeParameter()- Get node parameters- And more from n8n documentation
// Navigate to a URL
await $page.goto('https://example.com');
// Get page title and content
const title = await $page.title();
const content = await $page.textContent('body');
console.log('Page title:', title);
// Return results
return [{
json: {
title,
content,
url: $page.url()
}
}];await $page.goto('https://news.ycombinator.com');
// Scrape all story titles
const stories = await $page.$$eval('.titleline > a', elements =>
elements.map(el => ({
title: el.textContent,
url: el.href
}))
);
console.log(`Found ${stories.length} stories`);
return stories.map(story => ({ json: story }));await $page.goto('https://example.com/login');
// Fill form fields
await $page.fill('#username', 'myuser');
await $page.fill('#password', 'mypass');
// Click submit button
await $page.click('button[type="submit"]');
// Wait for navigation
await $page.waitForNavigation();
console.log('Logged in successfully');
return [{
json: {
success: true,
finalUrl: $page.url()
}
}];await $page.goto('https://www.google.com');
// Take screenshot
const screenshot = await $page.screenshot({
type: 'png',
fullPage: true
});
// Prepare binary data
const binaryData = await $helpers.prepareBinaryData(
Buffer.from(screenshot),
'screenshot.png',
'image/png'
);
return [{
json: {
url: $page.url(),
timestamp: new Date().toISOString()
},
binary: {
screenshot: binaryData
}
}];Script 1 - Login and Save Cookies
await $page.goto('https://www.example.com/login');
// Perform login
await $page.fill('#login-username', 'user');
await $page.fill('#login-password', 'pass');
await $page.click('#login-button');
// Wait for login to complete
await $page.waitForNavigation();
// Get browser context and save cookies
const context = $page.context();
const cookies = await context.cookies();
console.log('Login successful, cookies saved');
return [{
json: {
cookies,
loginSuccess: true
}
}];Script 2 - Restore Cookies and Access Protected Page
const { cookies } = $input.first().json;
// Create new context with saved cookies
const context = $page.context();
await context.addCookies(cookies);
// Navigate to authenticated page
await $page.goto('https://example.com/protected-page');
// Perform authenticated operations
const data = await $page.textContent('.protected-content');
console.log('Accessed protected page successfully');
return [{
json: {
data,
authenticated: true
}
}];await $page.goto('https://example.com/products');
// Wait for products to load
await $page.waitForSelector('.product-item');
// Get all product data
const products = await $page.$$eval('.product-item', items =>
items.map(item => ({
name: item.querySelector('.product-name')?.textContent,
price: item.querySelector('.product-price')?.textContent,
image: item.querySelector('img')?.src
}))
);
// Take a screenshot
const screenshot = await $page.screenshot({ type: 'png' });
const screenshotBinary = await $helpers.prepareBinaryData(
Buffer.from(screenshot),
'products.png',
'image/png'
);
console.log(`Scraped ${products.length} products`);
return [{
json: {
products,
scrapedAt: new Date().toISOString(),
totalProducts: products.length
},
binary: {
screenshot: screenshotBinary
}
}];await $page.goto('https://example.com');
// Use XPath to find elements
const button = await $page.locator('xpath=//button[contains(text(), "Submit")]');
await button.click();
// XPath for complex selections
const items = await $page.locator('xpath=//div[@class="item" and @data-active="true"]').all();
const itemTexts = await Promise.all(items.map(item => item.textContent()));
console.log(`Found ${itemTexts.length} active items`);
return [{
json: {
activeItems: itemTexts
}
}];try {
await $page.goto('https://example.com');
// Try to find element with timeout
const element = await $page.waitForSelector('.my-element', {
timeout: 5000
});
const text = await element.textContent();
return [{
json: {
success: true,
text
}
}];
} catch (error) {
console.error('Error occurred:', error.message);
return [{
json: {
success: false,
error: error.message
}
}];
}// Open a new page
const newPage = await $browser.newPage();
await newPage.goto('https://example.com/page2');
// Work with both pages
const page1Title = await $page.title();
const page2Title = await newPage.title();
console.log('Page 1:', page1Title);
console.log('Page 2:', page2Title);
// Close the new page
await newPage.close();
return [{
json: {
page1Title,
page2Title
}
}];await $page.goto('https://example.com');
// Generate PDF
const pdf = await $page.pdf({
format: 'A4',
printBackground: true,
margin: {
top: '20px',
right: '20px',
bottom: '20px',
left: '20px'
}
});
// Prepare binary data
const binaryData = await $helpers.prepareBinaryData(
Buffer.from(pdf),
'document.pdf',
'application/pdf'
);
return [{
json: {
url: $page.url(),
timestamp: new Date().toISOString()
},
binary: {
pdf: binaryData
}
}];await $page.goto('https://example.com');
// Wait for network to be idle
await $page.waitForLoadState('networkidle');
// Wait for specific element to appear
await $page.waitForSelector('.dynamic-content', { timeout: 10000 });
// Wait for element to be visible
await $page.locator('.modal').waitFor({ state: 'visible' });
// Extract data after everything has loaded
const content = await $page.textContent('.dynamic-content');
return [{
json: { content }
}];await $page.goto('https://example.com/downloads');
// Start waiting for download before clicking
const downloadPromise = $page.waitForEvent('download');
await $page.click('#download-button');
const download = await downloadPromise;
// Get download details
const fileName = download.suggestedFilename();
const downloadPath = await download.path();
console.log(`Downloaded: ${fileName}`);
return [{
json: {
fileName,
downloadPath,
success: true
}
}];- Always return an array: Your script must return an array of objects like
return [{ json: {...} }]; - Use console.log(): Debug by logging to the console - output appears in n8n UI during manual execution
- Handle errors: Use try-catch for robust scripts
- Binary data: Use
$helpers.prepareBinaryData()for images, PDFs, or other files - Async/await: All Playwright operations are async, always use
await - Access input data: Use
$jsonor$inputto access data from previous nodes - Browser cleanup: The browser is automatically closed after script execution
- Requires n8n version 1.0.0 or later
- Tested with Playwright version 1.49.0
- Supports Windows, macOS, and Linux
- Node.js 18.10 or later
- Approximately 1GB disk space for browser binaries
- Additional system dependencies may be required for browser automation
- Added Run Custom Script operation with full Playwright API access
- Added XPath selector support for Get Text, Click Element, and Fill Form operations
- Added sandboxed JavaScript execution environment
- Improved error handling and user feedback
- Fixed navigate operation to return page content properly
- Added selector-based operations (getText, clickElement, fillForm)
- Improved browser binary installation process
- Bug fixes and performance improvements
- Initial release
- Basic browser automation operations
- Support for Chromium, Firefox, and WebKit
- Screenshot and navigation capabilities
If browsers are not installed correctly:
- Clean the installation:
rm -rf ~/.cache/ms-playwright
# or for Windows:
rmdir /s /q %USERPROFILE%\AppData\Local\ms-playwright- Rebuild the package:
pnpm rebuild n8n-nodes-playwrightIf your custom script fails:
- Check that you're returning an array:
return [{ json: {...} }]; - Use
console.log()to debug and see output in n8n UI - Wrap code in try-catch blocks for better error handling
- Verify all Playwright operations use
await
If elements can't be found:
- Try using XPath instead of CSS selector (or vice versa)
- Use
await $page.waitForSelector('selector')to wait for elements - Check if content is in an iframe:
await $page.frameLocator('iframe').locator('selector') - Verify the page has fully loaded:
await $page.waitForLoadState('networkidle')
Contributions are welcome! Please feel free to submit a Pull Request.
If you encounter any issues or have questions:
- Check the Troubleshooting section
- Review the Playwright documentation
- Open an issue on GitHub
Mohamed Toema
Email: m.toema20@gmail.com
GitHub: @toema