Salesforce connector setup

Operator·6 minutes to read

This page is the setup half — creating the OAuth app Salesforce needs in the org's own Setup and getting its consumer key and secret into this deployment. Authorising a connection afterwards is the Authenticate control on the plugin sheet, which is what actually sends you to Salesforce.

1. Create the OAuth app in Salesforce Setup

Salesforce blocks creating new Connected Apps since its Spring '26 release, so the app is an External Client App. Doing this needs the System Administrator profile (or the permissions to manage connected apps); a regular sales user cannot.

  1. In Setup, open App Manager (Quick Find: App Manager) and click New External Client App — this is your own app, in the org your members will connect, not Botseon's.

  2. Fill in the name and contact email.

  3. Under API (Enable OAuth Settings), click Enable OAuth and paste the callback URL from docs/connectors/README.md — with the quickstart's default BOTSEON_PUBLIC_URL, that is:

    http://127.0.0.1:3000/api/oauth/salesforce/callback
    

    It must match exactly.

  4. Under OAuth Scopes, select the two in section 2.

  5. Create the app, then copy its Consumer Key and Consumer Secret.

Salesforce takes up to about ten minutes to make a new or edited app's OAuth settings available everywhere. A connect attempt that fails in the first ten minutes after saving is not a configuration error: wait and try again.

If the app's Permitted Users policy is "Admin approved users are pre-authorized", only users whose profile or permission set is assigned to the app can connect.

2. Scopes

Request the narrowest set that covers the tools this connector exposes (F-PLUG-1: minimum scopes per capability):

  • api — "Manage user data via APIs": the REST API, as the connected user.
  • refresh_token — "Perform requests at any time": stay connected without signing in again.

Salesforce has no read-only API scope — api covers reading and changing records alike — so the plugin sheet shows one sentence for the grant rather than a scope list: Salesforce grants all or nothing, so this account can read and change everything the connected user can. The separation lives in the tools instead: every tool that writes raises an approval card first. What the connected user can see and change is still bounded by their own Salesforce profile and sharing rules.

3. Enter the app

One command, with the client secret on standard input — there is no flag that takes it and no environment variable that holds it (see docs/connectors/README.md):

printf %s "$CLIENT_SECRET" | botseon oauth-app set \
  --org <organisation id> --slug salesforce \
  --client-id <the consumer key from step 1> \
  --auth-url https://<instance>.my.salesforce.com/services/oauth2/authorize \
  --token-url https://<instance>.my.salesforce.com/services/oauth2/token \
  --configured-by <your user id>

Both URLs are the org's own My Domain address — Salesforce recommends it over a generic login host, and it is the same <instance> the plugin variable below names. Use the my.salesforce.com address, never the lightning.force.com one.

It stores the secret encrypted and prints the redirect URL from step 1 back, so you can check the two match. botseon oauth-app show --org <organisation id> --slug salesforce says whether an app is configured and under which client id; nothing prints the secret back.

Then set the plugin's Salesforce instance: the part of your org's address before .my.salesforce.com. For https://acme.my.salesforce.com enter acme; for a Developer Edition at https://acme-dev-ed.develop.my.salesforce.com enter acme-dev-ed.develop; for a sandbox at https://acme--uat.sandbox.my.salesforce.com enter acme--uat.sandbox.

Never put either value in a bot's description, a memory, or anywhere else a model reads as ordinary text.

4. What this connector does and does not do

  • salesforce.soql_query — runs a SOQL SELECT. The query must end with LIMIT n, at most 200, so a tool call reads one page.
  • salesforce.get_record — one record by its id, all fields or the ones named.
  • salesforce.search_records — finds records across up to five objects (Accounts, Contacts and Opportunities by default).
  • salesforce.list_objects — lists the objects the connected user can read, standard and custom, by label and API name (Invoice__c), so a bot does not have to guess a custom object's name. It can narrow the list to custom objects or to a piece of the name.
  • salesforce.describe_object — lists one object's fields with their types, whether the user may edit each one, and a picklist's active values. Salesforce leaves out any field the connected user's field-level security hides, so a bot never sees one. It also names each lookup's relationship (Account__r) and, up to 100, the child relationships a subquery walks (Contacts), so a bot can query across objects.
  • salesforce.create_record — creates one record. Tagged write; the approval card names the object.
  • salesforce.update_record — changes one record's fields. Tagged write; the approval card names the record.

This connector pins Salesforce REST API version 67.0 (Summer '26). Salesforce gives at least a year's notice before retiring a version.

API access depends on the Salesforce edition. It is not available on Starter Suite or Free Suite, is a paid add-on on Pro Suite, and is included from Enterprise Edition up. If calls fail with a message that the REST API is not enabled for the organization, the edition is the cause.

Each org has a rolling 24-hour API allocation shared by every integration it runs; when it is used up, a bot's Salesforce calls pause until the allocation recovers.

Where an org's data is stored — including the Hyperforce EU regions and the paid EU Operating Zone — is chosen when the org is provisioned; it is the org's own address this connector calls either way.

This connector's catalogue row is available: true since 2026-10-21, when its live smoke passed against a real Developer Edition org (connector-live run 39571882193). Members can add it from Plugins as soon as this deployment holds the app from section 3; until then, connecting stops at the missing app.

5. The nightly live test (client credentials)

The connector-live workflow's Salesforce case (packages/connectors/src/salesforce.live.test.ts) gets its access token with the client credentials flow, a fresh token on every run. That is the supported path, for one reason: the test org's External Client App rotates refresh tokens, and Salesforce turns that setting on by default and locks it, so it cannot be unticked. A refresh token stored as a secret therefore works for exactly one run — the refresh hands back a new refresh token the workflow cannot save — and every later run fails with invalid_grant.

The client credentials flow has no refresh token at all. Per Salesforce Help, "OAuth 2.0 Client Credentials Flow for Server-to-Server Integration" (retrieved 2026-10-20), the test POSTs to the org's own My Domain token endpoint:

POST https://<instance>.my.salesforce.com/services/oauth2/token
grant_type=client_credentials&client_id=<consumer key>&client_secret=<consumer secret>

Salesforce does not accept scopes on that request — they come from the app — and the token acts as the app's run-as user, so what the test can read is bounded by that user's profile.

To set it up on the test app: under the app's OAuth settings, enable Client Credentials Flow, and in its policies pick a run-as user (an API-enabled user in the test org). The workflow then needs only SALESFORCE_TEST_INSTANCE and SALESFORCE_TEST_CLIENT_ID (repository variables) and SALESFORCE_TEST_CLIENT_SECRET (a secret). If the flow is refused, the test falls back to SALESFORCE_TEST_REFRESH_TOKEN and then to a stored SALESFORCE_TEST_TOKEN, if either is set; neither is needed any more. No token or secret is ever printed — a failed token request names only the HTTP status and Salesforce's error code.

Last verified against build 331481c.