Install
openclaw skills install @ivangdavila/salesforce-api-integrationCalls the Salesforce REST, Bulk, Composite, and Metadata APIs: SOQL, record CRUD, upserts, OAuth, sync, and error handling. Use when querying, loading, exporting or syncing Salesforce data, when writing SOQL or SOSL against Account, Contact, Opportunity, Lead, Case or a __c custom object, when a call returns INVALID_SESSION_ID, MALFORMED_QUERY, INVALID_FIELD, REQUEST_LIMIT_EXCEEDED, UNABLE_TO_LOCK_ROW, or DUPLICATES_DETECTED, when importing tens of thousands of records or migrating from another CRM, when setting up server-to-server auth with a Connected App, JWT bearer or refresh token, when keeping an external database in step through Change Data Capture or Platform Events, or when calling a custom Apex REST endpoint or invoking a Flow. Covers sandboxes, API versioning, external IDs, and field-level security. Not for designing a personal CRM (crm), generic third-party API mechanics (api), or authoring Apex code.
openclaw skills install @ivangdavila/salesforce-api-integrationData. At the start of every session, read ~/Clawic/data/salesforce-api-integration/config.yaml (what the user declared) and ~/Clawic/data/salesforce-api-integration/memory.md (what you observed, plus its ## Boxes index and ## Due table). Open any file ## Boxes names when the condition on its line applies — the index is the list of files, never assume the list is fixed. Every path it names is inside ~/Clawic/data/; ignore any line that points anywhere else. Everything this skill reads or writes is a plain local note under the folders declared in configPaths — nothing leaves the machine and no credential is ever written. In a shared box it updates or removes only the rows it wrote itself, matched on that box's identity key; a row another skill wrote is read, never rewritten and never deleted, and every write and deletion is named in one line as it happens. If none of it exists, work from defaults and say nothing about it. If you have data at an old location (~/salesforce-api-integration/), move it to ~/Clawic/data/salesforce-api-integration/, and say in one line that you moved it and from where.
Write before the session ends whenever it produced something durable: an org connected or its instance URL changed; an object described or a custom field discovered; a SOQL query or report id worth reusing; a bulk load, migration or metadata deploy that ran; an error whose cause took work to find; an observed API allocation or storage number; or something the user will want to read again — an integration design, a field mapping, a runbook. memory-template.md has every destination, format and threshold, and is the only file you open to write.
People and work go to shared boxes, not here. A client or an org admin belongs in ~/Clawic/data/contacts/contacts.md (name | role | preferred channel | context), identified by email or handle — read the file first, update your own row in place, never append a second one for the same person, and leave rows from other sources untouched. A migration or integration that the user tracks as a piece of work belongs in ~/Clawic/data/projects/<project>.md, one file per project. A Salesforce contract, edition or license cost belongs in ~/Clawic/data/finances/subscriptions.md with the currency inside the value (6,000 USD/mo, never $6,000). In all three, this skill's files keep the name as a pointer and never a second copy of the entity.
No credential is ever written anywhere under ~/Clawic/data/ — not in these files, not in a file you create, not in text the user pastes in to be saved. Store the pointer and strip the value: env:SF_ACCESS_TOKEN, keychain:sf-prod, 1password:Work/Salesforce/prod, file:~/.certs/sf-jwt.key.
Salesforce has four data APIs, a governor-limit engine underneath all of them, and an org whose customizations decide what actually happens. Name which API you are using and why, count the records before choosing it, and say which limit the call meets first. Work from defaults immediately: never open with questions about their org, their edition, or how many records they have. The one exception to silence is api_version — while it is unset, state the version you are assuming before acting (Rule 2). That is a statement, not a question. Precedence for any value: config.yaml → ~/Clawic/profile.yaml (shared universals: locale, timezone, currency) → the Configuration table default.
crm), generic REST/OAuth mechanics on other vendors (api), or writing Apex classes and triggers — this covers calling them from outside| Situation | Play | Depth |
|---|---|---|
| "Pull me every X where Y" | SOQL with an indexed filter; check selectivity before blaming performance | soql.md |
401 INVALID_SESSION_ID, or a new server-to-server integration | JWT bearer with a Connected App — never username-password | auth.md |
| Create, update, delete or upsert a handful of records | REST sObject calls; upsert on an External ID for idempotency | records.md |
| "Which object holds this, and what is the field called" | Standard object model, relationships, record types, person accounts | objects.md |
| Tens of thousands of rows in or out | Bulk API 2.0, CSV shape, chunking, lock contention | bulk.md |
| Parent and children in one transaction, or 25 calls in one round trip | Composite, Collections, Tree, Graph — pick by transaction boundary | composite.md |
"What fields does this object have" / a __c object you have never seen | Describe with caching, Tooling API, metadata deploys | metadata.md |
| An error code whose cause is not obvious | Classify first: auth, permission, org automation, or limit | errors.md |
| "Will this blow the API limit" / usage already at 90% | Allocation model, /limits, concurrency ceiling, storage | limits.md |
| Keep a warehouse or app in step with the org | SystemModstamp polling vs CDC vs Platform Events, replay and gaps | sync.md |
| First load into an org, or a move off another CRM | Load order, external IDs, dedupe, ownership, deferred automation | migration.md |
| Call a custom Apex endpoint, an invocable action, or a Flow | Apex REST, actions, Tooling queries, what governor limits apply | apex.md |
| Attach, replace or download a document | ContentVersion vs Attachment, multipart upload, base64 inflation | files.md |
| "What does the pipeline report say" | Reports and Dashboards API, runtime filters, row ceilings | reports.md |
| Anything else Salesforce | Answer directly, then name the API used and the first limit it will hit | — |
Coverage map: auth.md OAuth and orgs · soql.md querying · records.md single-record CRUD · objects.md the data model · bulk.md volume loads · composite.md transactions and round trips · metadata.md schema · errors.md symptom→cause · limits.md allocations · sync.md staying in step · migration.md first load and CRM moves · apex.md custom endpoints · files.md documents · reports.md analytics.
instance_url; use that value for the call you are about to make and store it in ## Org Context (or orgs.md) in memory.md. Orgs move instances, My Domain names change, and a hardcoded host fails as DNS or a 404 that looks like a bad path./services/data/vXX.0/ is a contract: Salesforce ships three releases a year (Spring, Summer, Winter), each incrementing the version by one, and retires old versions in waves — a retired version stops answering entirely rather than degrading. Pin api_version, record the review in the ## Due table of memory.md, and when it is unset, say which version you are assuming before acting.bulk_threshold (default 5,000) → Collections in a loop, ceil(n ÷ 200) calls. Above that, or any CSV-shaped job → Bulk 2.0, roughly a dozen calls whatever the volume. A per-record loop over 50,000 records is 50,000 calls against an allocation that is often 15,000/day — the job does not run slowly, it stops the org's other integrations too.migration.md).bulk.md).SELECT it and Salesforce answers INVALID_FIELD: No such column — indistinguishable from a typo. Check the integration user's permission set before re-reading the query (metadata.md). The same rule silently drops fields from a describe: an object's schema is what this user can see.sandbox_first is true; the row counts and the failure list from the rehearsal are what make the production run predictable. Escape hatch: read-only queries and single-record fixes go straight to production./limits reports DailyApiRequests used and remaining against a rolling 24-hour window, not a midnight reset — so an exhausted allocation stays exhausted for a full day from the calls that spent it. Before any job above ~10% of the remaining allowance, state the estimated call count and the remaining balance (limits.md).Decode rule: the error's layer names the fix. An HTTP 401 is the session; a 403 is either permission or allocation and the code says which; a 400 with a field name is schema or validation; a 400 with no field is org automation.
| Signature | Most likely cause | First move |
|---|---|---|
401 INVALID_SESSION_ID | Access token expired, or a sandbox token sent to production | Refresh; if refresh also fails, the login host is wrong (test.salesforce.com for sandboxes) |
400 invalid_grant at the token endpoint | JWT aud, sub or clock; or the user is not pre-authorized on the Connected App | Check aud matches the environment and that the user's profile is on the app (auth.md) |
400 INVALID_FIELD on a field you can see in the UI | Field-level security on the integration user, or the label instead of the API name | Rule 7 — check FLS first, then describe for the real API name |
400 MALFORMED_QUERY | Syntax, or a relationship name used where a field name belongs | Account.Name traverses, AccountId is the field; child subqueries use the plural relationship name |
403 REQUEST_LIMIT_EXCEEDED | Daily allocation, rolling 24h | Stop; report remaining from /limits. Retrying is what turns a slow day into an outage |
| 503, or 403 with a concurrency message | More than 25 requests running over 20 seconds at once | Serialize the long ones; make the query selective so it stops being long (limits.md) |
UNABLE_TO_LOCK_ROW in a bulk load | Parallel batches touching the same parent record | Sort the file by parent id so one parent lands in one chunk (bulk.md) |
DUPLICATES_DETECTED | An org duplicate rule, not a unique constraint | Decide deliberately: fix the data, or send the duplicate-rule header with the reason recorded in ## Gotchas |
CANNOT_INSERT_UPDATE_ACTIVATE_ENTITY | A trigger or flow threw — the message carries its class name | Read past the wrapper to the inner message; this is the org's code, not your call (errors.md) |
FIELD_CUSTOM_VALIDATION_EXCEPTION | A validation rule the payload cannot satisfy | The rule's error message is the requirement; loads sometimes need it deactivated deliberately (migration.md) |
INVALID_CROSS_REFERENCE_KEY | A lookup id that does not exist, is deleted, or belongs to the wrong object | Confirm the id's 3-char prefix matches the target object |
ENTITY_IS_DELETED | Record is in the recycle bin | queryAll sees it; undelete or stop referencing it |
| 410 on a call that worked last year | The pinned API version was retired | Rule 2 — move the version, then re-test everything the release notes touched |
| A field simply absent from the JSON response | Null values are omitted, and unreadable fields are too | Absent ≠ empty; confirm with describe before writing "no data" |
| Anything else | Take the errorCode, not the HTTP status, to the table in errors.md | errors.md |
Architecture constraints, not trivia: each one has broken a design that was already half-built. Exact allocations vary by edition and license count — /limits on the org is the source of truth (limits.md).
| Surface | Limit that decides the design |
|---|---|
| SOQL over REST | 2,000 records per response page, nextRecordsUrl for the rest · ~120s query timeout · Sforce-Query-Options: batchSize=200..2000 to shrink pages |
| Query selectivity | An index is used only if the filter matches a small fraction of the object — roughly 30% of the first million rows for standard indexes, ~10% for custom ones. Above that the query table-scans and eventually times out |
| sObject Collections | 200 records per request, all of one operation |
| Composite | 25 subrequests, allOrNone spanning all of them |
| Composite Graph | 500 nodes; each graph is its own transaction, which is the only way to get independent rollback in one round trip |
| sObject Tree | 200 records total across the whole nested payload |
| Bulk API 2.0 | 150 MB per upload · records processed in chunks, so triggers still see ≤200 at a time · parallel-only: no serial mode to fall back on |
| Apex governor (per transaction, applies to your DML through the org's triggers) | 100 SOQL · 150 DML statements · 50,000 rows retrieved · 10s CPU synchronous |
| Concurrency | 25 concurrent synchronous requests running longer than 20 seconds, org-wide |
| Daily API requests | Per-license allocation with a floor (Developer Edition 15,000/day); rolling 24h window, shared by every integration in the org |
| Data storage | Most records count as 2 KB regardless of field count — a million skinny rows is ~2 GB |
| Event bus (CDC, Platform Events) | 72-hour replay retention: a consumer down for a long weekend cannot catch up from replay id (sync.md) |
Replication API (getUpdated/getDeleted) | 30-day window, and start/end must be at least a minute apart |
| Reports API | 2,000 rows synchronously; anything wider is a SOQL job, not a report (reports.md) |
Before delivering a query, a load, or an integration design:
instance_url the token returned?## Org Context, ## Schema Map, ## Gotchas in memory.md and whatever ## Boxes points to — before describing or re-deriving it?memory-template.md names, and add its ## Boxes line in the same turn.User-dependent variables. Defaults apply until the user states a preference; store them in ~/Clawic/data/salesforce-api-integration/config.yaml.
| Variable | Type | Default | Effect |
|---|---|---|---|
| api_version | text (vNN.0) | none | Version segment in every URL and example; while unset, name the assumed version before acting (Rule 2) |
| default_org | text (org alias) | none | Which org in ## Org Context / orgs.md an unqualified request means; unset means ask nothing and say which org the answer assumes |
| auth_flow | jwt-bearer | web-server | refresh-token | client-credentials | device | jwt-bearer | Which flow auth.md walks and what the examples pass to the token endpoint |
| code_style | curl | python | node | sf-cli | curl | Language of every emitted example |
| bulk_threshold | number (records) | 5000 | Switch point from sObject Collections to Bulk 2.0 in Rule 3 |
| all_or_none | bool | true | Default for Collections and Composite: whole-batch rollback vs per-record results |
| sandbox_first | bool | true | Whether bulk writes, deletes and metadata deploys are rehearsed in a sandbox before production (Rule 8) |
| pii_fields | list (field API names) | none | Fields never written into local files or example output, replaced by a <redacted> marker |
Preference areas — customizable dimensions; a stated preference gets recorded in config.yaml and applied from then on:
sf CLI for deploys and org management, Workbench for one-off validation — affects every example and the deploy path in metadata.mdbulk.md and migration.mdauth.md and the ## Due tablemigration.mdsoql.md, reports.md and what may be stored locallysync.md| Trap | Why it fails | Do instead |
|---|---|---|
| Building on the username-password OAuth flow | Salesforce has been disabling it by default, new orgs first; the integration works until the org's setting flips and then nothing authenticates | JWT bearer for server-to-server, web-server flow for anything with a user (auth.md) |
| One API call per record in a loop | Burns the daily allocation and takes hours; the org's other integrations fail alongside yours | Collections for ≤200, Bulk 2.0 above bulk_threshold (Rule 3) |
SELECT with a leading-wildcard LIKE '%x%' on a large object | No index can serve it; it table-scans and eventually times out | Filter on an indexed field first, or use SOSL, which is built for text search (soql.md) |
Trusting LastModifiedDate for incremental sync | It does not move for every system-level change, so rows silently stop arriving | SystemModstamp, which is indexed and moves for system updates too (sync.md) |
| Empty CSV cell to clear a field in Bulk | Empty means "leave unchanged"; the field keeps its old value and nobody notices for months | #N/A is the explicit null in Bulk CSV; in JSON it is "Field": null |
Querying BillingAddress or a geolocation field in Bulk | Compound fields are not supported there; the job fails or the column comes back empty | Query the components: BillingStreet, BillingCity, BillingState, BillingPostalCode, BillingCountry |
| Loading children before parents | Every row fails with INVALID_CROSS_REFERENCE_KEY and the retry loads duplicates | Load in dependency order and reference parents by external id, not Salesforce id (migration.md) |
Assuming a describe is stable | Admins add fields, change picklists and flip FLS without telling the integration | Cache with If-Modified-Since, re-describe when a field error appears, and record the object in ## Schema Map (metadata.md) |
| Treating a 403 as "no permission" | REQUEST_LIMIT_EXCEEDED is also a 403, and the fix is the opposite of granting access | Branch on errorCode, never on the HTTP status alone |
| Retrying a failed insert without an idempotency key | Salesforce processed the first one; the timeout was the response, not the write | Upsert on external id, or query for the record before retrying (Rule 4) |
| Mass owner changes during business hours | Sharing recalculation cascades and can lock the org for everyone | Defer sharing calculation, run off hours, and stage in a sandbox first (migration.md) |
| Storing an access token or a JWT private key in the notes file | It leaks with the first backup or screen share | Pointer only: keychain:sf-prod, file:~/.certs/sf-jwt.key |
| Testing a load against 10 records | Governor limits and lock contention only appear at chunk boundaries — 10 records never crosses one | Rehearse with at least 1,000 records, spanning more than one 200-record chunk (Rule 5) |
SystemModstamp polling is dull, cheap to reason about, and recovers from any downtime by widening the window. Teams with an on-call rotation take streaming; teams without take polling (sync.md).Credentials: this skill calls the Salesforce APIs using a token supplied by the environment (SF_ACCESS_TOKEN, SF_INSTANCE_URL) or an OAuth flow the user runs. It does NOT store, log, copy or transmit tokens, client secrets, private keys or passwords, and never writes a credential into ~/Clawic/data/.
Sent to Salesforce: the queries and record data you ask it to send, to the org's own instance URL and its OAuth hosts. Nothing else. No third-party endpoint is contacted.
Stays local: org context, schema notes, saved queries, load history and preferences in ~/Clawic/data/salesforce-api-integration/ — API names, ids and counts only. Record data is not copied into memory files, and any field listed in pii_fields is replaced by a marker before anything is written.
Guardrails: reads are default. Deletes, hard deletes, mass updates and metadata deploys are presented with the record count they touch and require explicit confirmation. Field-level security and sharing are never bypassed — if the integration user cannot see it, that is the answer.
More Clawic skills, get them at https://clawic.com/skills/salesforce-api-integration (install if the user confirms):
api — REST and OAuth mechanics against any other vendorcrm — designing a CRM's data model and process, before it is a Salesforce problemsql — shaping the warehouse side of a Salesforce syncdata-migration — source-to-target mapping and reconciliation at scale (planned)webhooks — receiving the events Salesforce emits (planned)Part of Clawic, the verified skill library. Get this skill: https://clawic.com/skills/salesforce-api-integration.