smithery/jeremylongshore

apollo-migration-deep-dive

Comprehensive Apollo.io migration strategies. Use when migrating from other CRMs to Apollo, consolidating data sources, or executing large-scale data migrations. Trigger with phrases like "apollo migration", "migrate to apollo", "apollo data import", "crm to apollo", "apollo migration strategy". '

Installation

$ npx skills add smithery/jeremylongshore --skill apollo-migration-deep-dive

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from smithery/jeremylongshore · top by installs.

npx skills add smithery/jeremylongshore

Browse all from smithery/jeremylongshore

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Skill metadata

Parsed from SKILL.md frontmatter.

Version1.13.0
LicenseMIT
CompatibilityDesigned for Claude Code
Allowed toolsRead, Write, Edit, Bash(kubectl:*), Bash(curl:*)
Declared agents claude-code

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 9,522 B
  • docs SUMMARY.md 334 B

History

  1. First recorded snapshot · 0 installs

SKILL.md

Apollo Migration Deep Dive

Current State

!npm list 2>/dev/null | head -10

Overview

Migrate contact and company data into Apollo.io from other CRMs (Salesforce, HubSpot) or CSV sources. Uses Apollo's Contacts API for creating/updating contacts and Bulk Create Contacts endpoint for high-throughput imports (up to 100 contacts per call). Covers field mapping, assessment, batch processing, reconciliation, and rollback.

Prerequisites

  • Apollo master API key (Contacts API requires master key)
  • Node.js 18+
  • Source CRM export in CSV or JSON format

Instructions

Step 1: Define Field Mappings

// src/migration/field-map.ts
interface FieldMapping {
  source: string;
  target: string;        // Apollo Contacts API field
  transform?: (v: any) => any;
  required: boolean;
}

// Salesforce -> Apollo
const salesforceMap: FieldMapping[] = [
  { source: 'FirstName', target: 'first_name', required: true },
  { source: 'LastName', target: 'last_name', required: true },
  { source: 'Email', target: 'email', required: true },
  { source: 'Title', target: 'title', required: false },
  { source: 'Phone', target: 'phone_number', required: false },
  { source: 'Company', target: 'organization_name', required: false },
  { source: 'Website', target: 'website_url', required: false,
    transform: (url: string) => url?.startsWith('http') ? url : `https://${url}` },
  { source: 'LinkedIn', target: 'linkedin_url', required: false },
];

// HubSpot -> Apollo
const hubspotMap: FieldMapping[] = [
  { source: 'firstname', target: 'first_name', required: true },
  { source: 'lastname', target: 'last_name', required: true },
  { source: 'email', target: 'email', required: true },
  { source: 'jobtitle', target: 'title', required: false },
  { source: 'phone', target: 'phone_number', required: false },
  { source: 'company', target: 'organization_name', required: false },
  { source: 'website', target: 'website_url', required: false },
];

function mapRecord(record: Record<string, any>, mappings: FieldMapping[]): Record<string, any> {
  const mapped: Record<string, any> = {};
  for (const m of mappings) {
    let value = record[m.source];
    if (m.required && !value) throw new Error(`Missing: ${m.source}`);
    if (value && m.transform) value = m.transform(value);
    if (value) mapped[m.target] = value;
  }
  return mapped;
}

Step 2: Pre-Migration Assessment

// src/migration/assessment.ts
import fs from 'fs';
import { parse } from 'csv-parse/sync';

async function assess(csvPath: string, mappings: FieldMapping[]) {
  const records = parse(fs.readFileSync(csvPath, 'utf-8'), { columns: true, skip_empty_lines: true });

  const stats = { total: records.length, valid: 0, invalid: 0,
    missing: {} as Record<string, number>, duplicateEmails: 0, errors: [] as string[] };
  const emails = new Set<string>();

  for (const record of records) {
    try {
      mapRecord(record, mappings);
      const email = record.Email ?? record.email;
      if (emails.has(email)) stats.duplicateEmails++;
      else emails.add(email);
      stats.valid++;
    } catch (err: any) {
      stats.invalid++;
      const field = err.message.replace('Missing: ', '');
      stats.missing[field] = (stats.missing[field] ?? 0) + 1;
      if (stats.errors.length < 5) stats.errors.push(err.message);
    }
  }

  console.log(`Total: ${stats.total}, Valid: ${stats.valid}, Invalid: ${stats.invalid}, Dupes: ${stats.duplicateEmails}`);
  if (Object.keys(stats.missing).length) console.log('Missing fields:', stats.missing);
  return stats;
}

Step 3: Batch Migration Using Bulk Create

Apollo's Bulk Create Contacts endpoint creates up to 100 contacts per call with intelligent deduplication.

