Welcome to the first installment of our Browser-to-Automation series!
In this beginner friendly guide, we are going to build a tool that captures text directly from your active browser tab and sends it to an n8n AI Agent for translation. By the end of this tutorial, you will understand how to connect a custom Chrome Extension to your n8n workflows securely.
The Vision & Architecture
Before we write any code, it is crucial to understand how the pieces fit together. We are using a Standard Popup Pattern.
Here is the high-level flow of data:
The Frontend (Chrome Extension)
We are building this extension using Manifest V3 (MV3), the current standard for Chrome extensions.
Why Manifest V3?
The biggest change in MV3 is the shift from persistent “Background Pages” to Service Workers. In older extensions, background scripts ran forever, eating up your computer’s memory. A Service Worker, however, is event-driven. It “wakes up” only when it receives a message (like clicking our translate button), does its job, and goes back to sleep. This is much better for performance, but it means we have to be deliberate about how our UI (popup.js) talks to our backend logic (background.js).
Let’s build out the files for our extension. Create a new folder on your computer and add the following files:
1. manifest.json
This is the “ID Card” of your app. It tells Chrome what permissions your extension needs and where your files are located.
{
"manifest_version": 3,
"name": "n8n AI Translator",
"version": "1.0",
"permissions": ["activeTab", "scripting"],
"host_permissions": ["http://localhost:<port>/*", "https://*/*"],
"action": {
"default_popup": "popup.html"
},
"background": {
"service_worker": "js/background.js"
}
}
The User Interface (popup.html & content.css)
This creates the little window that drops down when you click the extension icon.
popup.html
HTML
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="css/content.css">
</head>
<body>
<div class="header">n8n AI Translator</div>
<label>Selected Text:</label>
<textarea id="sourceText" placeholder="Highlight text on page first..."></textarea>
<label>Target Language:</label>
<select id="targetLang">
<option value="Arabic">Arabic</option>
<option value="Spanish">Spanish</option>
<option value="French">French</option>
</select>
<button id="sendBtn">Translate with AI</button>
<div id="status"></div>
<label>Result:</label>
<textarea id="resultText" readonly></textarea>
<script src="js/popup.js"></script>
</body>
</html>
CSS
body {
width: 640px;
padding: 15px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f9fafb;
color: #333;
}
.header {
font-size: 18px;
font-weight: bold;
color: #ff6d5a; /* n8n Orange */
margin-bottom: 15px;
text-align: center;
border-bottom: 1px solid #eee;
padding-bottom: 10px;
}
label {
display: block;
font-size: 12px;
font-weight: 600;
margin-bottom: 5px;
color: #666;
}
textarea {
width: 100%;
height: 100px;
border: 2px solid #ddd;
border-radius: 6px;
padding: 8px;
font-size: 13px;
margin-bottom: 10px;
box-sizing: border-box;
background: white;
}
select {
width: 100%;
padding: 8px;
border-radius: 6px;
border: 1px solid #ddd;
margin-bottom: 15px;
}
button {
width: 100%;
background-color: #ff6d5a;
color: white;
border: none;
padding: 10px;
border-radius: 6px;
font-weight: bold;
cursor: pointer;
transition: background 0.2s;
}
button:hover {
background-color: #e55c49;
}
#status {
font-size: 11px;
text-align: center;
margin: 5px 0;
color: #ff6d5a;
min-height: 15px;
}
The Logic Scripts (popup.js & background.js)
Create a folder named js.
js/popup.js grabs the highlighted text from your browser and talks to the background script.
document.addEventListener('DOMContentLoaded', async () => {
const sourceArea = document.getElementById('sourceText');
// 1. Automatically grab highlighted text from the active tab
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
chrome.scripting.executeScript({
target: { tabId: tab.id },
func: () => window.getSelection().toString(),
}, (selection) => {
if (selection && selection[0].result) {
sourceArea.value = selection[0].result;
}
});
// 2. Handle the Send Button
document.getElementById('sendBtn').addEventListener('click', () => {
const text = sourceArea.value;
const lang = document.getElementById('targetLang').value;
const status = document.getElementById('status');
const resultArea = document.getElementById('resultText');
status.innerText = "AI is thinking...";
// 3. Send message to background.js
chrome.runtime.sendMessage({
action: "translate",
payload: { text, targetLanguage: lang, tone: "academic" }
}, (response) => {
if (response && response.status === "success") {
resultArea.value = response.data.translation;
status.innerText = "Done!";
} else {
status.innerText = "Error: Check n8n connection.";
console.error(response);
}
});
});
});
js/background.js is your Service Worker. It receives the data from the popup and securely pushes it to n8n via a POST request.
IMPORTANT: Replace <YOUR_N8N_DOMAIN> and <YOUR_WEBHOOK_ID> with your actual n8n webhook URL. Change <YOUR_SECRET_KEY> to a secure password of your choosing.
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === "translate") {
// Configure your specific n8n webhook path here
const webhookUrl = "https://<YOUR_N8N_DOMAIN>/webhook/<YOUR_WEBHOOK_ID>";
fetch(webhookUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-KEY": "<YOUR_SECRET_KEY>" // This must match n8n Header Auth!
},
body: JSON.stringify({
payload: request.payload
})
})
.then(res => res.json())
.then(data => sendResponse(data))
.catch(err => sendResponse({ status: "error", message: err.message }));
return true; // Keeps the message channel open for the async fetch
}
});
Security
& The Handshake (CORS) ![]()
This is where most beginners get stuck. Because your Extension lives on a different “Origin” (chrome-extension://…) than your n8n server (https://…), Chrome’s security model natively blocks them from talking to each other. This is called the Same-Origin Policy.
To bypass this safely, we use CORS (Cross-Origin Resource Sharing).
When your extension tries to send the translation request, Chrome first sends an invisible “Preflight” request to n8n asking, “Hey, are you allowed to talk to this extension?” If n8n doesn’t explicitly reply “Yes”, Chrome blocks the translation.
How to configure this in n8n:
-
Find your Chrome Extension ID (Go to chrome://extensions/ in Chrome, ensure Developer Mode is on, and copy the ID).
-
In your n8n Webhook Node, check the Options settings.
-
Add Allowed Origins and paste: chrome-extension://<YOUR_EXTENSION_ID>. (Never use * in production!)
-
Set up Header Auth in n8n. The Name should be X-API-KEY and the Value should be the secret key you put in background.js.
The Backend (n8n Workflow)
The backend is a simple 3-node workflow:
- Webhook Node: Set to POST and Response Mode: Using ‘Respond to Webhook’ Node.
Webhook node documentation | n8n Docs
- AI Agent Node: Processes the $json.body.payload.text with strict rules to output ONLY the translation.
AI Agent node documentation | n8n Docs
- Respond to Webhook Node: Safely formats the AI’s output back into a JSON object so the Chrome Extension can read it.
Here is the full workflow code. You can copy this block and paste it directly into your n8n canvas!
Note: You will need to re-select your OpenAI credentials and add your specific Webhook ID/Extension ID once imported.
Conclusion
You have successfully built the foundational “Connectivity Layer” between a browser extension and n8n. You’ve mastered:
-
MV3 Service Workers for memory-efficient background tasks.
-
Message Passing between UI (
popup.js) and Logic (background.js). -
CORS Security to safely allow cross-origin communication.
-
JSON Response formatting inside n8n to ensure clean data delivery.
Load your unpacked extension into Chrome, highlight some text, and watch the automation magic happen!
Read more:
Cross-Origin Resource Sharing (CORS) - HTTP | MDN.
What is CORS? - Cross-Origin Resource Sharing Explained - AWS.



