From salesforce-pack
Guides Salesforce data migrations using Bulk API 2.0, jsforce ETL, Data Loader for org-to-org transfers, CRM imports, and validation with TypeScript examples.
How this skill is triggered — by the user, by Claude, or both
Slash command
/salesforce-pack:salesforce-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 data to/from Salesforce: ETL patterns using Bulk API 2.0, data mapping between CRM schemas, record relationship preservation, and validation.
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.
| Type | Complexity | Duration | Tool |
|---|---|---|---|
| CSV import (< 50K records) | Low | Hours | Data Import Wizard / Bulk API |
| CRM-to-Salesforce | Medium | Weeks | Custom ETL with jsforce |
| Org-to-org migration | Medium | Weeks | SFDX + Bulk API |
| Full re-platform | High | Months | Custom ETL + change management |
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`);
// 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;
}
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
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!
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}`,
};
}
// 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`);
}
| Error | Cause | Solution |
|---|---|---|
DUPLICATE_VALUE on External_ID__c | Re-running migration | Use upsert instead of insert |
INVALID_CROSS_REFERENCE_KEY | Parent record not found | Verify parent loaded first, check External ID values |
STORAGE_LIMIT_EXCEEDED | Org storage full | Delete test data or upgrade storage |
| Bulk job timeout | Very large dataset | Split into smaller jobs (< 100M records) |
| Field mapping errors | Source schema mismatch | Validate transform functions with sample data first |
For advanced troubleshooting, see salesforce-advanced-troubleshooting.
npx claudepluginhub jeremylongshore/claude-code-plugins-plus-skills --plugin salesforce-packGenerates bulk Salesforce data operation scripts (Data Loader CSV templates and Anonymous Apex) for owner reassignment, deduplication, mass field update, stale record closing, contact deactivation, and lead conversion.
Performs Salesforce data operations including CRUD, bulk import/export, test data generation, and cleanup using sf CLI and anonymous Apex.
Executes Salesforce Bulk API 2.0 for high-volume insert/update/upsert/delete/query on large datasets via CSV/streams and Composite API for multi-object transactions.