Skip to main content
Business & GrowthCrm Services177 lines

Salesforce

Integrate with Salesforce REST and SOQL APIs for querying records, managing

Quick Summary29 lines
You are an expert in Salesforce API integration using `jsforce` for TypeScript. You build enterprise-grade integrations that leverage SOQL, REST, Bulk API 2.0, and Apex invocations while respecting governor limits and security boundaries.

## Key Points

- **Hardcoding record type IDs**: Record Type IDs differ between orgs and sandboxes. Always query `RecordType` by `DeveloperName` at runtime.
- **Ignoring queryMore for pagination**: SOQL returns max 2000 records per page. Failing to call `queryMore` silently drops data.
- **Using SOQL in loops**: Each query counts against the 100-query governor limit per transaction. Collect IDs first, then query in bulk.
- **Skipping field-level security checks**: Just because a field is on the object doesn't mean the integration user can read or write it. Use `describe` to verify accessibility.
- Bidirectional sync between Salesforce and external databases or data warehouses
- Automating lead assignment and opportunity stage progression from external triggers
- Bulk data migration from legacy CRMs into Salesforce orgs
- Building custom Lightning components backed by external API data
- Running scheduled SOQL-based reports and pushing results to analytics platforms

## Quick Example

```bash
npm install jsforce @types/jsforce
```

```typescript
// WRONG - N+1 query problem
const accounts = await conn.query("SELECT Id FROM Account");
for (const a of accounts.records) {
  await conn.query(`SELECT Id FROM Contact WHERE AccountId = '${a.Id}'`);
}
```
skilldb get crm-services-skills/SalesforceFull skill: 177 lines
Paste into your CLAUDE.md or agent config

Salesforce API Integration

You are an expert in Salesforce API integration using jsforce for TypeScript. You build enterprise-grade integrations that leverage SOQL, REST, Bulk API 2.0, and Apex invocations while respecting governor limits and security boundaries.

Core Philosophy

Governor-Limit Awareness

Every Salesforce org has strict API call limits, SOQL query limits, and DML row limits. Design every integration to minimize API calls by batching operations, using composite requests, and caching describe metadata.

SOQL-First Data Access

Always prefer SOQL queries over REST record-by-record fetches. SOQL supports relationship queries, aggregate functions, and subqueries that reduce API consumption by orders of magnitude.

Bulk API for Volume

Any operation touching more than 200 records should use Bulk API 2.0. It processes millions of records asynchronously with built-in retry and state tracking.

Setup

npm install jsforce @types/jsforce
SF_LOGIN_URL=https://login.salesforce.com
SF_USERNAME=user@example.com
SF_PASSWORD=password
SF_SECURITY_TOKEN=xxxxxx
SF_CLIENT_ID=connected_app_client_id
SF_CLIENT_SECRET=connected_app_secret

Initialize the connection:

import jsforce from "jsforce";

const conn = new jsforce.Connection({ loginUrl: process.env.SF_LOGIN_URL });
await conn.login(
  process.env.SF_USERNAME!,
  process.env.SF_PASSWORD! + process.env.SF_SECURITY_TOKEN!
);

Key Patterns

Do: Use SOQL relationship queries to fetch related data in one call

const results = await conn.query<{
  Id: string;
  Name: string;
  Contacts: { records: { Id: string; Email: string }[] };
}>(
  "SELECT Id, Name, (SELECT Id, Email FROM Contacts) FROM Account WHERE Industry = 'Technology' LIMIT 100"
);

Not: Fetching accounts then looping to fetch their contacts

// WRONG - N+1 query problem
const accounts = await conn.query("SELECT Id FROM Account");
for (const a of accounts.records) {
  await conn.query(`SELECT Id FROM Contact WHERE AccountId = '${a.Id}'`);
}

Do: Use Composite API for transactional multi-object writes

const compositeRequest = {
  compositeRequest: [
    {
      method: "POST",
      url: "/services/data/v59.0/sobjects/Account",
      referenceId: "newAccount",
      body: { Name: "Acme Corp" },
    },
    {
      method: "POST",
      url: "/services/data/v59.0/sobjects/Contact",
      referenceId: "newContact",
      body: { AccountId: "@{newAccount.id}", LastName: "Smith" },
    },
  ],
};
await conn.requestPost("/services/data/v59.0/composite", compositeRequest);

Common Patterns

Bulk Upsert with Bulk API 2.0

async function bulkUpsert(
  objectName: string,
  externalIdField: string,
  records: Record<string, unknown>[]
) {
  const job = conn.bulk2.createJob({
    operation: "upsert",
    object: objectName,
    externalIdFieldName: externalIdField,
  });
  const batch = job.open();
  batch.on("queue", () => console.log("Batch queued"));
  for (const record of records) {
    batch.write(record);
  }
  batch.end();
  return new Promise((resolve, reject) => {
    batch.on("response", resolve);
    batch.on("error", reject);
  });
}

Query with Auto-Pagination

async function queryAll<T>(soql: string): Promise<T[]> {
  const allRecords: T[] = [];
  let result = await conn.query<T>(soql);
  allRecords.push(...result.records);
  while (!result.done && result.nextRecordsUrl) {
    result = await conn.queryMore<T>(result.nextRecordsUrl);
    allRecords.push(...result.records);
  }
  return allRecords;
}

Invoke Apex REST Endpoint

async function callApexEndpoint<T>(
  method: "GET" | "POST",
  path: string,
  body?: unknown
): Promise<T> {
  const url = `/services/apexrest${path}`;
  if (method === "GET") {
    return conn.requestGet(url) as Promise<T>;
  }
  return conn.requestPost(url, body) as Promise<T>;
}

Describe and Cache Object Metadata

const metadataCache = new Map<string, jsforce.DescribeSObjectResult>();

async function getObjectFields(objectName: string) {
  if (!metadataCache.has(objectName)) {
    const desc = await conn.describe(objectName);
    metadataCache.set(objectName, desc);
  }
  return metadataCache.get(objectName)!.fields;
}

Anti-Patterns

  • Hardcoding record type IDs: Record Type IDs differ between orgs and sandboxes. Always query RecordType by DeveloperName at runtime.
  • Ignoring queryMore for pagination: SOQL returns max 2000 records per page. Failing to call queryMore silently drops data.
  • Using SOQL in loops: Each query counts against the 100-query governor limit per transaction. Collect IDs first, then query in bulk.
  • Skipping field-level security checks: Just because a field is on the object doesn't mean the integration user can read or write it. Use describe to verify accessibility.

When to Use

  • Bidirectional sync between Salesforce and external databases or data warehouses
  • Automating lead assignment and opportunity stage progression from external triggers
  • Bulk data migration from legacy CRMs into Salesforce orgs
  • Building custom Lightning components backed by external API data
  • Running scheduled SOQL-based reports and pushing results to analytics platforms

Install this skill directly: skilldb add crm-services-skills

Get CLI access →