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.
npx claudepluginhub jeremylongshore/claude-code-plugins-plus-skills --plugin salesforce-packThis skill is limited to using the following tools:
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.
Guides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Guides building MCP servers enabling LLMs to interact with external services via tools. Covers best practices, TypeScript/Node (MCP SDK), Python (FastMCP).
Generates original PNG/PDF visual art via design philosophy manifestos for posters, graphics, and static designs on user request.
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,jane@acme.com,EXT-ACME-001,EXT-CONTACT-001
John,Doe,john@globex.com,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.