agentleFS
ShanksgShanksg/clawskills/skills/salesforce/skill.md

salesforce

Skill2 starsChanged 3 months agoMITView on GitHub

Last validated: 2026-10-14 | API: Salesforce REST API + Bulk API v2 | Version: v67.0 (Summer '26) Assumed product: Sales Cloud (CRM). Adjust object availability for other clouds. Version note: v64.0 = Summer '25, v65.0 = Winter '26, v66.0 = Spring '26, v67.0 = Summer '26, v68.0 = Winter '27 (rolling out Oct 2026). All examples in this doc pin /services/data/v67.0/. Older versions stay supported under Salesforce's ~3-year API support window. ⚠️ Checked 2026-10-14 — Winter '27 (v68.0) reviewed, examples…

  • Sends data out

6 other projects use a version of this.

Adopted by

Most recently taken up by

The file

Show its rules and text

We split this skill into the rules it gives an agent

23 rules. 0 are common, 12 are distinctive, and 11 are very specific to this project.

  • 0

    Common

    Most of the 6 edited versions we read keep these.

  • 12

    Distinctive

    This author's own choices. Worth a look.

  • 11

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Keep Opportunity pipeline data current by updating stages, amounts, and close dates from external triggers.

    line 22
  2. Very specific:

    All records have an Id field: 15-character case-sensitive or 18-character case-insensitive Salesforce ID.

    line 68
  3. Distinctive:

    Always use 18-character IDs in URLs and external storage to avoid case-sensitivity bugs.

    line 69
  4. Distinctive:

    If found, return existing ID (and optionally update fields).

    line 150
  5. Distinctive:

    If not found, create a new Lead.

    line 151
  6. Very specific:

    If validation rules fire (e.g., required custom field missing), errors[] is non-empty.

    line 179
  7. Very specific:

    If using Duplicate Management, a 409 with DUPLICATE_VALUE error is returned.

    line 181
  8. Very specific:

    Use SOQL OFFSET for small datasets, queryLocator (above) for large ones.

    line 219
  9. Distinctive:

    Goal: Create a Contact and link it to an Account in a single HTTP round-trip.

    line 311
  10. Very specific:

    Use @{referenceId.field} to pass results between steps.

    line 347
  11. Distinctive:

    Keep one object type per job.

    line 403
  12. Distinctive:

    Do not advance the upstream source cursor until every row is either successful or quarantined.

    line 411
  13. Distinctive:

    Goal: Create or update a HubSpot Contact whenever a Salesforce Lead is created or updated, deduplicating by email.

    line 443
  14. Distinctive:

    Write a sync marker back to Salesforce only after the HubSpot write succeeds.

    line 450
  15. Very specific:

    Prefer a watermark sync (LastModifiedDate >= cursor) plus a small overlap window to avoid missed updates.

    line 464
  16. Distinctive:

    Do not use Bulk API v2 for near-real-time sync unless you are processing a large backfill.

    line 465
  17. Distinctive:

    If HubSpot rejects one record, quarantine that record and continue the rest of the sync batch.

    line 466
  18. Very specific:

    Always filter on LastModifiedDate for incremental pulls.

    line 515
  19. Very specific:

    Check remaining: GET /services/data/v67.0/limits → DailyApiRequests.

    line 530
  20. Very specific:

    Upsert: Use PATCH /sobjects/{Object}/{ExternalIdField}/{value} — safe to call multiple times.

    line 552
  21. Very specific:

    External ID fields: Add a custom External ID field (e.g., Integration_ID__c) and always upsert rather than insert.

    line 553
  22. Very specific:

    Do NOT log: access tokens, VersionData (binary), field values that contain PII (SSN, payment info).

    line 582
  23. Distinctive:

    Admin vs user tokens: Never use System Administrator credentials for integrations.

    line 599

“Common” is measured: kept by at least 6 in 10 of the 6 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
skill.md

Salesforce Skill

Last validated: 2026-10-14 | API: Salesforce REST API + Bulk API v2 | Version: v67.0 (Summer '26) Assumed product: Sales Cloud (CRM). Adjust object availability for other clouds. Version note: v64.0 = Summer '25, v65.0 = Winter '26, v66.0 = Spring '26, v67.0 = Summer '26, v68.0 = Winter '27 (rolling out Oct 2026). All examples in this doc pin /services/data/v67.0/. Older versions stay supported under Salesforce's ~3-year API support window.

⚠️ Checked 2026-10-14 — Winter '27 (v68.0) reviewed, examples stay on v67.0 until rollout completes: the Winter '27 API release notes confirm v68.0 and list no breaking changes to the sObject REST or Bulk API 2.0 calls in this doc. The rollout is not finished: Salesforce's Trust status API (https://api.status.salesforce.com/v1/maintenances, filter name=Winter '27 Major Release) shows the largest wave scheduled for 2026-10-17, and orgs not yet upgraded cannot call /v68.0/. So examples keep /v67.0/, which works everywhere; once GET /services/data/ on your org lists 68.0, switching is a find-and-replace. Other v68.0 items relevant to integrations:

  • latest version alias: /services/data/latest/... routes to the newest version your org supports. Don't use it in production — it moves under you three times a year, the same reason this doc pins a number.
  • Instanced URLs: API traffic to instanced URLs (e.g. na123.salesforce.com) stops in sandboxes in Winter '27; production enforcement was postponed again, to Spring '27. Always call your My Domain URL (https://<MyDomainName>.my.salesforce.com).
  • Suspicious-activity protection: if Salesforce detects suspicious API data access it may terminate the session or freeze the integration user and email the admin. Treat a 401 INVALID_SESSION_ID as "re-authenticate once"; if that fails, stop and alert — a frozen user needs an admin.
  • SOAP login() needs the Use Any API Auth permission from 2026-12-01 and retires for API versions 31.0–64.0 in Summer '27; Platform API versions 31.0–40.0 are being retired. Neither affects this doc, which is REST-only on v67.0.

developer.salesforce.com now returns 403 to non-browser fetchers (the versions doc moved to https://developer.salesforce.com/docs/platform/api-rest/guide/resources-versions.html); the help.salesforce.com release notes render in headless Chrome (--headless=new --dump-dom). See issue https://github.com/Shanksg/clawskills/issues/12.

Sources: REST API versions · Winter '27 API release notes (JS-rendered; open in a browser or headless Chrome)


What this skill enables

  • Capture leads from any external source and create, deduplicate, and route them in Salesforce automatically.
  • Keep Opportunity pipeline data current by updating stages, amounts, and close dates from external triggers.
  • Log all customer interactions (calls, emails, meetings) as Activities (Task/Event) against the correct CRM record.
  • Sync Account and Contact data bidirectionally between Salesforce and other tools without duplication.
  • Process large data loads (10,000+ records) efficiently using Bulk API v2 with job-level error reporting.
  • Subscribe to real-time record changes via Change Data Capture (CDC) to drive downstream workflows.
  • Query any combination of objects and fields using SOQL with full pagination and incremental sync support.

Best-fit use cases

Use caseWhy it mattersPrimary objects/entitiesTypical triggerSuccess criteria
Lead capture from marketing/webAutomate lead intake without manual entryLeadForm submission, ad clickLead created; duplicate check passed; owner assigned
Lead-to-opportunity conversionStructured handoff from marketing to salesLead, Contact, Account, OpportunityLead qualified (status change or manual action)Lead converted; Opportunity with stage set
Opportunity stage syncKeep pipeline accurate for forecastingOpportunityStage change in external system or CRMStage, amount, close date updated correctly
Activity logging (call/email)Compliance, history, next-step trackingTask, EventCall ended, email sentTask/Event created with correct WhoId + WhatId
Account/Contact data syncSingle source of truth across toolsAccount, ContactExternal system updateNo duplicate records; fields updated without overwriting newer data
Case creation & routingAuto-route support requests to correct queueCaseSupport ticket created (Zendesk, email)Case created; assigned to correct queue; SLA start recorded
Bulk data import / migrationLoad historical data or large batch updatesAny objectScheduled ETL jobAll rows processed; error report generated for failures
Quote/Order generationDownstream document creation after dealOpportunity, Quote, OpportunityLineItemOpportunity stage = Closed WonQuote/Order created with correct products and pricing
Contact deduplicationPrevent CRM data pollutionContact, AccountPre-create checkExisting record found and returned; new record not created
Pipeline reporting pullAggregate data for dashboardsOpportunity, AccountScheduled report jobAll records retrieved with pagination; exported to BI
Change Data Capture subscriptionDrive real-time downstream actions without pollingAny enabled objectRecord create/update/deleteCDC event received and processed within seconds
File/attachment managementContract, proposal, document storageContentVersion, ContentDocumentDocument generated or uploadedFile linked to correct record; accessible to right users

Key concepts & data model

Core objects

ObjectDescriptionKey fields
LeadUnqualified prospect; not yet linked to Account/ContactFirstName, LastName, Email, Company, Status, LeadSource, OwnerId
ContactIndividual person linked to an AccountFirstName, LastName, Email, Phone, AccountId, OwnerId
AccountCompany or organizationName, Type, Industry, AnnualRevenue, OwnerId, ParentId
OpportunitySales deal with a stage and close dateName, AccountId, StageName, Amount, CloseDate, OwnerId, Probability
TaskLogged activity (call, email, to-do)Subject, Status, WhoId (Contact/Lead), WhatId (Opportunity/Account), ActivityDate, OwnerId
EventCalendar activity with start/end timeSubject, StartDateTime, EndDateTime, WhoId, WhatId, OwnerId
CaseSupport ticketSubject, Description, Status, Priority, AccountId, ContactId, OwnerId
ContentVersionA file/document versionTitle, PathOnClient, VersionData (base64), ContentDocumentId
ContentDocumentLinkLinks a file to a recordContentDocumentId, LinkedEntityId, ShareType

Identifying records

  • All records have an Id field: 15-character case-sensitive or 18-character case-insensitive Salesforce ID.
  • Always use 18-character IDs in URLs and external storage to avoid case-sensitivity bugs.
  • Natural keys: Email for Contact/Lead; Name for Account (not unique — use with caution).

Relationships

Account (1) ──< Contact (many)
Account (1) ──< Opportunity (many)
Opportunity (1) ──< OpportunityLineItem (many)
Contact/Lead (1) ──< Task/Event (many, via WhoId)
Opportunity/Account (1) ──< Task/Event (many, via WhatId)
Case (1) → Contact (via ContactId)
Case (1) → Account (via AccountId)
ContentDocument (1) ──< ContentVersion (many, 1 per version)
ContentDocument ──< ContentDocumentLink (links to any record)

Authentication & permissions

Supported auth methods

MethodUse caseNotes
OAuth 2.0 Authorization CodeUser-context web appsRequires browser redirect
OAuth 2.0 JWT Bearer FlowServer-to-server, no user interactionBest for automation; uses X.509 certificate
OAuth 2.0 Username-PasswordLegacy / testing onlyNot recommended for production
Session ID / SOAP loginLegacyAvoid for new integrations

Connected App setup (JWT Bearer Flow — recommended for automation)

  1. In Salesforce Setup → App Manager → New Connected App.
  2. Enable OAuth; add api and refresh_token scopes.
  3. Enable "Use digital signatures"; upload your RSA public key (X.509 PEM).
  4. Pre-authorize the integration user (Setup → Manage Connected Apps → edit the app → Manage Profiles/Permission Sets).
# Build the JWT assertion
# Header: {"alg":"RS256","typ":"JWT"}
# Payload: {"iss":"{consumer_key}","sub":"{username}","aud":"https://login.salesforce.com","exp":<unix+3min>}

# Exchange JWT for access token
curl -X POST https://login.salesforce.com/services/oauth2/token \
  -d "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer" \
  -d "assertion={signed_jwt}"

# Response includes: access_token, instance_url, token_type

Required OAuth scopes (least privilege)

ScopePurpose
apiFull REST API access (required baseline)
refresh_tokenObtain refresh tokens
offline_accessEquivalent to refresh_token in some flows
chatter_apiFor Chatter/feed posts only
custom_permissionsFor custom permission sets

Token storage

  • access_token: short-lived (typically 2 hours for connected apps). Never store in plaintext.
  • refresh_token: long-lived. Store in secrets manager (Vault, AWS Secrets Manager, etc.).
  • instance_url: the base URL for all API calls (e.g., https://yourorg.my.salesforce.com). Store alongside the token — it changes during org migration.

Multi-tenant

  • Salesforce supports multiple sandboxes (dev, staging) and a production org. Use https://test.salesforce.com for sandbox auth and https://login.salesforce.com for production.
  • Each org has a unique instance_url. Never hard-code it.

Common workflows (recipes)

Recipe 1: Create a Lead with duplicate check

Goal: Insert a new lead only if one doesn't already exist with the same email.

Steps:

  1. Query for an existing Lead by email.
  2. If found, return existing ID (and optionally update fields).
  3. If not found, create a new Lead.
# Step 1 — check for existing
BASE="https://yourorg.my.salesforce.com"
TOKEN="your_access_token"

curl -s -G "$BASE/services/data/v67.0/query" \
  --data-urlencode "q=SELECT Id, Email, Status FROM Lead WHERE Email = 'prospect@example.com' LIMIT 1" \
  -H "Authorization: Bearer $TOKEN"
# Response: {"totalSize": 0, "done": true, "records": []}  → safe to create

# Step 2 — create
curl -s -X POST "$BASE/services/data/v67.0/sobjects/Lead" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "FirstName": "Jane",
    "LastName": "Smith",
    "Email": "prospect@example.com",
    "Company": "Acme Corp",
    "LeadSource": "Web",
    "Status": "New"
  }'
# Response: {"id": "00Q...", "success": true, "errors": []}

Edge cases:

  • If validation rules fire (e.g., required custom field missing), errors[] is non-empty.
  • Leads with the same email can coexist — Salesforce does not enforce unique email by default. Consider enabling duplicate rules in Setup.
  • If using Duplicate Management, a 409 with DUPLICATE_VALUE error is returned.

Recipe 2: Query records with SOQL and paginate

Goal: Retrieve all Opportunities in a given pipeline stage, across pages.

# Initial query (returns up to 2000 records by default; Salesforce caps at 2000 per page)
curl -s -G "$BASE/services/data/v67.0/query" \
  --data-urlencode "q=SELECT Id, Name, StageName, Amount, CloseDate, Account.Name FROM Opportunity WHERE StageName = 'Proposal/Price Quote' AND CloseDate >= 2026-01-01 ORDER BY CloseDate ASC" \
  -H "Authorization: Bearer $TOKEN"

# Response includes:
# {
#   "totalSize": 5432,
#   "done": false,
#   "nextRecordsUrl": "/services/data/v67.0/query/01g...",
#   "records": [...]
# }

# Paginate — follow nextRecordsUrl until done = true
curl -s "$BASE/services/data/v67.0/query/01g..." \
  -H "Authorization: Bearer $TOKEN"

Incremental sync (updated_since pattern):

SELECT Id, Name, StageName, LastModifiedDate
FROM Opportunity
WHERE LastModifiedDate >= 2026-02-18T00:00:00Z
ORDER BY LastModifiedDate ASC

SOQL pitfalls:

  • WHERE cannot use LIKE on relationship fields (e.g., Account.Name LIKE '%Acme%').
  • Maximum 200 fields in a SELECT clause.
  • Use SOQL OFFSET for small datasets, queryLocator (above) for large ones.
  • Date literals: TODAY, LAST_N_DAYS:7, THIS_MONTH — very useful for relative filters.

Recipe 3: Update an Opportunity stage

Goal: Move an Opportunity to a new stage and update close date and amount.

OPP_ID="006..."

curl -s -X PATCH "$BASE/services/data/v67.0/sobjects/Opportunity/$OPP_ID" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "StageName": "Closed Won",
    "Amount": 75000.00,
    "CloseDate": "2026-02-28"
  }'
# 204 No Content on success

Upsert by external ID:

# Requires a custom External ID field on the object (e.g., External_ID__c)
curl -s -X PATCH "$BASE/services/data/v67.0/sobjects/Opportunity/External_ID__c/EXT-1234" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "StageName": "Negotiation/Review" }'
# Creates if not found, updates if found — idempotent

Validation: PATCH returns 204. Re-query and verify StageName matches.


Recipe 4: Log a Task (activity) against a Contact and Opportunity

Goal: Record a completed call as a Task linked to both a Contact and an Opportunity.

curl -s -X POST "$BASE/services/data/v67.0/sobjects/Task" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "Subject": "Discovery Call - Q1 2026",
    "Status": "Completed",
    "Priority": "Normal",
    "ActivityDate": "2026-02-19",
    "WhoId": "003...",
    "WhatId": "006...",
    "Description": "Discussed product fit. Next step: send proposal by 2026-02-25.",
    "CallDurationInSeconds": 1820,
    "CallType": "Outbound"
  }'

