GOGETMUSCLE Community Two-Way Sync for Google Contact Labels & HubSpot

Two-Way Sync for Google Contact Labels & HubSpot

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

  1. Make a Backup: Before running this, export your Google Contacts to a CSV file just in case.

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

  1. Go to script.google.com.

  2. Click + New Project.

  3. Name it HubSpot Label Sync.

  4. Crucial: On the left sidebar, click the + next to Services, select People API, and click Add.

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

  1. In the Script Editor, select the function installTrigger from the toolbar and click Run.

  2. Accept the permissions. (You may need to click “Advanced” > “Go to Script (Unsafe)”).

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

  1. Go to App Marketplace > Google Contacts.
  2. Set up the sync (or edit existing).

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

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

Leave a Reply

Your email address will not be published.

Related Post