# Custom Coded Workflow for Association Labels

**URL:** <https://community.hubspot.com/t/custom-coded-workflow-for-association-labels/139769>\
**Category:** APIs & Integrations\
**Tags:** professional\
**Created:** [September 18, 2025, 10:28pm UTC](https://community.hubspot.com/t/custom-coded-workflow-for-association-labels/139769 "2025-09-18T22:28:47Z")\
**Posts on this page:** 3\
**Page:** 1

<div class="post-metadata">

**Author:** ![EKasmarick6](https://avatars.discourse-cdn.com/v4/letter/e/8edcca/32.png) [@EKasmarick6](https://community.hubspot.com/u/EKasmarick6)\
**Post date:** [September 18, 2025, 10:28pm UTC](https://community.hubspot.com/t/custom-coded-workflow-for-association-labels/139769/1 "2025-09-18T22:28:47Z")

</div>

I am attempting to build a custom coded workflow that will apply an association label to the most recently updated custom object associated with a contact.

1. Search to see if there is more than one custom object associated, if only one, apply active policy label to that object and contact.

2. If thereis more than 1 custom object on the record, add the association label to the most updated custom object and remove it from the older one.

It is running successfully but not adding the label. Here is the code I am using.

For the scopes, we have contact read and write and custom object read and write.

/\*\*  
\* Policy-based Custom Code (Node 18)  
\* Discovers the correct associationTypeId + direction for label `active_policy`  
\* and applies it so exactly one Policy per Contact is labeled (winner = most recent by hs\_lastmodifieddate).  
\* Logs full diagnostics and returns them via outputFields.debug.  
\*/

const HUBSPOT\_API\_BASE = ‘[https://api.hubapi.com](https://api.hubapi.com)’;  
const POLICY\_TYPE\_ID = ‘2-49987112’; // custom object type id  
const CONTACTS\_TYPE = ‘contacts’;  
const SORT\_PROPERTY = ‘hs\_lastmodifieddate’;  
const LABEL\_INTERNAL = ‘active\_policy’;

async function hsFetch(url, options) {  
const res = await fetch(url, options);  
const txt = await res.text();  
if (!res.ok) throw new Error(`${options?.method || ‘GET’} ${url} → ${res.status}: ${txt.slice(0, 1200)}`);  
try { return JSON.parse(txt); } catch { return {}; }  
}  
async function hsFetchAllow404(url, options) {  
const res = await fetch(url, options);  
const txt = await res.text();  
if (res.status === 404) return null;  
if (!res.ok) throw new Error(`${options?.method || ‘GET’} ${url} → ${res.status}: ${txt.slice(0, 1200)}`);  
try { return JSON.parse(txt); } catch { return {}; }  
}

async function listAssociations(headers, fromType, fromId, toType) {  
let after, out = [];  
do {  
const u = new URL(`${HUBSPOT_API_BASE}/crm/v4/objects/${fromType}/${fromId}/associations/${toType}`);  
if (after) u.searchParams.set(‘after’, after);  
const d = await hsFetch(u.toString(), { headers });  
(d.results || []).forEach(x =\> out.push({  
toObjectId: String(x.toObjectId),  
labels: Array.isArray(x.labels) ? x.labels : []  
}));  
after = d.paging?.next?.after;  
} while (after);  
return out;  
}

async function batchReadPolicies(headers, ids) {  
if (!ids.length) return [];  
const d = await hsFetch(`${HUBSPOT_API_BASE}/crm/v3/objects/${POLICY_TYPE_ID}/batch/read`, {  
method: ‘POST’,  
headers,  
body: JSON.stringify({ properties:[SORT\_PROPERTY], inputs: ids.map(id =\> ({ id:String(id) })) })  
});  
return (d.results || []).map(o =\> ({  
id: String(o.id),  
lastModified: o.properties?.[SORT\_PROPERTY] ? Date.parse(o.properties[SORT\_PROPERTY]) : 0  
}));  
}

// Discover label definitions for a direction (fromType → toType)  
async function discoverLabelDefs(headers, fromType, toType) {  
const url = `${HUBSPOT_API_BASE}/crm/v4/associations/${fromType}/${toType}/labels`;  
const d = await hsFetch(url, { headers }); // { results: [{associationTypeId, label:{internalName,name}}] }  
const results = Array.isArray(d.results) ? d.results : [];  
const hit = results.find(x =\>  
x?.label?.internalName === LABEL\_INTERNAL || x?.label?.name === LABEL\_INTERNAL  
);  
return { all: results, defId: hit ? String(hit.associationTypeId) : null };  
}

// Try both shapes to set labels for ONE association  
async function setLabels(headers, fromType, fromId, toType, toId, defId, labels) {  
// A) definition id in PATH  
const urlA = `${HUBSPOT_API_BASE}/crm/v4/objects/${fromType}/${fromId}/associations/${toType}/${toId}/labels/${defId}`;  
const resA = await hsFetchAllow404(urlA, { method: ‘PUT’, headers, body: JSON.stringify({ labels }) });  
if (resA) return { ok: true, mode: ‘path’ };

// B) definition id in BODY  
const urlB = `${HUBSPOT_API_BASE}/crm/v4/objects/${fromType}/${fromId}/associations/${toType}/${toId}/labels`;  
const resB = await hsFetchAllow404(urlB, {  
method: ‘PUT’, headers, body: JSON.stringify({ labels, associationTypeId: Number(defId) })  
});  
if (resB) return { ok: true, mode: ‘body’ };

return { ok: false, mode: null };  
}

exports.main = async (event, callback) =\> {  
const token = process.env.PRIVATE\_APP\_TOKEN;  
if (!token) {  
console.error(‘Missing PRIVATE\_APP\_TOKEN secret.’);  
return callback({ outputFields: { debug: JSON.stringify({ error: ‘Missing PRIVATE\_APP\_TOKEN’ }) } });  
}

const policyId =  
event?.inputFields?.policyId ||  
event?.inputFields?.hs\_object\_id ||  
event?.object?.objectId ||  
null;  
if (!policyId) {  
console.error(‘Missing policyId. Map input “policyId” to {{ object.hs\_object\_id }}.’);  
return callback({ outputFields: { debug: JSON.stringify({ error: ‘Missing policyId’ }) } });  
}

const headers = { Authorization: `Bearer ${token}`, ‘Content-Type’: ‘application/json’ };

// Ensure policy exists  
await hsFetch(`${HUBSPOT_API_BASE}/crm/v3/objects/${POLICY_TYPE_ID}/${policyId}?properties=${encodeURIComponent(SORT_PROPERTY)}`, { headers });

// Contacts associated with this policy (Policy → Contacts)  
const contactAssocs = await listAssociations(headers, POLICY\_TYPE\_ID, policyId, CONTACTS\_TYPE);  
const contactIds = contactAssocs.map(a =\> a.toObjectId);  
if (!contactIds.length) {  
const payload = { success: true, info: ‘No contacts associated with this policy.’, policyId };  
console.log(JSON.stringify(payload, null, 2));  
return callback({ outputFields: { debug: JSON.stringify(payload) } });  
}

// Discover label defs in BOTH directions  
const fwd = await discoverLabelDefs(headers, CONTACTS\_TYPE, POLICY\_TYPE\_ID); // contacts → policy  
const rev = await discoverLabelDefs(headers, POLICY\_TYPE\_ID, CONTACTS\_TYPE); // policy → contacts

// Choose the direction that actually has the label  
// If both directions expose it, prefer reverse so it shows on the Policy page  
let APPLY\_FROM, APPLY\_TO, DEF\_ID;  
if (rev.defId) {  
APPLY\_FROM = POLICY\_TYPE\_ID; APPLY\_TO = CONTACTS\_TYPE; DEF\_ID = rev.defId;  
} else if (fwd.defId) {  
APPLY\_FROM = CONTACTS\_TYPE; APPLY\_TO = POLICY\_TYPE\_ID; DEF\_ID = fwd.defId;  
} else {  
const payload = {  
success: false,  
error: `Label “${LABEL_INTERNAL}” not found by API in either direction.`,  
labelsForward: fwd.all, labelsReverse: rev.all  
};  
console.log(JSON.stringify(payload, null, 2));  
return callback({ outputFields: { debug: JSON.stringify(payload) } });  
}

const actions = [];

// For each contact, ensure exactly one active label  
for (const contactId of contactIds) {  
// All policies for this contact (Contact → Policy)  
const policyAssocsForContact = await listAssociations(headers, CONTACTS\_TYPE, contactId, POLICY\_TYPE\_ID);  
if (!policyAssocsForContact.length) {  
actions.push({ contactId, info: ‘Contact has no policy associations.’ });  
continue;  
}

// Determine winner (single policy or most recent by hs\_lastmodifieddate)  
let winnerId = null;  
if (policyAssocsForContact.length === 1) {  
winnerId = policyAssocsForContact[0].toObjectId;  
} else {  
const policyIds = […new Set(policyAssocsForContact.map(p =\> p.toObjectId))];  
const details = await batchReadPolicies(headers, policyIds);  
if (details.length) {  
details.sort((a,b)=\> (b.lastModified||0)-(a.lastModified||0));  
winnerId = details[0].id;  
}  
}  
if (!winnerId) {  
actions.push({ contactId, warning: ‘No winner policy resolved.’ });  
continue;  
}

// Apply label to winner and clear from others in the chosen direction  
for (const assoc of policyAssocsForContact) {  
const pId = assoc.toObjectId;  
const isWinner = pId === winnerId;  
const desired = isWinner ? [LABEL\_INTERNAL] : [];

// Map from/to IDs for the chosen direction  
const fromId = (APPLY\_FROM === POLICY\_TYPE\_ID) ? pId : contactId;  
const toId = (APPLY\_TO === POLICY\_TYPE\_ID) ? pId : contactId;

const res = await setLabels(headers, APPLY\_FROM, fromId, APPLY\_TO, toId, DEF\_ID, desired);  
actions.push({  
step: ‘apply’,  
direction: `${APPLY_FROM}→${APPLY_TO}`,  
defId: DEF\_ID,  
contactId, policyId: pId,  
isWinner,  
appliedLabels: desired,  
ok: res.ok,  
mode: res.mode  
});  
}

// Verify from the perspective that will be visible on the Policy page  
const verifyFromPolicy = await listAssociations(headers, POLICY\_TYPE\_ID, winnerId, CONTACTS\_TYPE);  
actions.push({ step: ‘verify’, winnerPolicyId: winnerId, seenFromPolicy: verifyFromPolicy });  
}

const payload = {  
success: true,  
ranForPolicyId: String(policyId),  
chosenDirection: `${APPLY_FROM}→${APPLY_TO}`,  
defIdUsed: DEF\_ID,  
contactsTouched: contactIds,  
diagnostics: { labelsForward: fwd.all, labelsReverse: rev.all },  
actions  
};

console.log(JSON.stringify(payload, null, 2));  
return callback({ outputFields: { debug: JSON.stringify(payload) } });  
};

---

<div class="post-metadata">

**Author:** ![BrandonWoodruff](https://d10pqhm3lwygmk.cloudfront.net/user_avatar/community.hubspot.com/brandonwoodruff/32/61045_2.png) [@BrandonWoodruff](https://community.hubspot.com/u/BrandonWoodruff)\
**Post date:** [September 25, 2025, 12:55pm UTC](https://community.hubspot.com/t/custom-coded-workflow-for-association-labels/139769/2 "2025-09-25T12:55:00Z")

</div>

Hello,

Here are some things I would confirm:

- **Wrong direction** : Labels exist in one direction. If your UI label is configured on **Policy → Contact** but you apply on **Contact → Policy** , HubSpot won’t add it. The code already prefers the reverse direction when found; confirm in Settings.
- **Multiple associations between the same records** : Labels are applied per **association type id**. Ensure you are updating the **same association type** your records actually use.

Let me know if this helps, or if you have any other questions!

* * *

✔ Was I able to help answer your question? Help the community by **marking it as a solution.**

| ![](https://d2qpr2jzcazej7.cloudfront.net/original/3X/d/3/d314478a36473977ed45320812b9331d25e0d3c4.jpeg) | **[Brandon Woodruff](https://www.linkedin.com/in/brandon-woody-woodruff/)**  
Senior Software Developer @ [Pearagon](https://www.pearagon.com/)Still have questions? Reach out at [brandon@pearagon.com](mailto:brandon@pearagon.com) ![](https://d2qpr2jzcazej7.cloudfront.net/original/3X/d/c/dcad77e6cf96145dd117147531ae4423b9b5cd91.png) |

---

<div class="post-metadata">

**Author:** ![Jaycee\_Lewis](https://d10pqhm3lwygmk.cloudfront.net/user_avatar/community.hubspot.com/jaycee_lewis/32/61352_2.png) [@Jaycee\_Lewis](https://community.hubspot.com/u/Jaycee_Lewis)\
**Post date:** [September 26, 2025, 2:40pm UTC](https://community.hubspot.com/t/custom-coded-workflow-for-association-labels/139769/3 "2025-09-26T14:40:56Z")

</div>

Hey, @EKasmarick6 👋 Can you copy the output of your `debug`field and share it? I think the values for these fields will be helpful:

- diagnostics
- defIdUsed
- actions

Thanks! — Jaycee