Key fields:

  • WhoId: Contact or Lead ID (person).
  • WhatId: Opportunity, Account, Case, or other non-person object.
  • A single Task can have one WhoId and one WhatId. For multiple contacts, create a TaskRelation record.

Recipe 5: Attach a file to a record (ContentVersion)

Goal: Upload a PDF proposal and link it to an Opportunity.

# Step 1 — upload file as ContentVersion
BASE64_CONTENT=$(base64 -i proposal.pdf)

curl -s -X POST "$BASE/services/data/v67.0/sobjects/ContentVersion" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d "{
    \"Title\": \"Q1 2026 Proposal\",
    \"PathOnClient\": \"proposal.pdf\",
    \"VersionData\": \"$BASE64_CONTENT\",
    \"FirstPublishLocationId\": \"006...\"
  }"
# FirstPublishLocationId links to the Opportunity on first publish
# Response: {"id": "068...", "success": true}

For files >25 MB, use the multipart upload endpoint (/services/data/v67.0/sobjects/ContentVersion with multipart/form-data).


Recipe 6: Use the Composite API for multi-step operations

Goal: Create a Contact and link it to an Account in a single HTTP round-trip.

curl -s -X POST "$BASE/services/data/v67.0/composite" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "allOrNone": true,
    "compositeRequest": [
      {
        "method": "POST",
        "url": "/services/data/v67.0/sobjects/Contact",
        "referenceId": "newContact",
        "body": {
          "FirstName": "Jane",
          "LastName": "Smith",
          "Email": "jane.smith@acme.com",
          "AccountId": "001..."
        }
      },
      {
        "method": "POST",
        "url": "/services/data/v67.0/sobjects/Task",
        "referenceId": "welcomeTask",
        "body": {
          "Subject": "Welcome call",
          "WhoId": "@{newContact.id}",
          "ActivityDate": "2026-02-20",
          "Status": "Not Started"
        }
      }
    ]
  }'

