I’m working with a client in the housing industry who uses the Listing and Deal objects to manage their properties and sales. Here's the scenario: When a contact is associated with a listing and a deal, and then raises a ticket (e.g. a complaint about the floor in the house), we want the associated listing and deal to automatically be linked to that ticket. This way, the service rep has immediate context and a full overview in the help desk. (and they can follow potential issues on that specific listing even though the buyer sell the house down the road) So far, I haven’t found a way to do this with native workflows. Has anyone found a workaround or integration that could support this kind of cross-object association? Appreciate any insights!
@JRaaschou yes, did you review the workflow action I shared in my previous reply?
You would create a ticket-based workflow, use the 'Create associations' workflow action and would then have to find an identifier that is shared between the ticket, the listing, and the deal.
How exactly that would be done depends on your setup. Via additional workflows or sync properties, you would have to make sure that these records have a property where they share a value. For example, this could be a listing name or ID that is also present on the deal and also present on the ticket.
Karsten Köhler HubSpot Freelancer | RevOps & CRM Consultant | Community Hall of Famer
I ran into the same challenge – having to build manual workflows just to get the Record ID onto the ticket. The process of going from listings and deals → to the contact → and then from the contact → to the ticket is just unnecessarily complex.
Honestly, it’s mind-boggling that HubSpot doesn’t support something this basic out of the box. The whole point of associations starts to feel pretty pointless when you can’t easily cross-associate objects.
Anyway, Claude 4.0 came to the rescue with a custom API call through Operations Hub. 🙌 So if anyone else is stuck in the same situation – here’s the code, totally free to use: 😄
const hubspot = require('@hubspot/api-client');
exports.main = async (event) => {
const hubspotClient = new hubspot.Client({
accessToken: process.env.HAPIKEY,
});
const ticketId = event.inputFields['hs_ticket_id'];
const contactId = event.inputFields['hs_contact_id'];
const LISTINGS_OBJECT_TYPE_ID = '0-420';
try {
console.log(`🎫 Ticket ID: ${ticketId}`);
console.log(`👤 Contact ID: ${contactId}`);
if (!contactId) {
console.log("❗️No contact ID provided.");
return { success: false, error: "No contact ID" };
}
// LØSNING 1: Hent deals
console.log("🔍 Fetching deals using SDK...");
let dealIds = [];
try {
const dealsResponse = await hubspotClient.crm.associations.v4.basicApi.getPage(
'contacts',
contactId,
'deals'
);
dealIds = dealsResponse?.results?.map(r => r.toObjectId) ?? [];
console.log(`💼 Found ${dealIds.length} deals`);
} catch (dealError) {
console.log("⚠️ SDK deals failed, trying fallback...", dealError.message);
try {
const contactDeals = await hubspotClient.crm.contacts.associationsApi.getAll(
contactId,
'deals'
);
dealIds = contactDeals?.results?.map(r => r.id) ?? [];
} catch (basicError) {
console.log("❌ Basic deals API also failed:", basicError.message);
}
}
console.log(`💼 Final deals count: ${dealIds.length}`);
// LØSNING 2: Hent listings med flere strategies
console.log("🔍 Fetching listings with multiple strategies...");
let listingIds = [];
// Strategy 1: Forskellige object identifiers
const listingIdentifiers = [LISTINGS_OBJECT_TYPE_ID, 'listings', '2-420', 'p420'];
for (const identifier of listingIdentifiers) {
if (listingIds.length > 0) break;
console.log(`🔄 Trying identifier: ${identifier}`);
try {
const listingsResponse = await hubspotClient.crm.associations.v4.basicApi.getPage(
'contacts',
contactId,
identifier
);
listingIds = listingsResponse?.results?.map(r => r.toObjectId) ?? [];
if (listingIds.length > 0) {
console.log(`✅ Found ${listingIds.length} listings with identifier ${identifier}`);
break;
}
} catch (error) {
console.log(`❌ Identifier ${identifier} failed:`, error.message);
}
}
// Strategy 2: v3 associations
if (listingIds.length === 0) {
console.log("🔄 Trying v3 associations API...");
try {
const v3Response = await hubspotClient.crm.contacts.associationsApi.getAll(
contactId,
LISTINGS_OBJECT_TYPE_ID
);
listingIds = v3Response?.results?.map(r => r.id) ?? [];
console.log(`📋 v3 found ${listingIds.length} listings`);
} catch (v3Error) {
console.log("❌ v3 associations failed:", v3Error.message);
}
}
// Strategy 3: Direct search
if (listingIds.length === 0) {
console.log("🔄 Searching for listings directly...");
const searchFilters = [
{ propertyName: 'associations.contact', operator: 'EQ', value: contactId },
{ propertyName: 'hs_associated_contact_id', operator: 'EQ', value: contactId },
{ propertyName: 'contact_id', operator: 'EQ', value: contactId }
];
for (const filter of searchFilters) {
if (listingIds.length > 0) break;
try {
const searchResponse = await hubspotClient.crm.objects.searchApi.doSearch(LISTINGS_OBJECT_TYPE_ID, {
filterGroups: [{ filters: [filter] }],
limit: 100,
properties: ['hs_object_id']
});
listingIds = searchResponse?.results?.map(r => r.id) ?? [];
if (listingIds.length > 0) {
console.log(`✅ Found ${listingIds.length} listings with search`);
break;
}
} catch (searchError) {
console.log(`❌ Search failed:`, searchError.message);
}
}
}
console.log(`📋 Final listings count: ${listingIds.length}`);
// Debug hvis ingen listings findes
if (listingIds.length === 0) {
console.log("🔍 No listings found. Getting object properties...");
try {
const propertiesResponse = await hubspotClient.crm.properties.coreApi.getAll(LISTINGS_OBJECT_TYPE_ID);
const associationProps = propertiesResponse?.results?.filter(p =>
p.name.includes('contact') || p.name.includes('association')
) ?? [];
console.log(`📋 Available contact properties:`, associationProps.map(p => p.name));
} catch (propsError) {
console.log("❌ Properties fetch failed:", propsError.message);
}
}
// LØSNING 3: Lav associations
console.log(`🔗 Creating associations...`);
// Associate deals
for (const dealId of dealIds) {
try {
await hubspotClient.crm.associations.v4.basicApi.create(
'tickets',
ticketId,
'deals',
dealId,
[{
associationCategory: 'HUBSPOT_DEFINED',
associationTypeId: 28
}]
);
console.log(`✅ Associated deal ${dealId}`);
} catch (assocError) {
console.log(`❌ Failed to associate deal ${dealId}:`, assocError.message);
}
}
// Associate listings - med omfattende debugging
for (const listingId of listingIds) {
console.log(`🔍 Attempting to associate listing ${listingId} with ticket ${ticketId}`);
// Først - tjek om listing faktisk eksisterer
try {
const listingCheck = await hubspotClient.crm.objects.basicApi.getById(LISTINGS_OBJECT_TYPE_ID, listingId);
console.log(`📋 Listing ${listingId} exists:`, listingCheck.id);
} catch (checkError) {
console.log(`❌ Listing ${listingId} doesn't exist:`, checkError.message);
continue;
}
// Find korrekte association type IDs for listings
try {
console.log(`🔍 Getting association types between tickets and ${LISTINGS_OBJECT_TYPE_ID}...`);
const assocTypes = await hubspotClient.crm.associations.schemaApi.getAll('tickets', LISTINGS_OBJECT_TYPE_ID);
console.log(`📋 Available association types:`, JSON.stringify(assocTypes?.results?.map(r => ({
id: r.typeId,
label: r.label,
name: r.name
})), null, 2));
// Brug den første tilgængelige association type
const validAssocType = assocTypes?.results?.[0]?.typeId || 1;
console.log(`🎯 Using association type ID: ${validAssocType}`);
// Prøv association med korrekt type
await hubspotClient.crm.associations.v4.basicApi.create(
'tickets',
ticketId,
LISTINGS_OBJECT_TYPE_ID,
listingId,
[{
associationCategory: 'HUBSPOT_DEFINED',
associationTypeId: validAssocType
}]
);
console.log(`✅ Associated listing ${listingId} with correct type ${validAssocType}`);
// Verificer association blev lavet
const verification = await hubspotClient.crm.associations.v4.basicApi.getPage(
'tickets',
ticketId,
LISTINGS_OBJECT_TYPE_ID
);
const associatedListings = verification?.results?.map(r => r.toObjectId) || [];
console.log(`🔍 Verification - ticket ${ticketId} is now associated with listings:`, associatedListings);
if (associatedListings.includes(listingId)) {
console.log(`✅ CONFIRMED: Listing ${listingId} is properly associated!`);
} else {
console.log(`❌ FAILED: Listing ${listingId} association not found in verification`);
}
} catch (schemaError) {
console.log(`❌ Schema fetch failed, trying manual approaches:`, schemaError.message);
// Fallback - prøv forskellige association approaches
const approaches = [
{
name: 'Batch API with generic type',
action: () => hubspotClient.crm.associations.batchApi.create('tickets', LISTINGS_OBJECT_TYPE_ID, {
inputs: [{
from: { id: ticketId },
to: { id: listingId },
type: 'ticket_to_listing'
}]
})
},
{
name: 'Direct API v3 style',
action: () => hubspotClient.apiRequest({
method: 'PUT',
path: `/crm/v3/objects/tickets/${ticketId}/associations/${LISTINGS_OBJECT_TYPE_ID}/${listingId}/ticket_to_listing`
})
},
{
name: 'Direct API v4 style',
action: () => hubspotClient.apiRequest({
method: 'PUT',
path: `/crm/v4/objects/tickets/${ticketId}/associations/${LISTINGS_OBJECT_TYPE_ID}/${listingId}`,
body: {
associationCategory: 'HUBSPOT_DEFINED',
associationTypeId: 1
}
})
}
];
for (const approach of approaches) {
try {
console.log(`🔄 Trying ${approach.name}...`);
await approach.action();
console.log(`✅ ${approach.name} succeeded`);
// Test verification igen
try {
const verification2 = await hubspotClient.crm.associations.v4.basicApi.getPage(
'tickets',
ticketId,
LISTINGS_OBJECT_TYPE_ID
);
const associatedListings2 = verification2?.results?.map(r => r.toObjectId) || [];
if (associatedListings2.includes(listingId)) {
console.log(`✅ CONFIRMED: ${approach.name} worked! Listing is associated.`);
break;
} else {
console.log(`❌ ${approach.name} didn't create visible association`);
}
} catch (verifyError) {
console.log(`❌ Verification failed after ${approach.name}`);
}
} catch (approachError) {
console.log(`❌ ${approach.name} failed:`, approachError.message);
}
}
}
}
const result = {
success: true,
associatedDeals: dealIds.length,
associatedListings: listingIds.length,
ticketId: ticketId
};
console.log(`✅ Final result:`, JSON.stringify(result));
return result;
} catch (err) {
console.error("🚫 Global Error:", err.message);
return {
success: false,
error: err.message
};
}
};
The specific issue is that the contact that raises the ticket is associated to the deal and listing and i would like the ticket to automatically be associated to the deal and listing.
@JRaaschou yes, did you review the workflow action I shared in my previous reply?
You would create a ticket-based workflow, use the 'Create associations' workflow action and would then have to find an identifier that is shared between the ticket, the listing, and the deal.
How exactly that would be done depends on your setup. Via additional workflows or sync properties, you would have to make sure that these records have a property where they share a value. For example, this could be a listing name or ID that is also present on the deal and also present on the ticket.
Karsten Köhler HubSpot Freelancer | RevOps & CRM Consultant | Community Hall of Famer
I ran into the same challenge – having to build manual workflows just to get the Record ID onto the ticket. The process of going from listings and deals → to the contact → and then from the contact → to the ticket is just unnecessarily complex.
Honestly, it’s mind-boggling that HubSpot doesn’t support something this basic out of the box. The whole point of associations starts to feel pretty pointless when you can’t easily cross-associate objects.
Anyway, Claude 4.0 came to the rescue with a custom API call through Operations Hub. 🙌 So if anyone else is stuck in the same situation – here’s the code, totally free to use: 😄
const hubspot = require('@hubspot/api-client');
exports.main = async (event) => {
const hubspotClient = new hubspot.Client({
accessToken: process.env.HAPIKEY,
});
const ticketId = event.inputFields['hs_ticket_id'];
const contactId = event.inputFields['hs_contact_id'];
const LISTINGS_OBJECT_TYPE_ID = '0-420';
try {
console.log(`🎫 Ticket ID: ${ticketId}`);
console.log(`👤 Contact ID: ${contactId}`);
if (!contactId) {
console.log("❗️No contact ID provided.");
return { success: false, error: "No contact ID" };
}
// LØSNING 1: Hent deals
console.log("🔍 Fetching deals using SDK...");
let dealIds = [];
try {
const dealsResponse = await hubspotClient.crm.associations.v4.basicApi.getPage(
'contacts',
contactId,
'deals'
);
dealIds = dealsResponse?.results?.map(r => r.toObjectId) ?? [];
console.log(`💼 Found ${dealIds.length} deals`);
} catch (dealError) {
console.log("⚠️ SDK deals failed, trying fallback...", dealError.message);
try {
const contactDeals = await hubspotClient.crm.contacts.associationsApi.getAll(
contactId,
'deals'
);
dealIds = contactDeals?.results?.map(r => r.id) ?? [];
} catch (basicError) {
console.log("❌ Basic deals API also failed:", basicError.message);
}
}
console.log(`💼 Final deals count: ${dealIds.length}`);
// LØSNING 2: Hent listings med flere strategies
console.log("🔍 Fetching listings with multiple strategies...");
let listingIds = [];
// Strategy 1: Forskellige object identifiers
const listingIdentifiers = [LISTINGS_OBJECT_TYPE_ID, 'listings', '2-420', 'p420'];
for (const identifier of listingIdentifiers) {
if (listingIds.length > 0) break;
console.log(`🔄 Trying identifier: ${identifier}`);
try {
const listingsResponse = await hubspotClient.crm.associations.v4.basicApi.getPage(
'contacts',
contactId,
identifier
);
listingIds = listingsResponse?.results?.map(r => r.toObjectId) ?? [];
if (listingIds.length > 0) {
console.log(`✅ Found ${listingIds.length} listings with identifier ${identifier}`);
break;
}
} catch (error) {
console.log(`❌ Identifier ${identifier} failed:`, error.message);
}
}
// Strategy 2: v3 associations
if (listingIds.length === 0) {
console.log("🔄 Trying v3 associations API...");
try {
const v3Response = await hubspotClient.crm.contacts.associationsApi.getAll(
contactId,
LISTINGS_OBJECT_TYPE_ID
);
listingIds = v3Response?.results?.map(r => r.id) ?? [];
console.log(`📋 v3 found ${listingIds.length} listings`);
} catch (v3Error) {
console.log("❌ v3 associations failed:", v3Error.message);
}
}
// Strategy 3: Direct search
if (listingIds.length === 0) {
console.log("🔄 Searching for listings directly...");
const searchFilters = [
{ propertyName: 'associations.contact', operator: 'EQ', value: contactId },
{ propertyName: 'hs_associated_contact_id', operator: 'EQ', value: contactId },
{ propertyName: 'contact_id', operator: 'EQ', value: contactId }
];
for (const filter of searchFilters) {
if (listingIds.length > 0) break;
try {
const searchResponse = await hubspotClient.crm.objects.searchApi.doSearch(LISTINGS_OBJECT_TYPE_ID, {
filterGroups: [{ filters: [filter] }],
limit: 100,
properties: ['hs_object_id']
});
listingIds = searchResponse?.results?.map(r => r.id) ?? [];
if (listingIds.length > 0) {
console.log(`✅ Found ${listingIds.length} listings with search`);
break;
}
} catch (searchError) {
console.log(`❌ Search failed:`, searchError.message);
}
}
}
console.log(`📋 Final listings count: ${listingIds.length}`);
// Debug hvis ingen listings findes
if (listingIds.length === 0) {
console.log("🔍 No listings found. Getting object properties...");
try {
const propertiesResponse = await hubspotClient.crm.properties.coreApi.getAll(LISTINGS_OBJECT_TYPE_ID);
const associationProps = propertiesResponse?.results?.filter(p =>
p.name.includes('contact') || p.name.includes('association')
) ?? [];
console.log(`📋 Available contact properties:`, associationProps.map(p => p.name));
} catch (propsError) {
console.log("❌ Properties fetch failed:", propsError.message);
}
}
// LØSNING 3: Lav associations
console.log(`🔗 Creating associations...`);
// Associate deals
for (const dealId of dealIds) {
try {
await hubspotClient.crm.associations.v4.basicApi.create(
'tickets',
ticketId,
'deals',
dealId,
[{
associationCategory: 'HUBSPOT_DEFINED',
associationTypeId: 28
}]
);
console.log(`✅ Associated deal ${dealId}`);
} catch (assocError) {
console.log(`❌ Failed to associate deal ${dealId}:`, assocError.message);
}
}
// Associate listings - med omfattende debugging
for (const listingId of listingIds) {
console.log(`🔍 Attempting to associate listing ${listingId} with ticket ${ticketId}`);
// Først - tjek om listing faktisk eksisterer
try {
const listingCheck = await hubspotClient.crm.objects.basicApi.getById(LISTINGS_OBJECT_TYPE_ID, listingId);
console.log(`📋 Listing ${listingId} exists:`, listingCheck.id);
} catch (checkError) {
console.log(`❌ Listing ${listingId} doesn't exist:`, checkError.message);
continue;
}
// Find korrekte association type IDs for listings
try {
console.log(`🔍 Getting association types between tickets and ${LISTINGS_OBJECT_TYPE_ID}...`);
const assocTypes = await hubspotClient.crm.associations.schemaApi.getAll('tickets', LISTINGS_OBJECT_TYPE_ID);
console.log(`📋 Available association types:`, JSON.stringify(assocTypes?.results?.map(r => ({
id: r.typeId,
label: r.label,
name: r.name
})), null, 2));
// Brug den første tilgængelige association type
const validAssocType = assocTypes?.results?.[0]?.typeId || 1;
console.log(`🎯 Using association type ID: ${validAssocType}`);
// Prøv association med korrekt type
await hubspotClient.crm.associations.v4.basicApi.create(
'tickets',
ticketId,
LISTINGS_OBJECT_TYPE_ID,
listingId,
[{
associationCategory: 'HUBSPOT_DEFINED',
associationTypeId: validAssocType
}]
);
console.log(`✅ Associated listing ${listingId} with correct type ${validAssocType}`);
// Verificer association blev lavet
const verification = await hubspotClient.crm.associations.v4.basicApi.getPage(
'tickets',
ticketId,
LISTINGS_OBJECT_TYPE_ID
);
const associatedListings = verification?.results?.map(r => r.toObjectId) || [];
console.log(`🔍 Verification - ticket ${ticketId} is now associated with listings:`, associatedListings);
if (associatedListings.includes(listingId)) {
console.log(`✅ CONFIRMED: Listing ${listingId} is properly associated!`);
} else {
console.log(`❌ FAILED: Listing ${listingId} association not found in verification`);
}
} catch (schemaError) {
console.log(`❌ Schema fetch failed, trying manual approaches:`, schemaError.message);
// Fallback - prøv forskellige association approaches
const approaches = [
{
name: 'Batch API with generic type',
action: () => hubspotClient.crm.associations.batchApi.create('tickets', LISTINGS_OBJECT_TYPE_ID, {
inputs: [{
from: { id: ticketId },
to: { id: listingId },
type: 'ticket_to_listing'
}]
})
},
{
name: 'Direct API v3 style',
action: () => hubspotClient.apiRequest({
method: 'PUT',
path: `/crm/v3/objects/tickets/${ticketId}/associations/${LISTINGS_OBJECT_TYPE_ID}/${listingId}/ticket_to_listing`
})
},
{
name: 'Direct API v4 style',
action: () => hubspotClient.apiRequest({
method: 'PUT',
path: `/crm/v4/objects/tickets/${ticketId}/associations/${LISTINGS_OBJECT_TYPE_ID}/${listingId}`,
body: {
associationCategory: 'HUBSPOT_DEFINED',
associationTypeId: 1
}
})
}
];
for (const approach of approaches) {
try {
console.log(`🔄 Trying ${approach.name}...`);
await approach.action();
console.log(`✅ ${approach.name} succeeded`);
// Test verification igen
try {
const verification2 = await hubspotClient.crm.associations.v4.basicApi.getPage(
'tickets',
ticketId,
LISTINGS_OBJECT_TYPE_ID
);
const associatedListings2 = verification2?.results?.map(r => r.toObjectId) || [];
if (associatedListings2.includes(listingId)) {
console.log(`✅ CONFIRMED: ${approach.name} worked! Listing is associated.`);
break;
} else {
console.log(`❌ ${approach.name} didn't create visible association`);
}
} catch (verifyError) {
console.log(`❌ Verification failed after ${approach.name}`);
}
} catch (approachError) {
console.log(`❌ ${approach.name} failed:`, approachError.message);
}
}
}
}
const result = {
success: true,
associatedDeals: dealIds.length,
associatedListings: listingIds.length,
ticketId: ticketId
};
console.log(`✅ Final result:`, JSON.stringify(result));
return result;
} catch (err) {
console.error("🚫 Global Error:", err.message);
return {
success: false,
error: err.message
};
}
};