Overview
This guide walks you through building an n8n workflow that alerts you whenever more than one unique person from the same company visits your website through RB2B.
In this example, we use Google Sheets for storing account activity and Slack for sending notifications.
You’re welcome to swap these out for whatever tools fit your stack — Airtable, Notion, HubSpot, Salesforce, Teams, email, or any other system n8n supports. The workflow logic remains the same; only the “where you store it” and “how you notify your team” pieces change.
This workflow is ideal for surfacing high-intent accounts, supporting account-based outreach, and helping your sales or revenue team know when multiple people from the same company are actively researching your product.
What This Workflow Does
Each time RB2B identifies a visitor:
The visitor is normalized using your existing Normalize Visitor node.
The workflow checks whether the visitor’s company already exists in the
RB2B_Account_VisitsGoogle Sheet.If the company is new:
A new row is created.
The visitor is added as the first unique person for that account.
If the company already exists:
The visitor is added to the company’s list only if they haven’t visited before.
If they have visited before, their last-seen time is updated.
If the company now has more than one unique visitor:
The workflow builds a Slack message.
A notification is sent to your chosen Slack channel.
You’ll receive messages like:
🔥 MULTI-VISITOR ACCOUNT DETECTED 🔥
You’ve had 2 visitors from Acme Corp https://acme.com
• John Doe ([email protected]) — last seen: 2025-05-12
• Sarah Lee ([email protected]) — last seen: 2025-05-12
What You Need Before You Begin
This workflow branches from your existing visitor-tracking setup and assumes:
You already have a working Normalize Visitor code node
You already store per-visitor data in
RB2B_Person_VisitsYou already store per-event logs in
RB2B_Page_Visits
This workflow adds one more sheet:
Google Sheets Setup
Create a new sheet called: RB2B_Account_Visits and set up the following columns:
account_keycompany_namewebsitefirst_seen_accountlast_seen_accountunique_visitorsunique_visitors_count
unique_visitors will contain a JSON array with visitor details.
Step-By-Step Workflow
Below is the full list of nodes in the order they appear.
1. Code Node: Normalize Visitor
(Already part of your main workflow)
This provides:
identity_key
account_key (Company Name)
first/last name
company details
location
captured_url
seen_at
This is the starting point for the account-level logic.
2. Google Sheets: Get Account Row
Looks up the company in RB2B_Account_Visits.
Column:
account_keyValue:
={{ $json.account_key }}Sheet:
RB2B_Account_VisitsOperation: Get Row(s)
If a row exists → company has visited before
If no rows → new company
3. IF Node: Check if New Account
Condition:
{{ Object.keys($('Get Account Row').item.json).length > 0 }}TRUE path → existing account
FALSE path → new account
New Account Path (FALSE)
4. Code Node: Build New Account Row
Code:
const incoming = $('Normalize Visitor').item.json;
const firstSeen = incoming.seen_at || "";
// Build the first visitor entry for this account
const firstVisitor = {
first_name: incoming.first_name || "Anonymous",
last_name: incoming.last_name || "Visitor",
email: incoming.business_email || "",
last_seen_at: incoming.seen_at || ""
};
const uniqueVisitors = [firstVisitor];
return [
{
account_key: incoming.account_key || "",
company_name: incoming.company_name || "",
website: incoming.website || "",
first_seen_account: firstSeen,
last_seen_account: firstSeen,
// Store as JSON array of visitor objects
unique_visitors: JSON.stringify(uniqueVisitors),
unique_visitors_count: uniqueVisitors.length
}
];
5. Google Sheets: Add New Row to RB2B_Account_Visits
Operation:
Append RowSheet:
RB2B_Account_VisitsMapping: Automatic
Writes the new company row
No alert is sent yet.
Existing Account Path (TRUE)
6. Code Node: Update Account Row
Code:
// Existing account row from Sheets
const accountRow = $('Get Account Row').item.json;
// Latest normalized event
const incoming = $('Normalize Visitor').item.json;
const identityEmail = (incoming.business_email || "").trim().toLowerCase();
const seenAt = incoming.seen_at || "";
// Parse existing unique visitors array
let uniqueVisitors = [];
try {
if (accountRow.unique_visitors) {
uniqueVisitors = JSON.parse(accountRow.unique_visitors);
}
} catch (e) {
uniqueVisitors = [];
}
// Helper to build a key for a visitor (for dedupe)
function visitorKey(v) {
const email = (v.email || "").trim().toLowerCase();
if (email) return email;
const fn = (v.first_name || "").trim().toLowerCase();
const ln = (v.last_name || "").trim().toLowerCase();
return `${fn}|${ln}`;
}
// Key for the incoming visitor
const incomingKey = identityEmail || visitorKey({
first_name: incoming.first_name,
last_name: incoming.last_name,
email: ""
});
// Find existing visitor (if any)
let existingIndex = -1;
if (incomingKey) {
existingIndex = uniqueVisitors.findIndex(v => visitorKey(v) === incomingKey);
}
const prevUniqueCount = uniqueVisitors.length;
if (existingIndex === -1) {
// New unique visitor for this account
uniqueVisitors.push({
first_name: incoming.first_name || "Anonymous",
last_name: incoming.last_name || "Visitor",
email: incoming.business_email || "",
last_seen_at: seenAt
});
} else {
// Existing visitor: update their last_seen_at (and refresh other fields)
const existing = uniqueVisitors[existingIndex];
uniqueVisitors[existingIndex] = {
first_name: incoming.first_name || existing.first_name || "Anonymous",
last_name: incoming.last_name || existing.last_name || "Visitor",
email: incoming.business_email || existing.email || "",
last_seen_at: seenAt || existing.last_seen_at || ""
};
}
const newUniqueCount = uniqueVisitors.length;
const isNewUniqueVisitor = newUniqueCount > prevUniqueCount;
// Update account timestamps
const firstSeenAccount = accountRow.first_seen_account || seenAt || "";
const lastSeenAccount = seenAt || accountRow.last_seen_account || "";
return [
{
rowNumber: accountRow.rowNumber,
account_key: accountRow.account_key,
company_name: accountRow.company_name || incoming.company_name || "",
website: accountRow.website || incoming.website || "",
first_seen_account: firstSeenAccount,
last_seen_account: lastSeenAccount,
unique_visitors: JSON.stringify(uniqueVisitors),
unique_visitors_count: newUniqueCount,
}
];
7. Google Sheets: Update Row in RB2B_Account_Visits
Operation:
Update RowColumn to match on:
account_keyMapping: Automatic
This cleanly updates the account without duplicates.
8. IF Node: Double Check Unique Visitors
Triggers alerts only when a company has more than one unique visitor.
Condition 1: {{ $json.unique_visitors_count }} is greater than 1
Condition 2: {{ $json.company_name }} is not empty
If TRUE → send Slack alert
If FALSE → end workflow
Alert Path
9. Code Node: Build Company Alert Slack Block
This node builds a message like:
🔥 MULTI-VISITOR ACCOUNT DETECTED 🔥
You've had 3 visitors from *Acme Corp* https://acme.com
• John Doe ([email protected]) – last seen: 2025-05-12
• Sarah Lee ([email protected]) – last seen: 2025-05-12
The node outputs a single value:
slack_text
Code:
const account = $json;
// Parse visitors array from JSON stored in unique_visitors
let visitors = [];
try {
visitors = JSON.parse(account.unique_visitors || '[]');
} catch (e) {
visitors = [];
}
const companyName = account.company_name || account.account_key || "Unknown company";
const website = account.website || "";
const uniqueCount = account.unique_visitors_count || visitors.length || 0;
// Build lines of the Slack message
const lines = [];
lines.push(`🔥 *MULTI-VISITOR ACCOUNT DETECTED* 🔥`);
lines.push(`You've had ${uniqueCount} visitors from *${companyName}* ${website}`);
lines.push(""); // blank line
if (visitors.length === 0) {
lines.push(`_No visitor details available_`);
} else {
visitors.forEach(v => {
const first = v.first_name || "Anonymous";
const last = v.last_name || "Visitor";
const email = (v.email && v.email.trim().length > 0) ? v.email : "no email";
const lastSeen = v.last_seen_at || "unknown";
lines.push(`• *${first} ${last}* (${email}) – _last seen: ${lastSeen}_`);
});
}
const slack_text = lines.join('\n');
return [
{
slack_text
}
];
10. Slack Node: Send Slack Message
Message Type: Simple Text Message
Message Text:
={{ $json.slack_text }}Channel: your alert channel (e.g.
rb2b_n8n_alerts)Use Markdown: Off (simple Slack formatting still displays correctly)
This sends the final notification.