allOrNone: true — if any step fails, all are rolled back. Use @{referenceId.field} to pass results between steps.


Recipe 7: Bulk upsert with Bulk API v2

Goal: Load 50,000 lead records from a CSV, upserting by email.

# Step 1 — create a bulk job
curl -s -X POST "$BASE/services/data/v67.0/jobs/ingest" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "object": "Lead",
    "operation": "upsert",
    "externalIdFieldName": "Email",
    "contentType": "CSV",
    "lineEnding": "LF"
  }'
# Response: {"id": "7507...", "state": "Open", ...}

JOB_ID="7507..."

# Step 2 — upload CSV data
curl -s -X PUT "$BASE/services/data/v67.0/jobs/ingest/$JOB_ID/batches" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: text/csv" \
  --data-binary @leads.csv

# Step 3 — close the job (triggers processing)
curl -s -X PATCH "$BASE/services/data/v67.0/jobs/ingest/$JOB_ID" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"state": "UploadComplete"}'

# Step 4 — poll job status until Complete or Failed
curl -s "$BASE/services/data/v67.0/jobs/ingest/$JOB_ID" \
  -H "Authorization: Bearer $TOKEN"
# state: InProgress → JobComplete

# Step 5 — retrieve failed records
curl -s "$BASE/services/data/v67.0/jobs/ingest/$JOB_ID/failedResults" \
  -H "Authorization: Bearer $TOKEN"

