Hi everyone,
Like many of you, I’ve struggled with a specific limitation in the HubSpot Google Contacts integration: It doesn’t sync Labels (Contact Groups).
If I tag a contact as “VIP” or “Partner” on my Android phone or Gmail, that information stays stuck in Google. HubSpot syncs the name and email but loses the segmentation. Conversely, if I segment people in HubSpot, I can’t easily push that back to a Google Contact Group on my phone.
The Solution:
I created a Google Apps Script that works alongside the native HubSpot Data Sync. It creates a Two-Way Sync between Google Labels and a HubSpot Custom Field.
Full transparency: I built this script with the help of AI. I am sharing it because it works well for me and solves a major headache, but please test it carefully!
Important Warnings
- Make a Backup: Before running this, export your Google Contacts to a CSV file just in case.
- Beta Testing: This logic is sound, but every environment is different. Please test this on a small batch of contacts or a secondary account first if possible. Feedback is welcome!
Step 1: Install the Google Script (Do this first!)
We install the script first so it can create the necessary fields in Google Contacts. This makes the HubSpot setup easier later.
- Go to script.google.com.
- Click + New Project.
- Name it HubSpot Label Sync.
- Crucial: On the left sidebar, click the + next to Services, select People API, and click Add.
- Paste the code below into the editor (delete any existing code).
/**
* CONFIGURATION
*/
const CONFIG = {
SYNC_FIELD_NAME: "HubSpot Labels", // Must match your HubSpot Property Name later
CHECKSUM_FIELD_NAME: "HS_Sync_Hash", // Technical field, do not touch
IGNORED_LABELS: ['contactGroups/myContacts', 'contactGroups/starred'],
SEPARATOR: ";"
};
function installTrigger() {
const triggers = ScriptApp.getProjectTriggers();
for (let t of triggers) {
if (t.getHandlerFunction() === 'startSync') {
console.log("Sync is already running!");
return;
}
}
// Runs every 15 minutes to handle large lists safely
ScriptApp.newTrigger('startSync').timeBased().everyMinutes(15).create();
console.log("✅ Installed! Sync runs every 15 mins.");
}
function startSync() {
const groupMap = fetchGroupMap();
let pageToken = null;
do {
const response = People.People.Connections.list('people/me', {
personFields: 'names,memberships,userDefined',
pageSize: 1000,
pageToken: pageToken
});
const connections = response.connections || [];
if (connections.length > 0) {
for (const person of connections) {
try { processContact(person, groupMap); }
catch (e) { console.error(`Error: ${e.message}`); }
}
}
pageToken = response.nextPageToken;
} while (pageToken);
}
function processContact(person, groupMap) {
const resourceName = person.resourceName;
const currentMemberships = person.memberships || [];
const realLabelNames = [];
currentMemberships.forEach(mem => {
if (mem.contactGroupMembership) {
const res = mem.contactGroupMembership.contactGroupResourceName;
const name = groupMap.idToName[res];
if (name && !CONFIG.IGNORED_LABELS.includes(res)) realLabelNames.push(name);
}
});
realLabelNames.sort();
const realLabelString = realLabelNames.join(CONFIG.SEPARATOR);
const userDefined = person.userDefined || [];
let syncFieldVal = "", checksumVal = "";
userDefined.forEach(f => {
if (f.key === CONFIG.SYNC_FIELD_NAME) syncFieldVal = f.value;
if (f.key === CONFIG.CHECKSUM_FIELD_NAME) checksumVal = f.value;
});
const syncFieldArr = syncFieldVal ? syncFieldVal.split(CONFIG.SEPARATOR).map(s => s.trim()).filter(s=>s).sort() : [];
const normalizedSyncString = syncFieldArr.join(CONFIG.SEPARATOR);
const googleChanged = (realLabelString !== checksumVal);
const hubspotChanged = (normalizedSyncString !== checksumVal);
if (!googleChanged && !hubspotChanged) return;
const contactToUpdate = { etag: person.etag };
const fieldsToUpdate = [];
if (googleChanged) {
const newUserDefined = rebuildUserDefined(userDefined, {
[CONFIG.SYNC_FIELD_NAME]: realLabelString,
[CONFIG.CHECKSUM_FIELD_NAME]: realLabelString
});
contactToUpdate.userDefined = newUserDefined;
fieldsToUpdate.push('userDefined');
} else if (hubspotChanged) {
const newUserDefined = rebuildUserDefined(userDefined, {
[CONFIG.CHECKSUM_FIELD_NAME]: normalizedSyncString
});
contactToUpdate.userDefined = newUserDefined;
fieldsToUpdate.push('userDefined');
const newMemberships = [];
currentMemberships.forEach(mem => {
if (mem.contactGroupMembership) {
const res = mem.contactGroupMembership.contactGroupResourceName;
if (CONFIG.IGNORED_LABELS.includes(res)) newMemberships.push({ contactGroupMembership: { contactGroupResourceName: res } });
}
});
syncFieldArr.forEach(labelName => {
let groupId = groupMap.nameToId[labelName];
if (!groupId) {
groupId = People.ContactGroups.create({ contactGroup: { name: labelName } }).resourceName;
groupMap.nameToId[labelName] = groupId;
}
newMemberships.push({ contactGroupMembership: { contactGroupResourceName: groupId } });
});
contactToUpdate.memberships = newMemberships;
fieldsToUpdate.push('memberships');
}
if (fieldsToUpdate.length > 0) {
People.People.updateContact(contactToUpdate, resourceName, { updatePersonFields: fieldsToUpdate.join(',') });
}
}
function fetchGroupMap() {
let pageToken = null;
const map = { nameToId: {}, idToName: {} };
do {
const response = People.ContactGroups.list({ groupFields: 'name', pageSize: 1000, pageToken: pageToken });
(response.contactGroups || []).forEach(g => {
const name = g.formattedName || g.name;
const id = g.resourceName;
if (name && id) { map.nameToId[name] = id; map.idToName[id] = name; }
});
pageToken = response.nextPageToken;
} while (pageToken);
return map;
}
function rebuildUserDefined(cur, updates) {
let arr = cur ? [...cur] : [];
for (const [key, val] of Object.entries(updates)) {
const idx = arr.findIndex(f => f.key === key);
if (idx > -1) arr[idx].value = val; else arr.push({ key: key, value: val });
}
return arr;
}
Step 2: Initialize the Data
-
In the Script Editor, select the function installTrigger from the toolbar and click Run.
-
Accept the permissions. (You may need to click “Advanced” > “Go to Script (Unsafe)”).
-
Once that is done, select startSync and click Run manually once.
- This will look at your current Google Contacts and write your existing labels into a new custom field called “HubSpot Labels”.
Step 3: Configure HubSpot
Now that your Google Contacts have the field, let’s set up the HubSpot sync.
-
Go to App Marketplace > Google Contacts.
-
Set up the sync (or edit existing).
-
Go to the Field Mappings tab and add a new mapping:
- Google Contacts side: add a custom field and name it exactly “HubSpot Labels”. It will look like you’re creating a new field, but it’ll actually pick up the existing field.
- HubSpot Side: Create a new single-line text field.
-
Turn on the sync.
How it works
- Google → HubSpot: The script detects you added a Label (e.g., “VIP”), writes “VIP” to the text field, and HubSpot syncs that text.
- HubSpot → Google: You write “VIP” in the HubSpot text field, the sync pushes it to Google, and the script detects the change and automatically creates/adds the “VIP” Contact Group.
Hope this helps! Let me know if you run into any issues.