Skip to docs content
Browse documentation
On this page

Connect Salesforce

Add Salesforce to Minion as a custom OAuth app, allow your My Domain host with a wildcard, and run SOQL from execute or a package.

Connecting Salesforce lets your agents and packages run SOQL queries, read object metadata and work with records through the Salesforce REST API, as the Salesforce user who approved the connection, without the token ever reaching their code.

Minion has no built-in Salesforce preset. You add Salesforce as a custom OAuth app. The one Salesforce-specific wrinkle is the API host, so read "The instance URL" below before you start.

Before you start

  • You need to be an owner or admin of your Minion organization. Members can use an integration but can't add an app or connect one.
  • In Salesforce you need permission to create an External Client App or a Connected App. That's usually a system administrator.
  • Know your org's My Domain URL. Find it in Salesforce under Setup > My Domain. It looks like https://acme.my.salesforce.com, or https://acme--dev.sandbox.my.salesforce.com for a sandbox.
  • Production orgs sign in at login.salesforce.com. Sandboxes sign in at test.salesforce.com. Each one is a separate OAuth app in Minion.

The instance URL

Salesforce doesn't serve the API from the login host. Each org has its own host, and Salesforce returns it as instance_url when you sign in. Minion stores the access and refresh tokens but not instance_url, and there's no field for it. So your code has to know the host.

Minion's API hosts field accepts wildcards. *.my.salesforce.com matches any subdomain at any depth, so it covers acme.my.salesforce.com and acme--dev.sandbox.my.salesforce.com. It doesn't match my.salesforce.com itself, which you never call. One app can therefore reach every My Domain host, and your code supplies which one: pass it as a parameter, or keep it in the package's storage.

If your org's API host isn't under my.salesforce.com, for example a Government Cloud org, add that host to API hosts as well.

Create the app on Salesforce

Salesforce offers External Client Apps and Connected Apps. Use an External Client App if your org lets you. Either works with Minion.

  1. In Setup, open External Client App Manager (or App Manager for a Connected App) and create a new app.
  2. Enable OAuth.
  3. Set the callback URL to exactly:
https://app.minion.codes/oauth/callback
  1. Add these OAuth scopes:
    • Manage user data via APIs (api)
    • Perform requests at any time (refresh_token, offline_access)
  2. Keep Require secret for Web Server Flow on. PKCE can stay on too. Minion sends a PKCE challenge.
  3. Under the app's OAuth policies, set the refresh token policy to Refresh token is valid until revoked if you want the connection to keep working without reconnecting.
  4. Save. Then copy the Consumer Key and Consumer Secret. A new app can take a few minutes before it accepts logins.

Add it in Minion

  1. Open app.minion.codes/app/integrations .
  2. In the OAuth apps card, set Provider to Custom (any OAuth 2 provider).
  3. Set App name (optional) to salesforce. Don't leave it empty: a custom app's name defaults to custom.
  4. Paste the Consumer Key into Client ID and the Consumer Secret into Client secret.
  5. Set Default scopes (optional) to api refresh_token offline_access.
  6. Set API hosts (custom: required) to *.my.salesforce.com.
  7. Open Custom provider endpoints and fill in:
    • Authorization URL: https://login.salesforce.com/services/oauth2/authorize
    • Token URL: https://login.salesforce.com/services/oauth2/token
    • Revocation URL (optional): https://login.salesforce.com/services/oauth2/revoke
    • Client authentication: Secret in the request body
    • Keep Use PKCE (S256) checked.
  8. Click Save app.

For a sandbox, add a second app named salesforce-sandbox and replace login.salesforce.com with test.salesforce.com in all three URLs. The API host can stay *.my.salesforce.com.

You can also use your My Domain URL in place of the login host, for example https://acme.my.salesforce.com/services/oauth2/authorize. That helps when your org blocks logins through login.salesforce.com.

Connect

  1. In the Integrations card, choose salesforce under OAuth app.
  2. Leave Name (optional) empty to call the integration salesforce.
  3. Leave Extra scopes (optional) empty to use the app's default scopes. On a first connect, anything you type here replaces the defaults, so list every scope you need.
  4. Click Connect. Sign in to Salesforce as a user who can see the data your code needs, and approve.

Salesforce sends you back to the Integrations page with Connected "salesforce". and status connected. The account column stays empty for custom apps, and Token expires shows n/a. Salesforce doesn't say when its access tokens expire, so Minion refreshes the token when Salesforce answers 401 and retries the call once.

An agent can start the same flow with minion.integrations.connect({ provider: 'salesforce' }). An owner or admin opens the returned connectUrl while signed in to Minion and approves within 10 minutes.

More than one org: give each connection its own name in Name (optional), such as salesforce-eu. If each org has its own External Client App, add one Minion OAuth app per org.