# Step 6 — retrieve successful and unprocessed records for reconciliation
curl -s "$BASE/services/data/v67.0/jobs/ingest/$JOB_ID/successfulResults" \
  -H "Authorization: Bearer $TOKEN"

curl -s "$BASE/services/data/v67.0/jobs/ingest/$JOB_ID/unprocessedRecords" \
  -H "Authorization: Bearer $TOKEN"

When to use Bulk API v2: >1,000 records. For <1,000 records, REST is simpler and faster.

CSV and job design tips:

  • Keep one object type per job. Do not mix Leads and Contacts in the same ingest file.
  • Include only columns you intend to write. Extra CSV columns are treated as field references and can fail the row.
  • Shard very large backfills into multiple jobs so a single failure does not block the full dataset.
  • Reconcile with all three result streams: successfulResults, failedResults, and unprocessedRecords.

Partial-failure policy:

  • Retry only unprocessedRecords automatically.
  • Send failedResults to a remediation queue with the Salesforce error message and original source key.
  • Do not advance the upstream source cursor until every row is either successful or quarantined.

Recipe 8: Register a Change Data Capture (CDC) subscription

Goal: Receive real-time push events when Opportunity records are created or modified.

CDC requires the Streaming API (CometD protocol). Simplified flow:

  1. In Setup → Change Data Capture, enable OpportunityChangeEvent.
  2. Connect a CometD client to $BASE/cometd/62.0/.
  3. Handshake, then subscribe to /data/OpportunityChangeEvent.
