From hubspot-pack
Migrates CRM data to HubSpot using batch API imports, TypeScript field mapping, custom properties, and validation. For Salesforce/Pipedrive/spreadsheet sources or bulk imports.
How this skill is triggered — by the user, by Claude, or both
Slash command
/hubspot-pack:hubspot-migration-deep-diveThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
Comprehensive guide for migrating CRM data into HubSpot, including data mapping, batch imports via API, validation, and rollback procedures.
Comprehensive guide for migrating CRM data into HubSpot, including data mapping, batch imports via API, validation, and rollback procedures.
// Map source CRM fields to HubSpot properties
interface FieldMapping {
sourceField: string;
hubspotProperty: string;
transform?: (value: string) => string;
required: boolean;
}
const contactFieldMap: FieldMapping[] = [
{ sourceField: 'Email', hubspotProperty: 'email', required: true },
{ sourceField: 'First Name', hubspotProperty: 'firstname', required: false },
{ sourceField: 'Last Name', hubspotProperty: 'lastname', required: false },
{ sourceField: 'Phone', hubspotProperty: 'phone', required: false },
{ sourceField: 'Company', hubspotProperty: 'company', required: false },
{
sourceField: 'Lead Status',
hubspotProperty: 'lifecyclestage',
transform: (val) => {
// Map source values to HubSpot lifecycle stages
const map: Record<string, string> = {
'New': 'lead',
'Qualified': 'marketingqualifiedlead',
'Won': 'customer',
};
return map[val] || 'lead';
},
required: false,
},
];
function mapRecord(
source: Record<string, string>,
fieldMap: FieldMapping[]
): Record<string, string> {
const mapped: Record<string, string> = {};
for (const field of fieldMap) {
const value = source[field.sourceField];
if (value !== undefined && value !== '') {
mapped[field.hubspotProperty] = field.transform ? field.transform(value) : value;
} else if (field.required) {
throw new Error(`Missing required field: ${field.sourceField}`);
}
}
return mapped;
}
import * as hubspot from '@hubspot/api-client';
const client = new hubspot.Client({
accessToken: process.env.HUBSPOT_ACCESS_TOKEN!,
});
// Create custom properties that don't exist in HubSpot
async function ensureCustomProperties(objectType: string) {
const customProps = [
{
name: 'source_crm_id',
label: 'Source CRM ID',
type: 'string',
fieldType: 'text',
groupName: 'contactinformation',
description: 'Original record ID from source CRM',
},
{
name: 'migration_date',
label: 'Migration Date',
type: 'date',
fieldType: 'date',
groupName: 'contactinformation',
description: 'Date record was migrated to HubSpot',
},
];
for (const prop of customProps) {
try {
// POST /crm/v3/properties/{objectType}
await client.crm.properties.coreApi.create(objectType, prop);
console.log(`Created property: ${prop.name}`);
} catch (error: any) {
if (error?.body?.category === 'DUPLICATE_PROPERTY') {
console.log(`Property already exists: ${prop.name}`);
} else {
throw error;
}
}
}
}
interface MigrationResult {
total: number;
created: number;
updated: number;
errors: Array<{ record: any; error: string }>;
durationMs: number;
}
async function migrateContacts(
records: Record<string, string>[],
fieldMap: FieldMapping[]
): Promise<MigrationResult> {
const start = Date.now();
const result: MigrationResult = {
total: records.length,
created: 0,
updated: 0,
errors: [],
durationMs: 0,
};
// Process in batches of 100 (HubSpot batch limit)
const batchSize = 100;
for (let i = 0; i < records.length; i += batchSize) {
const batch = records.slice(i, i + batchSize);
const mapped = [];
for (const record of batch) {
try {
const properties = mapRecord(record, fieldMap);
properties.migration_date = new Date().toISOString().split('T')[0];
properties.source_crm_id = record.Id || record.id || '';
mapped.push({ properties });
} catch (error: any) {
result.errors.push({ record, error: error.message });
}
}
if (mapped.length === 0) continue;
try {
// Use batch upsert to handle existing contacts
// POST /crm/v3/objects/contacts/batch/upsert
const response = await client.apiRequest({
method: 'POST',
path: '/crm/v3/objects/contacts/batch/upsert',
body: {
inputs: mapped.map(m => ({
properties: m.properties,
idProperty: 'email',
id: m.properties.email,
})),
},
});
const data = await response.json();
result.created += data.results?.length || 0;
} catch (error: any) {
// On batch failure, try individual records
for (const item of mapped) {
try {
await client.crm.contacts.basicApi.create({
properties: item.properties,
associations: [],
});
result.created++;
} catch (err: any) {
if (err?.body?.category === 'CONFLICT') {
// Contact exists, update instead
const existing = await client.crm.contacts.searchApi.doSearch({
filterGroups: [{
filters: [{ propertyName: 'email', operator: 'EQ', value: item.properties.email }],
}],
properties: ['email'], limit: 1, after: 0, sorts: [],
});
if (existing.results.length > 0) {
await client.crm.contacts.basicApi.update(existing.results[0].id, {
properties: item.properties,
});
result.updated++;
}
} else {
result.errors.push({ record: item.properties, error: err.message });
}
}
}
}
// Progress logging
const progress = Math.min(i + batchSize, records.length);
console.log(`Progress: ${progress}/${records.length} ` +
`(${result.created} created, ${result.updated} updated, ${result.errors.length} errors)`);
// Rate limit: max 10 requests/second
await new Promise(r => setTimeout(r, 200));
}
result.durationMs = Date.now() - start;
return result;
}
async function migrateDeals(
deals: any[],
contactEmailToId: Map<string, string>
): Promise<MigrationResult> {
const result: MigrationResult = {
total: deals.length, created: 0, updated: 0, errors: [], durationMs: 0,
};
const start = Date.now();
// Get pipeline stages
const pipelines = await client.crm.pipelines.pipelinesApi.getAll('deals');
const defaultPipeline = pipelines.results[0];
for (const deal of deals) {
try {
const associations = [];
// Associate with contact if we have a mapping
if (deal.contactEmail && contactEmailToId.has(deal.contactEmail)) {
associations.push({
to: { id: contactEmailToId.get(deal.contactEmail)! },
types: [{ associationCategory: 'HUBSPOT_DEFINED', associationTypeId: 3 }],
});
}
await client.crm.deals.basicApi.create({
properties: {
dealname: deal.name,
amount: String(deal.amount || 0),
pipeline: defaultPipeline.id,
dealstage: defaultPipeline.stages[0].id,
closedate: deal.closeDate || new Date().toISOString(),
source_crm_id: deal.id || '',
},
associations,
});
result.created++;
} catch (error: any) {
result.errors.push({ record: deal, error: error.message });
}
}
result.durationMs = Date.now() - start;
return result;
}
async function validateMigration(
expectedCounts: { contacts: number; deals: number }
): Promise<{ valid: boolean; checks: any[] }> {
const checks = [];
// Count contacts
const contacts = await client.crm.contacts.searchApi.doSearch({
filterGroups: [{
filters: [{ propertyName: 'migration_date', operator: 'HAS_PROPERTY', value: '' }],
}],
properties: ['email'], limit: 1, after: 0, sorts: [],
});
checks.push({
check: 'Contact count',
expected: expectedCounts.contacts,
actual: contacts.total,
passed: contacts.total >= expectedCounts.contacts * 0.95, // 95% threshold
});
// Check for required fields
const missingEmail = await client.crm.contacts.searchApi.doSearch({
filterGroups: [{
filters: [
{ propertyName: 'migration_date', operator: 'HAS_PROPERTY', value: '' },
{ propertyName: 'email', operator: 'NOT_HAS_PROPERTY', value: '' },
],
}],
properties: ['firstname'], limit: 1, after: 0, sorts: [],
});
checks.push({
check: 'Contacts with email',
missing: missingEmail.total,
passed: missingEmail.total === 0,
});
return {
valid: checks.every(c => c.passed),
checks,
};
}
| Issue | Cause | Solution |
|---|---|---|
PROPERTY_DOESNT_EXIST | Custom property not created | Run ensureCustomProperties first |
409 Conflict | Contact email already exists | Use batch upsert instead of batch create |
| Batch partial failure | Some records invalid | Fall back to individual creates |
| Association failure | Contact not yet created | Import contacts before deals |
For advanced troubleshooting, see hubspot-advanced-troubleshooting.
npx claudepluginhub luxdevnet/claude-plus-lux --plugin hubspot-pack4plugins reuse this skill
First indexed Jul 10, 2026
Bulk-migrate CRM data into HubSpot from Salesforce, Pipedrive, or Copper — or export off HubSpot — with field mapping, ID continuity, association re-linking, dedup safety, rate-limit budgeting, and rollback mitigation across 100K+ record datasets. Use when migrating any source CRM to HubSpot, recovering from a failed import with duplicate or unlinked records, or exporting out of HubSpot before switching platforms. Trigger with "hubspot bulk migration", "salesforce to hubspot", "pipedrive to hubspot", "copper to hubspot", "migrate off hubspot", "hubspot import dedup", "hubspot association relink", "hubspot field mapping", "hubspot id continuity".
Migrates contact and company data from Salesforce, HubSpot, or CSV sources into Apollo.io. Includes field mapping, batch processing, reconciliation, and rollback strategies.
Syncs contacts, deals, and campaigns bi-directionally with Salesforce, HubSpot, Zoho, or Pipedrive. Includes deduplication, field mapping, audit trails, and rollback.