// src/migration/batch-worker.ts
import axios from 'axios';

const client = axios.create({
  baseURL: 'https://api.apollo.io/api/v1',
  headers: { 'Content-Type': 'application/json', 'x-api-key': process.env.APOLLO_API_KEY! },
});

interface MigrationResult {
  total: number; created: number; existing: number; failed: number;
  createdIds: string[];
  errors: Array<{ record: any; error: string }>;
}

async function migrateBatch(records: Record<string, any>[], batchSize: number = 100): Promise<MigrationResult> {
  const result: MigrationResult = { total: records.length, created: 0, existing: 0, failed: 0,
    createdIds: [], errors: [] };

  for (let i = 0; i < records.length; i += batchSize) {
    const batch = records.slice(i, i + batchSize);

    try {
      // Bulk create endpoint handles deduplication
      const { data } = await client.post('/contacts/bulk_create', {
        contacts: batch,
      });

      const newContacts = data.contacts ?? [];
      const existingContacts = data.existing_contacts ?? [];
      result.created += newContacts.length;
      result.existing += existingContacts.length;
      result.createdIds.push(...newContacts.map((c: any) => c.id));
    } catch (err: any) {
      // Fall back to individual creates
      for (const record of batch) {
        try {
          const { data } = await client.post('/contacts', record);
          result.created++;
          result.createdIds.push(data.contact.id);
        } catch (e: any) {
          result.failed++;
          result.errors.push({ record, error: e.response?.data?.message ?? e.message });
        }
      }
    }

    // Rate limit: 100 requests/min for contacts
    if (i + batchSize < records.length) {
      await new Promise((r) => setTimeout(r, 1000));
    }

    console.log(`Progress: ${Math.min(i + batchSize, records.length)}/${records.length}`);
  }

  return result;
}

Step 4: Post-Migration Reconciliation

async function reconcile(sourceRecords: Record<string, any>[]) {
  let matched = 0, missing = 0, mismatched = 0;

  for (const source of sourceRecords.slice(0, 100)) {  // Sample reconciliation
    const { data } = await client.post('/contacts/search', {
      q_keywords: source.email, per_page: 1,
    });

    const contact = data.contacts?.[0];
    if (!contact) { missing++; continue; }

    const nameMatch = contact.first_name === source.first_name && contact.last_name === source.last_name;
    if (nameMatch) matched++;
    else { mismatched++; console.warn(`Mismatch: ${source.email}`); }
  }

  console.log(`Reconciliation: ${matched} matched, ${missing} missing, ${mismatched} mismatched`);
  return { matched, missing, mismatched };
}

Step 5: Rollback

async function rollback(contactIds: string[]) {
  console.log(`Rolling back ${contactIds.length} contacts...`);
  let deleted = 0;

  for (let i = 0; i < contactIds.length; i += 50) {
    const batch = contactIds.slice(i, i + 50);
    for (const id of batch) {
      try { await client.delete(`/contacts/${id}`); deleted++; }
      catch (err: any) { console.error(`Failed: ${id}: ${err.message}`); }
    }
    await new Promise((r) => setTimeout(r, 500));
    console.log(`Rollback: ${Math.min(i + 50, contactIds.length)}/${contactIds.length}`);
  }

  console.log(`Rolled back ${deleted}/${contactIds.length} contacts`);
}

Output

  • Field mappings for Salesforce and HubSpot to Apollo Contacts API
  • Pre-migration assessment with validation, duplicates, and missing fields
  • Batch migration via POST /contacts/bulk_create (100 per call)
  • Post-migration reconciliation sampling
  • Rollback procedure deleting created contacts

Error Handling

Issue Resolution
403 on create Contacts API requires master key
Bulk create fails Falls back to individual POST /contacts calls
Duplicate contacts Apollo's bulkcreate handles dedup — returns existingcontacts
Field mapping error Review source field names, check for case sensitivity
Rate limited Increase delay between batches

Examples

Full Migration Pipeline

const assessment = await assess('./salesforce-export.csv', salesforceMap);
if (assessment.invalid > assessment.total * 0.1) {
  console.error('Too many invalid records (>10%). Clean data first.');
  process.exit(1);
}

const records = parseCsv('./salesforce-export.csv').map((r) => mapRecord(r, salesforceMap));
const result = await migrateBatch(records, 100);
console.log(`Created: ${result.created}, Existing: ${result.existing}, Failed: ${result.failed}`);

// Save contact IDs for potential rollback
fs.writeFileSync('migration-ids.json', JSON.stringify(result.createdIds));

await reconcile(records);

Resources

Next Steps

After migration, verify data with apollo-prod-checklist.