# Pseudocode — use the `cometd` or `aiosfstream` Python library
client = CometDClient(instance_url, access_token)
client.subscribe("/data/OpportunityChangeEvent", handler)

def handler(event):
    change_type = event["payload"]["ChangeEventHeader"]["changeType"]  # CREATE, UPDATE, DELETE, UNDELETE
    changed_fields = event["payload"]["ChangeEventHeader"]["changedFields"]
    record_ids = event["payload"]["ChangeEventHeader"]["recordIds"]
    # Process the change

Pitfalls: CDC events do not include all field values — only changed fields. Fetch the full record if you need complete data.


Cross-tool recipe: Salesforce Lead -> HubSpot Contact sync

Goal: Create or update a HubSpot Contact whenever a Salesforce Lead is created or updated, deduplicating by email.

Flow:

  1. Query Salesforce Leads modified since the last sync boundary using LastModifiedDate.
  2. Skip records with blank Email or IsConverted = true.
  3. In HubSpot, search by email first. If found, PATCH the Contact. If not, create it.
  4. Store the mapping Lead.Id -> HubSpot contact id in your integration DB, but treat email as the primary dedup key.
  5. Write a sync marker back to Salesforce only after the HubSpot write succeeds.

Field mapping (minimum viable):

Salesforce LeadHubSpot Contact
Emailemail
FirstNamefirstname
LastNamelastname
Companycompany
Phonephone
LeadSourcelead_source or custom property

