'associations' API search and action on objects Deal, Company, Contact

I am in need your help with a question related to associations API. How can I perform the following action:

  • search for deals
    • with associated Companies
    • that have association label ‘lead-from-partner’
  • for those deals
    • set property ‘source’ to value ‘lead_from_partner’
    • set property ‘source level 2’ to value ‘installer’
  • for primary contact of those deals
    • set property ‘source’ to ‘lead_from_installer’
    • set property ‘source level 2’ to ‘installer’

I have tried both from a workflow with code custom action, and by creating a private app. The problem with the private app was giving the correct scope access level, what scope is required and how can it be enabled.
Custom code in workflow

exports.main = async (event, context) => {
 try {
 // ======== ENHANCED SECURITY VALIDATION ========
 console.log('Initializing custom code action');
 console.log('Available secrets:', Object.keys(context.secrets || {}));

 // Validate secret configuration with explicit checks
 const ACCESS_TOKEN = context.secrets?.PRIVATE_APP_ACCESS_TOKEN;
 if (!ACCESS_TOKEN || ACCESS_TOKEN.trim() === '') {
 const errorDetails = [
 '403 - Invalid secret configuration',
 `Secret present: ${!!ACCESS_TOKEN}`,
 `Secret empty: ${ACCESS_TOKEN === ''}`,
 'Required checks:',
 '1. Secret name EXACTLY "PRIVATE_APP_ACCESS_TOKEN" (case-sensitive)',
 '2. Value contains valid Private App token (starts with pat-)',
 '3. Secret added to THIS workflow action',
 '4. Private App has required scopes'
 ].join('\n');

 throw new Error(errorDetails);
 }

 // ======== API CLIENT CONFIGURATION ========
 const hubspotFetch = async (endpoint, options = {}) => {
 console.log('Making request to:', endpoint);
 const response = await fetch(`https://api.hubapi.com${endpoint}`, {
 ...options,
 headers: {
 'Content-Type': 'application/json',
 Authorization: `Bearer ${ACCESS_TOKEN}`,
 ...options.headers,
 },
 });

 if (!response.ok) {
 const errorBody = await response.text();
 console.error('API request failed:', {
 status: response.status,
 endpoint,
 errorBody
 });
 throw new Error(`API Error ${response.status}: ${errorBody.slice(0, 100)}`);
 }

 return response.json();
 };

 // ======== DEAL PROCESSING ========
 const dealId = event.object.objectId;
 console.log('Processing deal ID:', dealId);

 // Get associations using v4 API
 const associations = await hubspotFetch(
 `/crm/v4/objects/deal/${dealId}/associations/companies?associationType=lead_from_partner`
 );

 console.log('Association results:', associations.results?.length || 0);

 if (!associations.results?.length) {
 console.warn('No companies found with "lead_from_partner" association');
 return { outputFields: {} };
 }

 const companyId = associations.results[0].id;
 console.log('Associated company ID:', companyId);

 // Parallel processing for efficiency
 const [company, initialUpdate] = await Promise.all([
 hubspotFetch(`/crm/v3/objects/companies/${companyId}?properties=name`),
 hubspotFetch(`/crm/v3/objects/deals/${dealId}`, {
 method: 'PATCH',
 body: JSON.stringify({
 properties: {
 source: 'installer',
 source___level_2: 'Pending Company Name'
 }
 })
 })
 ]);

 console.log('Company details retrieved:', company.properties.name);

 // Final property update
 await hubspotFetch(`/crm/v3/objects/deals/${dealId}`, {
 method: 'PATCH',
 body: JSON.stringify({
 properties: {
 source___level_2: company.properties.name
 }
 })
 });

 console.log('Deal properties updated successfully');

 return {
 outputFields: {
 updated_source: 'installer',
 updated_source_level_2: company.properties.name
 }
 };

 } catch (error) {
 console.error('Fatal error:', {
 message: error.message,
 stack: error.stack,
 eventObjectId: event.object?.objectId,
 availableSecrets: Object.keys(context.secrets || {})
 });

 return {
 outputFields: {},
 errors: [{
 message: `Custom Code Error: ${error.message}`,
 category: 'CONFIGURATION_ERROR',
 errorType: 'AUTHENTICATION_FAILURE'
 }]
 };
 }
};
​

Any suggestions/input on how to perform the above?

Hi @Martén :waving_hand:

There might be a more straightforward method of achieving what you’re after.

It’s super well hidden, but in a deal-based workflow you can filter the enrollment triggers to look for companies with specific association labels.

From there you should be able to update all the relevant values.

same-object-association-workflow-enrollment.gif

Sorry if you’ve already tried this, but it’s worth mentioning just in case.

Edd

@EddBrisley , you’re amazing :star_struck: Thank you, it worked :slightly_smiling_face: Really well hidden, but it dramatically simplifies the approach and makes it more easy to reproduce for other use cases.
Thank you!

@Martén You’re welcome! :raising_hands: