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.md
source 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

PitfallDetectionPrevention
N+1 SOQL queriesHigh API call count, loop with
.query()
Use relationship SOQL
API limit exhaustion
REQUEST_LIMIT_EXCEEDED
Monitor
/limits/
, use Bulk API
SOQL injectionString concatenation in
.query()
Use
escapeSoql()
SELECT FIELDS(ALL)Slow queries, large payloadsSelect only needed fields
Hardcoded IDsDifferent behavior in sandbox vs prodQuery by Name, use External IDs
Partial failure ignoredSilent data lossCheck
.success
on every result
Wrong login URLConnected to wrong orgUse
SF_LOGIN_URL
env var
No External IDDuplicate records on re-syncAdd External_ID__c, use upsert
No retry for LOCK_ROWRandom crashes under loadRetry with exponential backoff
Polling over CDCWasted API callsUse Change Data Capture

Resources