Operational guardrails:

  • Prefer a watermark sync (LastModifiedDate >= cursor) plus a small overlap window to avoid missed updates.
  • Do not use Bulk API v2 for near-real-time sync unless you are processing a large backfill.
  • If HubSpot rejects one record, quarantine that record and continue the rest of the sync batch.

Query patterns & filtering

SOQL reference patterns

-- Exact match
WHERE Email = 'user@example.com'

-- Pattern match
WHERE Name LIKE '%Acme%'

-- Date range
WHERE CloseDate >= 2026-01-01 AND CloseDate <= 2026-03-31

-- Relative date
WHERE LastModifiedDate >= LAST_N_DAYS:7

-- Null check
WHERE OwnerId = null

-- IN list
WHERE StageName IN ('Prospecting', 'Qualification')

-- Subquery (semi-join)
WHERE AccountId IN (SELECT Id FROM Account WHERE Industry = 'Technology')

-- Relationship traversal
SELECT Id, Name, Account.Name FROM Contact WHERE Account.Industry = 'Healthcare'

-- Aggregate
SELECT StageName, COUNT(Id), SUM(Amount) FROM Opportunity GROUP BY StageName

-- SOSL (search across objects)
FIND {Acme Corp} IN ALL FIELDS RETURNING Account(Id, Name), Contact(Id, Name)

Pagination

Initial: GET /query?q=SELECT...      → nextRecordsUrl if done=false
Next:    GET {nextRecordsUrl}         → follow until done=true

Max page size: 2,000 records. Set queryBatchSize header or use LIMIT/OFFSET (OFFSET max: 2,000).

Incremental sync

Always filter on LastModifiedDate for incremental pulls. Store the max LastModifiedDate from each run as the next sync's start boundary.


Reliability: rate limits, retries, idempotency

API call limits (per 24 hours, rolling)

EditionCalls/day (approximate)
Essentials1,000 per licensed user
Professional1,000 per licensed user
Enterprise1,000 per licensed user
Unlimited5,000 per licensed user

Check remaining: GET /services/data/v67.0/limits → DailyApiRequests.

Rate limit headers

Sforce-Limit-Info: api-usage=1234/15000

Retry strategy

