agentsclimarketplace

Salesforce migration deep dive

Skill jeremylongshore/claude-code-plugins-plus-skills/plugins/saas-packs/salesforce-pack/skills/salesforce-migration-deep-dive

425 plugins, 2,810 skills, 200 agents for Claude Code. Open-source marketplace at tonsofskills.com with the ccpi CLI package manager.

Install
npx -y skills add jeremylongshore/claude-code-plugins-plus-skills --skill salesforce-migration-deep-dive

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

What its author says it does

Copied from the file, not written here

'Execute Salesforce data migrations using Bulk API, Data Loader, and ETL patterns.

The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.

SKILL.md

8.1 KB, as published. Nobody here has run it

Salesforce Migration Deep Dive

Overview

Comprehensive guide for migrating data to/from Salesforce: ETL patterns using Bulk API 2.0, data mapping between CRM schemas, record relationship preservation, and validation.

Prerequisites

  • Source and target Salesforce orgs (or external CRM)
  • jsforce with Bulk API 2.0 access
  • Understanding of sObject relationships and External IDs
  • Staging sandbox for dry runs

Migration Types

TypeComplexityDurationTool
CSV import (< 50K records)LowHoursData Import Wizard / Bulk API
CRM-to-SalesforceMediumWeeksCustom ETL with jsforce
Org-to-org migrationMediumWeeksSFDX + Bulk API
Full re-platformHighMonthsCustom ETL + change management

Instructions

Step 1: Data Assessment

const conn = await getConnection();

// Count records per object
const objectCounts = await Promise.all(
  ['Account', 'Contact', 'Lead', 'Opportunity', 'Case'].map(async (obj) => {
    const result = await conn.query(`SELECT COUNT(Id) total FROM ${obj}`);
    return { object: obj, count: result.records[0].total };
  })
);

console.table(objectCounts);
// Account:      15,234
// Contact:      45,678
// Lead:         23,456
// Opportunity:  8,901
// Case:         67,890

// Check data storage limits
const limits = await conn.request('/services/data/v59.0/limits/');
console.log(`Data storage: ${limits.DataStorageMB.Max - limits.DataStorageMB.Remaining}/${limits.DataStorageMB.Max} MB`);

Step 2: Schema Mapping

// Map source fields to Salesforce sObject fields
interface FieldMapping {
  source: string;
  target: string;
  transform?: (value: any) => any;
  required: boolean;
}

const accountMappings: FieldMapping[] = [
  { source: 'company_name', target: 'Name', required: true },
  { source: 'industry_code', target: 'Industry', required: false,
    transform: (code) => INDUSTRY_MAP[code] || 'Other' },
  { source: 'annual_rev', target: 'AnnualRevenue', required: false,
    transform: (v) => typeof v === 'string' ? parseFloat(v.replace(/[$,]/g, '')) : v },
  { source: 'website_url', target: 'Website', required: false },
  { source: 'employee_count', target: 'NumberOfEmployees', required: false },
  { source: 'external_id', target: 'External_ID__c', required: true },
];

function transformRecord(
  source: Record<string, any>,
  mappings: FieldMapping[]
): Record<string, any> {
  const target: Record<string, any> = {};
  for (const mapping of mappings) {
    let value = source[mapping.source];
    if (value === undefined || value === null) {
      if (mapping.required) throw new Error(`Missing required field: ${mapping.source}`);
      continue;
    }
    if (mapping.transform) value = mapping.transform(value);
    target[mapping.target] = value;
  }
  return target;
}

Step 3: Migration Order (Respecting Relationships)

Migration order matters! Parent objects must be loaded before children.

1. Account          (no dependencies)
2. Contact          (depends on Account via AccountId)
3. Opportunity      (depends on Account via AccountId)
4. OpportunityContactRole (depends on Opportunity + Contact)
5. Case             (depends on Account + Contact)
6. Task / Event     (depends on Contact via WhoId, Account via WhatId)

