What is Custom Code in HubSpot?

HubSpot Operations Hub (Professional and Enterprise) includes a "Custom Code" workflow action. It lets you run JavaScript or Python code within workflows, opening up possibilities that standard workflow actions can't handle.

What You Can Do

  • Complex data transformations
  • API calls to external systems
  • Advanced deduplication logic
  • Data validation and enrichment
  • Custom calculations and formatting

Limitations

  • 20-second execution timeout
  • Limited to Node.js 16 or Python 3.9
  • Some npm packages not available
  • Requires Operations Hub Professional or Enterprise

Setting Up Custom Code Actions

Basic Structure

A custom code action receives input properties and must return output data:

// JavaScript example
exports.main = async (event, callback) => {
  // Access input properties
  const email = event.inputFields['email'];
  const firstName = event.inputFields['firstname'];

  // Your logic here
  const result = processData(email, firstName);

  // Return output
  callback({
    outputFields: {
      processedEmail: result.email,
      isValid: result.isValid
    }
  });
};

Using the HubSpot API

You can make API calls using the built-in hubspot client:

const hubspot = require('@hubspot/api-client');

exports.main = async (event, callback) => {
  const hubspotClient = new hubspot.Client({
    accessToken: process.env.HUBSPOT_ACCESS_TOKEN
  });

  // Search for contacts
  const searchRequest = {
    filterGroups: [{
      filters: [{
        propertyName: 'email',
        operator: 'EQ',
        value: event.inputFields['email']
      }]
    }]
  };

  const response = await hubspotClient.crm.contacts.searchApi.doSearch(searchRequest);

  callback({
    outputFields: {
      matchCount: response.total
    }
  });
};

Deduplication Strategies

The Duplicate Problem

Duplicates enter HubSpot from multiple sources: form submissions, imports, integrations, manual entry. They cause:

  • Inaccurate reporting
  • Multiple sales reps contacting the same person
  • Inconsistent data across records
  • Wasted marketing spend

Deduplication Approaches

1. Prevention (Best)

Stop duplicates before they're created:

exports.main = async (event, callback) => {
  const hubspot = require('@hubspot/api-client');
  const client = new hubspot.Client({ accessToken: process.env.HUBSPOT_ACCESS_TOKEN });

  const email = event.inputFields['email'].toLowerCase().trim();

  // Check for existing contact
  const search = await client.crm.contacts.searchApi.doSearch({
    filterGroups: [{
      filters: [{
        propertyName: 'email',
        operator: 'EQ',
        value: email
      }]
    }]
  });

  if (search.total > 0) {
    // Duplicate found - return existing contact ID
    callback({
      outputFields: {
        isDuplicate: true,
        existingContactId: search.results[0].id
      }
    });
  } else {
    callback({
      outputFields: {
        isDuplicate: false,
        existingContactId: null
      }
    });
  }
};

2. Merge on Detection

When a duplicate is detected, merge records automatically:

exports.main = async (event, callback) => {
  const hubspot = require('@hubspot/api-client');
  const client = new hubspot.Client({ accessToken: process.env.HUBSPOT_ACCESS_TOKEN });

  const primaryId = event.inputFields['primaryContactId'];
  const duplicateId = event.inputFields['duplicateContactId'];

  // Merge duplicate into primary (duplicate gets deleted)
  await client.crm.contacts.publicObjectApi.merge({
    primaryObjectId: primaryId,
    objectIdToMerge: duplicateId
  });

  callback({
    outputFields: {
      mergeComplete: true,
      survivingContactId: primaryId
    }
  });
};

3. Fuzzy Matching

For catching duplicates with slight variations:

// Normalize and compare emails
function normalizeEmail(email) {
  if (!email) return '';
  let normalized = email.toLowerCase().trim();

  // Handle Gmail dot-insensitivity
  if (normalized.includes('@gmail.com')) {
    const [local, domain] = normalized.split('@');
    normalized = local.replace(/\./g, '') + '@' + domain;
  }

  // Remove plus addressing
  normalized = normalized.replace(/\+[^@]+/, '');

  return normalized;
}

exports.main = async (event, callback) => {
  const email = event.inputFields['email'];
  const normalizedEmail = normalizeEmail(email);

  // Search using normalized email
  // ... rest of duplicate check logic
};

Working Code Examples

Phone Number Formatting

exports.main = async (event, callback) => {
  let phone = event.inputFields['phone'] || '';

  // Remove all non-digits
  phone = phone.replace(/\D/g, '');

  // Format as (XXX) XXX-XXXX for US numbers
  if (phone.length === 10) {
    phone = `(${phone.slice(0,3)}) ${phone.slice(3,6)}-${phone.slice(6)}`;
  } else if (phone.length === 11 && phone[0] === '1') {
    phone = `+1 (${phone.slice(1,4)}) ${phone.slice(4,7)}-${phone.slice(7)}`;
  }

  callback({
    outputFields: {
      formattedPhone: phone
    }
  });
};

Company Domain Extraction

exports.main = async (event, callback) => {
  const email = event.inputFields['email'] || '';

  // Extract domain from email
  const domain = email.split('@')[1] || '';

  // Exclude common personal email domains
  const personalDomains = ['gmail.com', 'yahoo.com', 'hotmail.com', 'outlook.com'];
  const isBusinessEmail = !personalDomains.includes(domain.toLowerCase());

  callback({
    outputFields: {
      emailDomain: domain,
      isBusinessEmail: isBusinessEmail
    }
  });
};

Lead Scoring Enhancement

exports.main = async (event, callback) => {
  let score = 0;

  // Company size scoring
  const employees = parseInt(event.inputFields['numberofemployees']) || 0;
  if (employees >= 1000) score += 20;
  else if (employees >= 200) score += 15;
  else if (employees >= 50) score += 10;

  // Industry scoring
  const targetIndustries = ['Technology', 'Financial Services', 'Healthcare'];
  if (targetIndustries.includes(event.inputFields['industry'])) {
    score += 15;
  }

  // Engagement scoring
  if (event.inputFields['recent_conversion_event_name']) score += 10;
  if (parseInt(event.inputFields['hs_email_open']) > 5) score += 5;

  callback({
    outputFields: {
      customLeadScore: score,
      scoreCategory: score >= 30 ? 'Hot' : score >= 15 ? 'Warm' : 'Cold'
    }
  });
};

Best Practices

Error Handling

Always wrap API calls in try/catch:

exports.main = async (event, callback) => {
  try {
    // Your logic here
    const result = await riskyOperation();
    callback({ outputFields: { success: true, data: result } });
  } catch (error) {
    console.error('Error:', error.message);
    callback({ outputFields: { success: false, error: error.message } });
  }
};

Testing

  • Test with sample data before going live
  • Use workflow enrollment triggers that limit scope during testing
  • Check logs for errors after each run

Performance

  • Keep execution under 20 seconds
  • Minimize API calls (batch when possible)
  • Use simple logic when standard actions would work

Frequently Asked Questions

Do I need Operations Hub for custom code?

Yes, custom code actions require Operations Hub Professional or Enterprise. Without it, you'll need to use external automation tools like Zapier or n8n.

Can I call external APIs?

Yes, you can make HTTP requests to external APIs using the axios library (pre-installed). Just be mindful of the 20-second timeout.

What happens if my code fails?

If your code throws an error, the workflow action fails and the contact won't proceed down that branch. Use try/catch and return error information in output fields for graceful handling.

Need help with HubSpot automation?

I build custom HubSpot workflows including advanced deduplication and data cleanup. Get in touch to discuss your project.

Book a Call