HTTP StatusMeaningAction
200/201/204SuccessDone
400Bad request (schema/validation error)Do NOT retry — fix the payload
401Unauthorized — token expiredRefresh token, then retry once
403Forbidden — insufficient permissionsDo NOT retry — fix permissions
404Record not foundDo NOT retry — check ID
429Rate limitedRespect Retry-After header; exponential backoff
500/503Salesforce internal errorRetry with exponential backoff (up to 3 attempts)

Idempotency

  • Upsert: Use PATCH /sobjects/{Object}/{ExternalIdField}/{value} — safe to call multiple times.
  • External ID fields: Add a custom External ID field (e.g., Integration_ID__c) and always upsert rather than insert.
  • Duplicate check: Query first, then create if not found. Wrap in a Composite request with allOrNone: true to atomically check-and-create.

Concurrency

Salesforce uses optimistic locking. If two updates conflict, one returns an error. Handle UNABLE_TO_LOCK_ROW by retrying with a short delay (1–3 seconds).


Error handling & troubleshooting

Common error codes

Error codeMeaningFix
REQUIRED_FIELD_MISSINGRequired field not providedAdd the missing field
DUPLICATE_VALUEDuplicate rule violationQuery for existing record and return it
FIELD_CUSTOM_VALIDATION_EXCEPTIONValidation rule firedCheck validation rules in Setup; fix payload
INVALID_FIELDField name wrong or doesn't existVerify field API name (not label)
UNABLE_TO_LOCK_ROWConcurrent update conflictRetry after 1–3s delay
ENTITY_IS_DELETEDRecord is in recycle binQuery with ALL ROWS clause; undelete or skip
INSUFFICIENT_ACCESS_ON_CROSS_REFERENCE_OBJECTNo access to related recordCheck OwnerId / sharing rules
STRING_TOO_LONGField value exceeds max lengthTruncate the value
MALFORMED_ID15/18 char ID format issueCheck the ID; use 18-char form
API_CURRENTLY_DISABLEDAPI access not enabled for userEnable API access on the user's profile

Debug logging

Log: object type, operation, record ID (not PII), error code, error message, HTTP status. Do NOT log: access tokens, VersionData (binary), field values that contain PII (SSN, payment info).

Governor limits to monitor

  • DailyApiRequests — primary limit
  • DailyBulkApiRequests — Bulk API calls
  • DataStorageMB — storage per org (not per-call but affects org health)

Security, privacy, compliance

  • Record-level security: Salesforce enforces OWD (org-wide defaults), sharing rules, and role hierarchy. Integration users need explicit sharing access — test with the integration user profile, not an admin.
  • Field-level security: Fields may be invisible to the integration user's profile. If a field returns null unexpectedly, check FLS in Setup.
  • PII: Contact/Lead objects contain PII. Log field names and IDs, not field values.
  • GDPR/CCPA: Salesforce supports data deletion via the delete API. For right-to-erasure workflows, delete the Contact/Lead record and scrub related Tasks/Events.
  • Audit trail: Salesforce Field History Tracking provides per-field audit logs (enable per object in Setup). Setup Audit Trail logs configuration changes.
  • Admin vs user tokens: Never use System Administrator credentials for integrations. Create a dedicated Integration User with a custom profile scoped to the minimum required objects and fields.
  • Event monitoring: Enable Event Monitoring (add-on) to audit API login events, data exports, and apex executions.

Testing checklist

  • Auth test: Exchange JWT for access token; call GET /services/data/v67.0/ — returns API version list.
  • CRUD test: Create a Lead → read by ID → update Status → delete (hard delete via DELETE /sobjects/Lead/{id}).
  • SOQL pagination test: Query an object with >2,000 records; follow nextRecordsUrl to completion; verify totalSize matches record count.
  • Rate limit test: Call GET /services/data/v67.0/limits and log DailyApiRequests; simulate limit approach with retries.
  • Bulk API test: Upload a 500-row CSV; poll job to completion; verify numberRecordsProcessed = 500; check failedResults.
  • Upsert idempotency test: Upsert same record twice with same External ID — verify second call updates rather than duplicates.
  • Permission test: Call with a token scoped to a user without Field-level access — verify affected fields return null.
  • Negative tests:
    • Missing required field → REQUIRED_FIELD_MISSING
    • Invalid Salesforce ID → MALFORMED_ID
    • Revoked token → 401 Unauthorized
    • Record in recycle bin → ENTITY_IS_DELETED

Sources

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.