Use External IDs to resolve relationships without knowing Salesforce IDs:
- Create External_ID__c on Account, Contact, Opportunity
- Use external ID references in child records

Step 4: Bulk Migration with External ID Relationships

import { getConnection } from './salesforce/connection';
import fs from 'fs';

const conn = await getConnection();

// Step 4a: Load Accounts first
const accountCsv = `Name,Industry,External_ID__c
Acme Corp,Technology,EXT-ACME-001
Globex Inc,Manufacturing,EXT-GLOBEX-002
Initech LLC,Consulting,EXT-INITECH-003`;

const accountResults = await conn.bulk2.loadAndWaitForResults({
  object: 'Account',
  operation: 'upsert',
  externalIdFieldName: 'External_ID__c',
  input: accountCsv,
});
console.log(`Accounts: ${accountResults.successfulResults.length} success, ${accountResults.failedResults.length} failed`);

// Step 4b: Load Contacts with Account relationship via External ID
const contactCsv = `FirstName,LastName,Email,Account.External_ID__c,External_ID__c
Jane,Smith,[email protected],EXT-ACME-001,EXT-CONTACT-001
John,Doe,[email protected],EXT-GLOBEX-002,EXT-CONTACT-002`;

const contactResults = await conn.bulk2.loadAndWaitForResults({
  object: 'Contact',
  operation: 'upsert',
  externalIdFieldName: 'External_ID__c',
  input: contactCsv,
});
// Account.External_ID__c resolves to the correct AccountId automatically!

Step 5: Validation

async function validateMigration(
  sourceCount: number,
  objectType: string
): Promise<{ passed: boolean; details: string }> {
  const conn = await getConnection();

  // Count migrated records
  const result = await conn.query(
    `SELECT COUNT(Id) total FROM ${objectType} WHERE External_ID__c != null`
  );
  const targetCount = result.records[0].total;

  // Check for orphaned relationships
  let orphans = 0;
  if (objectType === 'Contact') {
    const orphanResult = await conn.query(
      `SELECT COUNT(Id) total FROM Contact WHERE AccountId = null AND External_ID__c != null`
    );
    orphans = orphanResult.records[0].total;
  }

  const passed = targetCount === sourceCount && orphans === 0;
  return {
    passed,
    details: `Source: ${sourceCount}, Target: ${targetCount}, Orphans: ${orphans}`,
  };
}

Step 6: Rollback Plan

// Delete migrated records using External ID marker
async function rollbackMigration(objectType: string): Promise<void> {
  const conn = await getConnection();

  // Query all migrated records (identified by External_ID__c)
  const records = await conn.query(
    `SELECT Id FROM ${objectType} WHERE External_ID__c != null`
  );

  // Delete in reverse order (children first)
  const ids = records.records.map((r: any) => r.Id);
  for (let i = 0; i < ids.length; i += 200) {
    const batch = ids.slice(i, i + 200);
    await conn.sobject(objectType).destroy(batch);
  }

  console.log(`Rolled back ${ids.length} ${objectType} records`);
}

Output

  • Data assessment with record counts and storage usage
  • Field mapping layer transforming source to Salesforce schema
  • Bulk API migration respecting parent-child relationships
  • External ID-based relationship resolution (no hardcoded IDs)
  • Validation and rollback procedures

Error Handling

ErrorCauseSolution
DUPLICATE_VALUE on External_ID__cRe-running migrationUse upsert instead of insert
INVALID_CROSS_REFERENCE_KEYParent record not foundVerify parent loaded first, check External ID values
STORAGE_LIMIT_EXCEEDEDOrg storage fullDelete test data or upgrade storage
Bulk job timeoutVery large datasetSplit into smaller jobs (< 100M records)
Field mapping errorsSource schema mismatchValidate transform functions with sample data first

Resources

Next Steps

For advanced troubleshooting, see salesforce-advanced-troubleshooting.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.