Claude-code-plugins-plus-skills salesforce-known-pitfalls
install
source · Clone the upstream repo
git clone https://github.com/jeremylongshore/claude-code-plugins-plus-skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/jeremylongshore/claude-code-plugins-plus-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/saas-packs/salesforce-pack/skills/salesforce-known-pitfalls" ~/.claude/skills/jeremylongshore-claude-code-plugins-plus-skills-salesforce-known-pitfalls && rm -rf "$T"
manifest:
plugins/saas-packs/salesforce-pack/skills/salesforce-known-pitfalls/SKILL.mdsource content
Salesforce Known Pitfalls
Overview
The 10 most common and costly mistakes when integrating with Salesforce, with real error messages and correct patterns.
Pitfall #1: SOQL N+1 Query Pattern (Most Common)
Anti-Pattern
// Query accounts, then query contacts for each = N+1 API calls const accounts = await conn.query('SELECT Id, Name FROM Account LIMIT 100'); for (const account of accounts.records) { // 100 extra API calls! (plus 100 extra SOQL queries in Apex) const contacts = await conn.query( `SELECT Id, Name, Email FROM Contact WHERE AccountId = '${account.Id}'` ); } // Total: 101 API calls for what should be 1
Correct Pattern
// Single relationship query — 1 API call const accounts = await conn.query(` SELECT Id, Name, (SELECT Id, FirstName, LastName, Email FROM Contacts) FROM Account LIMIT 100 `); // accounts.records[0].Contacts.records → child contacts
Pitfall #2: Ignoring API Limits (Org-Wide Shared Pool)
Anti-Pattern
// This integration uses 80,000 API calls/day // Sales team uses 60,000/day // Total: 140,000 > 150,000 limit → everyone gets blocked
Correct Pattern
// 1. Check limits before batch operations const limits = await conn.request('/services/data/v59.0/limits/'); if (limits.DailyApiRequests.Remaining < estimatedCalls) { throw new Error('Insufficient API calls remaining'); } // 2. Use sObject Collections (1 call = 200 records) await conn.sobject('Contact').create(contacts); // batch of up to 200 // 3. Use Bulk API for 10K+ (separate limit pool) await conn.bulk2.loadAndWaitForResults({ object: 'Contact', operation: 'insert', input: csv });
Pitfall #3: SOQL Injection
Anti-Pattern
// User input directly in SOQL — injectable const name = req.query.name; // Could be: "'; SELECT Id FROM User; --" await conn.query(`SELECT Id FROM Account WHERE Name = '${name}'`);
Correct Pattern
function escapeSoql(value: string): string { return value.replace(/\\/g, '\\\\').replace(/'/g, "\\'"); } await conn.query(`SELECT Id FROM Account WHERE Name = '${escapeSoql(name)}'`);
Pitfall #4: SELECT FIELDS(ALL) (Performance Killer)
Anti-Pattern
// Fetches ALL fields — 200+ columns on Account, massive payload const result = await conn.query('SELECT FIELDS(ALL) FROM Account LIMIT 100');
Correct Pattern
// Select only what you need — 5-10x faster, much less data transfer const result = await conn.query('SELECT Id, Name, Industry, AnnualRevenue FROM Account LIMIT 100');
Pitfall #5: Hardcoded Salesforce Record IDs
Anti-Pattern
// IDs are different across sandbox and production! const adminProfileId = '00e5f000001abc'; // Works in sandbox... const queueId = '00G5f000002def'; // ...breaks in production await conn.sobject('Case').create({ OwnerId: queueId, ProfileId: adminProfileId });
Correct Pattern
// Look up by name, not by ID const queue = await conn.query("SELECT Id FROM Group WHERE Name = 'Support Queue' AND Type = 'Queue'"); const queueId = queue.records[0].Id; const profile = await conn.query("SELECT Id FROM Profile WHERE Name = 'System Administrator'"); const profileId = profile.records[0].Id;
Pitfall #6: Not Handling Partial Success in Bulk Operations
Anti-Pattern
const results = await conn.sobject('Contact').create(contacts); console.log('Done!'); // Ignoring that some records may have failed
Correct Pattern
const results = await conn.sobject('Contact').create(contacts); const failures = results.filter(r => !r.success); if (failures.length > 0) { console.error(`${failures.length}/${results.length} records failed:`); for (const failure of failures) { console.error(` ${failure.errors.map(e => `${e.statusCode}: ${e.message}`).join('; ')}`); } }
Pitfall #7: Using test.salesforce.com for Production
Anti-Pattern
// Sandbox login URL used for production — silently connects to sandbox const conn = new jsforce.Connection({ loginUrl: 'https://test.salesforce.com', // WRONG for production });
Correct Pattern
const conn = new jsforce.Connection({ loginUrl: process.env.SF_LOGIN_URL, // 'https://login.salesforce.com' for prod // OR: 'https://test.salesforce.com' for sandboxes }); // Always use environment variables — never hardcode login URLs
Pitfall #8: Not Using External IDs for Upsert
Anti-Pattern
// Without External ID: create duplicates on every sync run await conn.sobject('Account').create({ Name: 'Acme Corp', Industry: 'Tech' }); // Run again → duplicate Account created!
Correct Pattern
// With External ID: upsert is idempotent — safe to retry await conn.sobject('Account').upsert({ External_ID__c: 'CRM-ACME-001', // Custom External ID field Name: 'Acme Corp', Industry: 'Tech', }, 'External_ID__c'); // Run again → updates existing record, no duplicate
Pitfall #9: Missing Error Handling for UNABLE_TO_LOCK_ROW
Anti-Pattern
// Record locking is COMMON in Salesforce — this crashes on contention await conn.sobject('Account').update({ Id: accountId, Name: 'New Name' }); // Error: UNABLE_TO_LOCK_ROW → unhandled, crashes the process
Correct Pattern
async function updateWithRetry(objectType: string, data: any, maxRetries = 3) { for (let attempt = 1; attempt <= maxRetries; attempt++) { try { return await conn.sobject(objectType).update(data); } catch (error: any) { if (error.errorCode === 'UNABLE_TO_LOCK_ROW' && attempt < maxRetries) { await new Promise(r => setTimeout(r, 1000 * attempt)); // Backoff continue; } throw error; } } }
Pitfall #10: Polling When CDC Is Available
Anti-Pattern
// Poll every 5 minutes — wastes 288 API calls/day even when nothing changed cron.schedule('*/5 * * * *', async () => { const changes = await conn.query(` SELECT Id, Name FROM Account WHERE LastModifiedDate >= ${fiveMinAgo} `); // Usually returns 0 records — wasted API call });
Correct Pattern
// CDC — only fires when data actually changes, zero wasted API calls conn.streaming.topic('/data/AccountChangeEvent').subscribe((event) => { // Only called when an Account is actually created/updated/deleted handleAccountChange(event); });
Quick Reference Card
| Pitfall | Detection | Prevention |
|---|---|---|
| N+1 SOQL queries | High API call count, loop with | Use relationship SOQL |
| API limit exhaustion | | Monitor , use Bulk API |
| SOQL injection | String concatenation in | Use |
| SELECT FIELDS(ALL) | Slow queries, large payloads | Select only needed fields |
| Hardcoded IDs | Different behavior in sandbox vs prod | Query by Name, use External IDs |
| Partial failure ignored | Silent data loss | Check on every result |
| Wrong login URL | Connected to wrong org | Use env var |
| No External ID | Duplicate records on re-sync | Add External_ID__c, use upsert |
| No retry for LOCK_ROW | Random crashes under load | Retry with exponential backoff |
| Polling over CDC | Wasted API calls | Use Change Data Capture |