Table of Contents
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