Skip to main content

n8n Workflow: Detecting When Multiple People From the Same Company Visit Your Website

Updated over a week ago

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:

  1. The visitor is normalized using your existing Normalize Visitor node.

  2. The workflow checks whether the visitor’s company already exists in the RB2B_Account_Visits Google Sheet.

  3. If the company is new:

    • A new row is created.

    • The visitor is added as the first unique person for that account.

  4. 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.

  5. 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_Visits

  • You 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_key

  • company_name

  • website

  • first_seen_account

  • last_seen_account

  • unique_visitors

  • unique_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_key

  • Value: ={{ $json.account_key }}

  • Sheet: RB2B_Account_Visits

  • Operation: 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 Row

  • Sheet: RB2B_Account_Visits

  • Mapping: 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 Row

  • Column to match on: account_key

  • Mapping: 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.

Did this answer your question?