Check it works

Run this with execute, with params { "instanceUrl": "https://acme.my.salesforce.com" }. It reads your org's API limits and returns no record data.

export default async function main(params: { instanceUrl?: string } = {}) {
  if (!params.instanceUrl) throw new Error('Pass instanceUrl, your My Domain URL.')
  const base = params.instanceUrl.replace(/\/+$/, '')
  const versions = await minion.integrations.fetch({ name: 'salesforce', url: `${base}/services/data/` })
  if (!versions.ok) throw new Error(`Salesforce returned ${versions.status}: ${await versions.text()}`)
  const latest = (await versions.json()).at(-1)
  const limits = await minion.integrations.fetch({ name: 'salesforce', url: `${base}${latest.url}/limits` })
  if (!limits.ok) throw new Error(`Salesforce returned ${limits.status}: ${await limits.text()}`)
  const body = await limits.json()
  return { apiVersion: latest.version, dailyApiRequests: body.DailyApiRequests }
}

An API version and your daily request limit in the result mean the integration, its token and the allowed host all line up.

Use it in a package

In package code, import createAuthenticatedFetch from minion:runtime. Keep the My Domain URL in the package's storage so callers don't pass it every time. Package storage is reachable from the package's own code, so the package gets a small second export that saves it.

package.json:

{
  "name": "@your-org/salesforce-tools",
  "exports": { "./query": "./src/query.ts", "./set-instance": "./src/set-instance.ts" }
}

src/set-instance.ts:

import { packageStorage } from 'minion:runtime'

export default async function main({ instanceUrl }) {
  await packageStorage().set('instanceUrl', instanceUrl.replace(/\/+$/, ''))
  return { saved: true }
}

src/query.ts:

import { createAuthenticatedFetch, packageStorage } from 'minion:runtime'

const salesforce = createAuthenticatedFetch('salesforce')

export default async function main({ soql, apiVersion = 'v62.0' }) {
  const instanceUrl = await packageStorage().get('instanceUrl')
  if (!instanceUrl) throw new Error('Run set-instance with your My Domain URL first.')
  const url = `${instanceUrl}/services/data/${apiVersion}/query?q=${encodeURIComponent(soql)}`
  const res = await salesforce(url)
  if (!res.ok) throw new Error(`Salesforce returned ${res.status}: ${await res.text()}`)
  return await res.json()
}

Save and publish it with minion.packages.save and publish: true. Publishing needs an owner or admin and a README.md that says the package needs the salesforce integration and a one-time set-instance call. Other code can only import a published version. Then call it from execute:

import setInstance from 'minion:@your-org/salesforce-tools/set-instance'
import query from 'minion:@your-org/salesforce-tools/query'

export default async function main() {
  await setInstance({ instanceUrl: 'https://acme.my.salesforce.com' })
  return query({ soql: 'SELECT Id, Name FROM Account LIMIT 5' })
}

After the first run you can drop the setInstance line. Replace your-org with your organization slug. See /docs/packages for saving and publishing.

Troubleshooting

  • redirect_uri_mismatch. The callback URL on the Salesforce app must be exactly https://app.minion.codes/oauth/callback. No trailing slash.
  • invalid_client_id right after you created the app. Salesforce hasn't finished setting it up. Wait a few minutes and click Connect again.
  • invalid_client or a secret error. The Consumer Secret is wrong or was regenerated. In Minion, open Manage on the app, paste the new secret and click Rotate. Then connect again.
  • Sandbox login fails. The app still points at login.salesforce.com. Use a separate app with test.salesforce.com URLs.
  • An error saying integration "salesforce" may not send its token to login.salesforce.com. You called the login host. API calls go to your My Domain host.
  • An error saying it may not send its token to some other host. Your org's API host isn't under my.salesforce.com. Add it to API hosts: submit the OAuth apps form again with the same app name, the client ID, all three endpoints and the full host list. A blank client secret keeps the current one.
  • It works for a while, then the integration turns error. The app didn't grant a refresh token, or its refresh token policy expired it. Minion fires an integration.auth.failed event with a reconnectUrl. Check that refresh_token and offline_access are in the app's scopes and the policy is valid until revoked. Then click Connect next to the integration to reconnect. A package can subscribe to integration.auth.failed to alert you.
  • INVALID_SESSION_ID that doesn't clear. The user's session or the grant was revoked in Salesforce. Reconnect.
  • Login blocked by IP restrictions. Minion calls Salesforce from its own servers. Relax IP restrictions in the app's OAuth policies, or the org's IP rules reject it.

Disconnecting the integration in Minion revokes the grant at Salesforce through the Revocation URL, then deletes the token in Minion.

For how OAuth integrations work in general, see /docs/oauth . To run code, see /docs/execute .