← Agent Frameworks 🕐 25 min read
Agent Frameworks

Salesforce Automation via CLI, MCP, and AI Agents (2026)

Salesforce automation has undergone a structural shift in 2025–2026.

Salesforce automation has undergone a structural shift in 2025–2026. What was once a patchwork of sfdx scripts, middleware integrations, and brittle Selenium automation has converged into a coherent programmatic stack: the sf CLI for headless DevOps, a native MCP server layer for LLM tool calls, REST/Bulk APIs with JWT auth for data pipelines, and a growing ecosystem of AI agent frameworks that treat Salesforce as a set of callable tools rather than a UI to be scraped.

The inflection point was Salesforce’s Headless 360 initiative, announced at TrailheaDX 2026 (April 2026), which made explicit what practitioners had been doing piecemeal: the entire platform — CRM, Agentforce, Data Cloud, Slack — is now addressable as APIs, MCP tools, and CLI commands without ever opening a browser. Simultaneously, the Anthropic–Salesforce strategic partnership (announced October 2025, expanded February 2026) made Claude a first-class consumer of Salesforce MCP tools, with every Developer Edition org now shipping with hosted MCP servers and Claude Sonnet as the default coding model.

This note covers each layer of the automation stack with practitioner-level specificity: what works, what breaks, and where the real friction lives.


1. Salesforce CLI (sf / sfdx)

1.1 Migration State: sfdx → sf

The sfdx executable is deprecated. As of June 2024, Salesforce removed sfdx-style command reference documentation. All new automation should use the sf executable with sf-style commands. The two coexist on installed systems but diverge in output schema; scripts parsing sfdx JSON output will need updates.

Migration reference: sf migrate maps old commands to equivalents. The most common are:

Old (sfdx) New (sf)
sfdx force:org:login:web sf org login web
sfdx force:data:soql:query sf data query
sfdx force:source:deploy sf project deploy start
sfdx force:apex:test:run sf apex test run
sfdx force:mdapi:retrieve sf project retrieve start

1.2 What the CLI Can Do Headlessly

The sf CLI exposes 220+ commands across these functional areas relevant to automation:

Authentication

  • sf org login jwt — JWT Bearer flow; takes a certificate key file and connected app consumer key; no browser required; the correct auth method for CI/CD and agent pipelines.
  • sf org login access-token — inject a pre-obtained access token directly; useful in container environments where the JWT private key is mounted as a secret.

Data Operations

  • sf data query --query "SELECT Id, Name FROM Account LIMIT 100" --result-format json — SOQL query, returns structured JSON; pipe to jq or parse directly in scripts.
  • sf data export tree --query "SELECT ..." --output-dir ./export — exports records and relationships as JSON files for seeding, backup, or migration.
  • sf data upsert bulk --sobject Account --file accounts.csv --external-id ExternalId__c — wraps Bulk API 2.0 for large-volume upsert; handles job lifecycle (create → upload → close → poll → results) automatically.
  • sf data delete bulk --sobject Lead --query "SELECT Id FROM Lead WHERE Status = 'Unqualified'" — bulk delete via query.

Metadata & Deployment

  • sf project deploy start --source-dir force-app/main/default — deploys source-format metadata; supports --dry-run for validation.
  • sf project retrieve start --metadata ApexClass:MyClass — targeted retrieve by type and name.
  • sf package create, sf package version create — managed/unlocked package lifecycle, scriptable end-to-end.

Apex Execution

  • sf apex run --file scripts/setup.apex — anonymous Apex execution; useful for data seeding, configuration, and one-off operations that need Apex governor access.
  • sf apex test run --class-names MyTest --result-format json — runs specific test classes; returns pass/fail with coverage JSON.

Agentforce (GA in Summer '26)

  • sf agent preview start — starts an interactive agent test session programmatically.
  • sf agent preview send --message "Summarize open opportunities for ACME Corp" — sends a message to the active session.
  • sf agent preview end — terminates the session and returns transcript JSON.

This makes end-to-end Agentforce regression testing scriptable from a CI pipeline for the first time.

1.3 Security Breaking Change: Credentials in CLI Output (May 2026)

Critical for existing scripts. Starting May 27, 2026, sf removed access tokens, passwords, and SFDX Auth URLs from standard CLI output, including --json outputs. Scripts that grep for sfdxAuthUrl or parse accessToken out of sf org display --json will silently fail to get credentials after this date.

The migration path:

  • Use sf org display --verbose only when explicitly needed in a dedicated step; output goes to a dedicated secure channel.
  • In CI/CD pipelines, mount the JWT private key as a secret and authenticate freshly per run rather than reusing captured tokens.
  • The temporary env var workaround (SF_ACCESS_TOKEN) is also decommissioned in Summer '26.

1.4 JSON-First Scripting Pattern

The CLI is designed for machine consumption via --json. Always use it in scripts:

# Example: query and extract Account IDs into a file
sf data query \
  --query "SELECT Id, Name, AnnualRevenue FROM Account WHERE AnnualRevenue > 1000000" \
  --result-format json \
  --json | jq -r '.result.records[].Id' > account_ids.txt

The --json flag wraps every command output in a consistent envelope:

{
  "status": 0,
  "result": { ... },
  "warnings": []
}

Non-zero status indicates failure; parse before trusting result. Error details appear in result.message and result.stack.

1.5 CI/CD Integration Patterns

Standard patterns for GitHub Actions / CircleCI / Jenkins:

  1. Store the JWT private key as a base64-encoded secret (SF_PRIVATE_KEY_BASE64).
  2. Decode at runtime: echo $SF_PRIVATE_KEY_BASE64 | base64 -d > server.key.
  3. Auth: sf org login jwt --client-id $SF_CLIENT_ID --jwt-key-file server.key --username $SF_USERNAME --set-default.
  4. Proceed with deploy/test/data operations.
  5. The org alias is scoped to the process; no global keychain pollution.

For sandbox refreshes and scratch org creation (common in automated testing pipelines):

sf org create scratch --definition-file config/project-scratch-def.json --alias ci-org --duration-days 1
sf project deploy start --target-org ci-org
sf apex test run --target-org ci-org --result-format json --code-coverage
sf org delete scratch --target-org ci-org --no-prompt

2. MCP Servers for Salesforce

2.1 Architecture Overview

Model Context Protocol (MCP) is the transport layer that exposes Salesforce capabilities as callable tools for LLMs. An MCP server runs as a local process (stdio transport) or a remote HTTP endpoint (SSE/streamable HTTP transport); an MCP client (Claude Desktop, Claude Code, Cursor, Windsurf, Codex) connects and calls tools by name with structured arguments.

For Salesforce, the ecosystem has split into three tiers: Salesforce’s official hosted MCP servers (Headless 360), the official DX MCP server for development workflows, and community-built servers for specific integration needs.

2.2 Official Salesforce MCP Servers (Headless 360)

Announced at TDX 2026 and reaching GA/Developer Preview through Summer '26, Headless 360 is the umbrella brand for Salesforce’s native MCP offering.

Salesforce DX MCP Server (@salesforce/mcp)

The primary development-focused server. Exposes 60+ tools organized into named toolsets:

Toolset Sample Tools
metadata retrieve_metadata, deploy_metadata, list_metadata_types
data run_soql_query, run_sosl_search, upsert_record, delete_record
testing run_apex_tests, get_test_coverage, run_agent_test
lwc create_lwc_component, lint_lwc, preview_lwc
devops create_scratch_org, create_package_version, deploy_pipeline_stage
analysis analyze_apex_class, detect_security_issues, explain_flow
agentforce create_agent, test_agent, deploy_agent_to_production

Configuration in Claude Code’s ~/.claude/claude_desktop_config.json:

{
  "mcpServers": {
    "salesforce-dx": {
      "command": "npx",
      "args": ["@salesforce/mcp", "--toolsets", "metadata,data,testing,devops"],
      "env": {
        "SF_TARGET_ORG": "my-sandbox-alias"
      }
    }
  }
}

With this configured, a prompt like “Deploy the AccountTrigger class to my sandbox and run its tests” executes as a multi-tool call: retrieve_metadatadeploy_metadatarun_apex_tests → result.

Data 360 MCP Server

Released as open source in Developer Preview (May 2026). Connects Data Cloud APIs to any MCP client via stdio transport. Uses a “facade tool architecture” — roughly 200 API operations are exposed through a smaller set of meta-tools that use semantic routing internally, avoiding context window exhaustion.

Primary use case: natural language queries against Data Cloud unified profiles and calculated insights without writing SOQL/SQL directly.

Tableau MCP Server

Exposes Tableau REST API operations — publishing workbooks, querying data sources, triggering refreshes — as MCP tools. Relevant for automation pipelines that need to push fresh data and trigger dashboard refreshes without a human login.

MuleSoft MCP Server

Exposes API catalog discovery and runtime invocation. An LLM can ask “what APIs exist for order management” and get back a structured catalog, then invoke specific endpoints.

2.3 Community MCP Servers

Several community implementations precede and supplement the official servers. These are more useful for pure CRM data operations (CRUD on standard and custom objects) vs. the DX server’s development focus.

jpmonette/salesforce-mcp (GitHub)

  • Minimal implementation using jsforce (Node.js Salesforce library).
  • Tools: SOQL query, record CRUD on any SObject, describe object schema.
  • Auth: username/password + security token, or connected app OAuth. Minimal overhead to set up.
  • Best for: quick CRM read/write from Claude Desktop without the full DX stack.

tsmztech/mcp-server-salesforce (GitHub)

  • More complete implementation; exposes metadata operations alongside data operations.
  • Tools include: query_salesforce, create_record, update_record, delete_record, describe_object, list_objects, get_metadata, deploy_metadata.
  • Handles both production and sandbox via configurable SF_LOGIN_URL.
  • Auth: OAuth 2.0 with username/password flow or JWT Bearer.

advancedcommunities/salesforce-mcp-server (GitHub)

  • Drives Salesforce via the sf CLI rather than direct API calls.
  • Useful when the operator already has authenticated sf sessions and doesn’t want to manage separate credentials for the MCP server.
  • Tools: SOQL via sf data query, Apex execution via sf apex run, metadata deploy/retrieve, org info.
  • Limitation: slower than direct API calls because each tool call spawns a CLI subprocess.

smn2gnt/MCP-Salesforce (GitHub)

  • Focused specifically on SOQL and SOSL search operations.
  • Minimal footprint; useful as a read-only data access layer for LLMs that need CRM context without write access.

forcedotcom/mobile-mcp-tools (GitHub)

  • Official Salesforce repository for mobile-specific MCP tools.
  • Covers mobile app testing, Mobile SDK integration, and push notification testing.
  • More niche; relevant for teams building Salesforce mobile apps.

2.4 What LLMs Can Do via MCP

With a Salesforce MCP server connected, an LLM can execute multi-step workflows in a single conversation turn without the human writing any SOQL or API calls:

CRM data retrieval:

“Show me all open opportunities over $500K that haven’t had activity in 30 days”

Resolves to: run_soql_querySELECT Id, Name, Amount, LastActivityDate FROM Opportunity WHERE StageName != 'Closed Won' AND StageName != 'Closed Lost' AND Amount > 500000 AND LastActivityDate < LAST_N_DAYS:30

Record enrichment:

“Update the Industry field on all Accounts in the Technology sector that are missing it, using the Billing Address to infer”

Resolves to: run_soql_query to find candidates → multiple update_record calls with inferred values.

Deployment workflow:

“Create a new scratch org, deploy the feature/dashboard-redesign branch metadata to it, run all tests, and report back”

Resolves to: create_scratch_orgdeploy_metadatarun_apex_tests → result summary.

Schema discovery:

“What custom fields exist on the Lead object that are not on any page layout?”

Resolves to: describe_object for Lead → get_metadata for page layouts → diff and report.


3. Browser-Use and Computer-Use Automation

3.1 When Browser Automation Is Necessary

The Salesforce API surface is comprehensive but not complete. Operations that have no API equivalent as of mid-2026:

  • Certain Setup menu configurations (custom settings UI-only paths, some security controls)
  • Report Builder interactive configuration (reports themselves can be queried via Analytics API, but creation through the UI has no equivalent)
  • Lightning App Builder drag-and-drop layout configuration
  • Some Einstein-specific setup flows
  • Trailhead / myTrailhead administrative tasks
  • Org-level UI configuration that predates API parity (legacy console setups)

For these, browser automation is the only programmatic path.

3.2 Playwright for Salesforce

Playwright is the baseline for Salesforce browser automation in 2026. Key characteristics that matter for Salesforce specifically:

Shadow DOM handling. Salesforce Lightning (LWC) uses Shadow DOM to encapsulate component internals. Playwright pierces shadow boundaries natively with locator.shadow() and page.locator('selector >> css=inner-selector'). This is the primary reason older Selenium-based automation broke on Lightning — Selenium required explicit shadow root traversal; Playwright handles it transparently.

Salesforce is migrating from Synthetic Shadow DOM (a polyfill) to Native Shadow DOM. Summer '26 includes components in “mixed shadow mode” (Beta). This migration can break automation that relied on synthetic shadow’s slightly more permissive traversal. Test against native shadow in a Developer Edition before it reaches production.

Authentication. The cleanest pattern for headless Playwright against Salesforce:

  1. Use the REST API to get an OAuth access token (JWT Bearer flow, no browser needed).
  2. Construct the Salesforce session URL: https://<instance>.salesforce.com/secur/frontdoor.jsp?sid=<access_token>.
  3. Navigate to this URL in Playwright — Salesforce recognizes the session and drops a valid cookie.
  4. Proceed with UI interactions; no username/password form handling required.

This avoids MFA prompts, login page variability, and the SF_USE_ADVANCED_API_SECURITY_FOR_HEADLESS_LOGIN requirement for direct username/password flows.

Practical script skeleton:

import asyncio
import requests
from playwright.async_api import async_playwright

async def salesforce_session(access_token: str, instance_url: str):
    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=True)
        context = await browser.new_context()
        page = await context.new_page()
        # Session injection — bypasses MFA
        await page.goto(f"{instance_url}/secur/frontdoor.jsp?sid={access_token}")
        await page.wait_for_url("**/lightning/**")
        # Proceed with UI automation
        return page, browser

Locator strategy for Lightning components. Lightning component selectors often look like c-custom-modal lightning-input[data-field-name="Email"]. The reliable pattern:

  • Prefer data-* attributes and aria-label over CSS class-based selectors; Lightning generates random class names on re-render.
  • Use page.get_by_role() and page.get_by_label() — these survive UI refactors better than brittle CSS paths.
  • For inline-edit on list views and record pages, wait for [data-target-reveals] attributes to appear rather than timing out.

3.3 Browser-Use Python Library

The browser-use library (50,000+ GitHub stars as of 2026) wraps Playwright with an LLM-driven agent loop: the model observes the DOM state, decides the next action, executes it via Playwright, and repeats until the task is complete. This is the closest thing to a general-purpose Salesforce UI agent without custom code.

Pattern for Salesforce:

from browser_use import Agent
from langchain_anthropic import ChatAnthropic

async def run_salesforce_task(task: str, salesforce_url: str):
    agent = Agent(
        task=task,
        llm=ChatAnthropic(model="claude-sonnet-4-5"),
        browser_config={"headless": True}
    )
    # Pre-seed session cookie
    await agent.browser.navigate(f"{salesforce_url}/secur/frontdoor.jsp?sid={access_token}")
    result = await agent.run()
    return result

Strengths: Handles tasks that resist hand-coded automation — navigating unfamiliar page structures, recovering from unexpected modal dialogs, interpreting confirmation screens. Good for one-off administrative tasks.

Limitations:

  • Expensive: each action requires an LLM call. A 10-step task might cost $0.05–0.20 depending on model and DOM complexity.
  • Non-deterministic: the model may choose different paths on retries; unsuitable for high-volume production pipelines.
  • Session stability: long-running tasks (>30 steps) risk Salesforce session timeout; build in checkpoint logic.
  • Shadow DOM: as of 2026, browser-use passes the DOM tree as text context to the LLM; Shadow DOM boundaries may cause the model to miss elements. Workaround: inject a DOM-flattening script via page.evaluate() before the agent loop starts.

3.4 AgentQL

AgentQL provides a query language for AI agents to extract structured data from web pages without CSS selectors or XPath. For Salesforce specifically:

import agentql

page = agentql.wrap(playwright_page)
# Natural language element selection
result = await page.query_elements("""
{
  opportunity_table {
    rows[] {
      name
      stage
      amount
      close_date
    }
  }
}
""")

AgentQL resolves the query semantically — it figures out which DOM elements correspond to “opportunity_table” and “amount” rather than requiring the developer to know the CSS structure. This is particularly useful for Salesforce because Lightning component class names change between releases.

Current limitation: AgentQL’s semantic resolution calls its own API, adding ~200–500ms latency per query. For automation scripts running thousands of queries, this is significant. Use it for low-frequency, high-brittleness scenarios; use explicit Playwright locators for high-volume paths.

3.5 Stagehand

Stagehand (Browserbase) is a higher-level browser automation framework that uses LLM function calls to drive Playwright. Three primitives:

  • act() — perform an action described in natural language: await page.act("Click the New Opportunity button")
  • extract() — extract structured data: await page.extract({ schema: OpportunitySchema, instruction: "Extract all opportunities in the list" })
  • observe() — get possible actions on the current page

For Salesforce, extract() is particularly valuable — it handles the Shadow DOM complexity internally and returns typed data without requiring the developer to write any selectors.

Stagehand vs. browser-use for Salesforce:

  • Stagehand is faster at extraction tasks (single LLM call per extract()); browser-use is better at multi-step navigation tasks.
  • Stagehand integrates with Browserbase’s hosted browser infrastructure, eliminating the need to manage Chromium instances in production.
  • browser-use has a more active community and more Salesforce-specific examples in the wild.

4. Salesforce APIs for Automation

4.1 API Landscape Summary

API Best For Max Volume Auth
REST API CRUD, <10K records, ad-hoc queries 15K concurrent connections OAuth 2.0 all flows
Bulk API 2.0 Mass insert/upsert/delete/query 100M records/24h OAuth 2.0 all flows
Streaming API (CometD) Real-time change events, CDC Subscription-based OAuth 2.0 all flows
Metadata API Deploying/retrieving org configuration Per-org deployment slots OAuth 2.0 all flows
Tooling API Apex execution, code coverage, debug Org governor limits OAuth 2.0 all flows
Connect API Chatter, Experience Cloud, Files Rate-limited per endpoint OAuth 2.0 all flows
Analytics API Reports, dashboards, Einstein Discovery Report-specific OAuth 2.0 all flows

SOAP login() retires in Summer '27 (API versions 31.0–64.0). Any integration using username/password over SOAP will break. Migrate to OAuth JWT Bearer before the cutoff.

4.2 OAuth 2.0 JWT Bearer Flow (Headless Standard)

This is the canonical auth pattern for any automated system. No user interaction, no refresh token rotation, certificate-based.

Setup (one-time, done by Salesforce admin):

  1. Create a Connected App in Salesforce Setup.
  2. Enable OAuth; add scopes: api, refresh_token (omit for pure JWT), offline_access.
  3. Generate an RSA key pair (openssl req -x509 -newkey rsa:2048 -keyout server.key -out server.crt -days 365 -nodes).
  4. Upload server.crt to the Connected App’s “Use Digital Signatures” field.
  5. Pre-authorize the integration user: Setup → Connected App → “Manage” → “Edit Policies” → “Permitted Users: Admin approved users are pre-authorized”.
  6. Assign the Connected App permission to the integration user profile or permission set.

Runtime flow:

  1. Build a JWT: header ({"alg":"RS256","typ":"JWT"}) + payload ({"iss": "<consumer_key>", "sub": "<integration_username>", "aud": "https://login.salesforce.com", "exp": <now+300>}).
  2. Sign with private key using RS256.
  3. POST to https://login.salesforce.com/services/oauth2/token with grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=<signed_jwt>.
  4. Response: {"access_token": "...", "instance_url": "https://yourorg.salesforce.com", "token_type": "Bearer"}.
  5. Use access_token as Authorization: Bearer <token> on subsequent API calls.

Access tokens expire after the Connected App’s session timeout (default 2 hours; configurable). Re-run the JWT flow to get a fresh token — there is no refresh token in the JWT Bearer flow by design.

Python implementation using simple-salesforce or jsforce handles this automatically if you provide the certificate path and consumer key.

4.3 REST API Practical Limits

  • Daily limit: 100,000 + (1,000 × Enterprise users) + (5,000 × Unlimited users) API calls per 24-hour rolling window. This pool is shared by REST, SOAP, Bulk API, Connect API.
  • Concurrent long-running requests: 25 simultaneous requests lasting ≥20 seconds in production; 5 in Developer Edition.
  • HTTP 403 REQUEST_LIMIT_EXCEEDED: returned when daily limit is hit; no retry will succeed until the window rolls. Build circuit-breaker logic with the /services/data/v62.0/limits endpoint — check DailyApiRequests.Remaining before starting high-volume operations.
  • Composite API: bundle up to 25 subrequests in a single HTTP call. Use services/data/v62.0/composite to update parent + child records in one round trip. This is the most practical way to reduce API call consumption in agent pipelines.
  • sObject Collections: update/insert up to 200 records per call with /services/data/v62.0/composite/sobjects. Far more efficient than looping individual record updates from an agent.

4.4 Bulk API 2.0

The correct choice for any operation involving >10,000 records or for agents that might trigger large data exports/imports.

Job lifecycle:

  1. Create job: POST /services/data/v62.0/jobs/ingest with {"object": "Account", "operation": "upsert", "externalIdFieldName": "ExternalId__c", "contentType": "CSV"}
  2. Upload CSV: PUT /services/data/v62.0/jobs/ingest/<jobId>/batches with CSV body.
  3. Close job: PATCH /services/data/v62.0/jobs/ingest/<jobId> with {"state": "UploadComplete"}.
  4. Poll state: GET /services/data/v62.0/jobs/ingest/<jobId> until state is JobComplete or Failed.
  5. Retrieve results: GET /services/data/v62.0/jobs/ingest/<jobId>/successfulResults and .../failedResults.

Key limits:

  • 100 million records per rolling 24 hours.
  • 150 million records if counting individual Bulk API 2.0 HTTP requests.
  • Maximum 10,000 records per batch in Bulk API v1 (superseded); no per-batch limit in Bulk API 2.0 — CSV size limits apply (150MB per upload request).
  • Set columnDelimiter: PIPE if data contains commas.

4.5 Streaming API / Change Data Capture

For agent pipelines that need to react to Salesforce data changes in real time (vs. polling):

  • Change Data Capture (CDC): Subscribe to /data/AccountChangeEvent or any enabled SObject’s change channel. Receives create/update/delete/undelete events within seconds.
  • Platform Events: Custom pub/sub events. Producers publish via REST API; consumers subscribe via CometD or Apex triggers. Useful for triggering agent workflows from Salesforce-side events.
  • PushTopic Streaming (legacy): Being superseded by CDC; avoid for new implementations.

CometD client libraries exist for Python (cometd-asyncio), Node.js (faye), and Java. The Bayeux handshake requires an active OAuth token; implement token refresh logic for long-running subscriptions.


5. AI Agent Patterns

5.1 LangChain + Salesforce

The langchain-salesforce community integration package exposes Salesforce as a set of LangChain tools. The typical setup:

from langchain_anthropic import ChatAnthropic
from langchain_salesforce import SalesforceTool, SalesforceToolkit
from langchain.agents import create_tool_calling_agent, AgentExecutor
from langchain_core.prompts import ChatPromptTemplate

# Initialize toolkit
toolkit = SalesforceToolkit.from_env()  # reads SF_USERNAME, SF_PASSWORD, SF_SECURITY_TOKEN
tools = toolkit.get_tools()
# Tools: query_records, create_record, update_record, delete_record, 
#        describe_object, run_apex, get_object_list

llm = ChatAnthropic(model="claude-opus-4-5")
prompt = ChatPromptTemplate.from_messages([
    ("system", "You are a Salesforce assistant. Use tools to answer questions about CRM data."),
    ("human", "{input}"),
    ("placeholder", "{agent_scratchpad}")
])

agent = create_tool_calling_agent(llm, tools, prompt)
executor = AgentExecutor(agent=agent, tools=tools, verbose=True)

result = executor.invoke({"input": "Find all Accounts in California with no open Opportunities"})

Limitations of LangChain + Salesforce in production:

  • The community toolkit uses username/password auth, not JWT Bearer. Blocked by MFA-enforced profiles.
  • No Bulk API support; large data tasks will exhaust REST limits.
  • No streaming; the agent waits for full SOQL results before continuing.
  • Tool call errors from Salesforce (e.g., SOQL syntax errors) propagate as raw exception text; the LLM may retry with a malformed query in a loop.

5.2 LangGraph for Multi-Step Salesforce Workflows

LangGraph provides stateful, graph-based orchestration. The right choice when the Salesforce automation involves decision branches, human-in-the-loop approval, or multiple coordinated agents.

Architecture pattern for a lead enrichment pipeline:

from langgraph.graph import StateGraph, END
from typing import TypedDict, List

class LeadEnrichmentState(TypedDict):
    lead_ids: List[str]
    raw_leads: List[dict]
    enriched_leads: List[dict]
    failed_leads: List[str]
    updated_count: int

# Node definitions
async def fetch_leads(state):
    """Query Salesforce for unenriched leads"""
    result = await sf.query("SELECT Id, FirstName, LastName, Company, Email FROM Lead WHERE Industry = null LIMIT 100")
    return {"raw_leads": result["records"]}

async def enrich_with_llm(state):
    """Use LLM to infer missing fields"""
    ...

async def update_salesforce(state):
    """Bulk update enriched records"""
    ...

async def handle_failures(state):
    """Log failures, create tasks for manual review"""
    ...

# Graph
graph = StateGraph(LeadEnrichmentState)
graph.add_node("fetch", fetch_leads)
graph.add_node("enrich", enrich_with_llm)
graph.add_node("update", update_salesforce)
graph.add_node("handle_failures", handle_failures)

graph.add_edge("fetch", "enrich")
graph.add_conditional_edges("enrich", route_by_confidence, {
    "high": "update",
    "low": "handle_failures"
})
graph.add_edge("update", END)
graph.set_entry_point("fetch")

LangGraph’s key advantage here: if the update node hits a Salesforce API limit, the graph can checkpoint the state and resume after the window resets — without losing progress on the already-enriched leads.

5.3 Claude with Salesforce Function Tools (Direct API Pattern)

For operators who want to use Claude directly (via the Anthropic API) without LangChain overhead, the tool use pattern gives precise control:

import anthropic
import simple_salesforce as sf_lib

tools = [
    {
        "name": "soql_query",
        "description": "Execute a SOQL query against Salesforce and return results as JSON",
        "input_schema": {
            "type": "object",
            "properties": {
                "query": {"type": "string", "description": "The SOQL query to execute"}
            },
            "required": ["query"]
        }
    },
    {
        "name": "update_record",
        "description": "Update fields on a Salesforce record",
        "input_schema": {
            "type": "object",
            "properties": {
                "sobject": {"type": "string"},
                "record_id": {"type": "string"},
                "fields": {"type": "object", "description": "Field name to value mapping"}
            },
            "required": ["sobject", "record_id", "fields"]
        }
    }
]

def execute_tool(tool_name, tool_input, sf):
    if tool_name == "soql_query":
        return sf.query_all(tool_input["query"])
    elif tool_name == "update_record":
        getattr(sf, tool_input["sobject"]).update(
            tool_input["record_id"], 
            tool_input["fields"]
        )
        return {"success": True}

# Agentic loop
client = anthropic.Anthropic()
messages = [{"role": "user", "content": "Close all opportunities in 'Needs Analysis' stage that haven't had activity in 90 days as Lost, with reason 'Stale - no activity'"}]

while True:
    response = client.messages.create(
        model="claude-opus-4-5",
        max_tokens=4096,
        tools=tools,
        messages=messages
    )
    
    if response.stop_reason == "end_turn":
        break
    elif response.stop_reason == "tool_use":
        tool_results = []
        for block in response.content:
            if block.type == "tool_use":
                result = execute_tool(block.name, block.input, sf)
                tool_results.append({
                    "type": "tool_result",
                    "tool_use_id": block.id,
                    "content": str(result)
                })
        messages.append({"role": "assistant", "content": response.content})
        messages.append({"role": "user", "content": tool_results})

This pattern is production-grade and gives full control over error handling, retry logic, and logging. The agent will naturally use multiple tool calls — first a SOQL query to identify stale opportunities, then a loop of update_record calls, then optionally a follow-up query to confirm the updates.

5.4 Composio Salesforce Integration

Composio provides a managed integration layer that exposes 250+ Salesforce actions as pre-built tools callable from LangChain, Claude, OpenAI, and other frameworks. The advantage over hand-rolling tool definitions is that Composio handles:

  • OAuth credential management and refresh
  • Tool definition schema generation from Salesforce object describe
  • Rate limit awareness and retry logic
from composio_langchain import ComposioToolSet, App

toolset = ComposioToolSet()
tools = toolset.get_tools(apps=[App.SALESFORCE])
# Returns 250+ tools: SALESFORCE_QUERY_RECORDS, SALESFORCE_CREATE_LEAD, 
# SALESFORCE_UPDATE_OPPORTUNITY, SALESFORCE_CREATE_TASK, etc.

The trade-off: Composio routes tool calls through its own API, adding latency and a data processing dependency. Not appropriate for orgs with strict data residency requirements.


6. Real-World Examples and Community Patterns

6.1 Documented Agent Implementations

LangChain + Salesforce + Groq (Salesforce Ben tutorial, 2025) A public walkthrough showing a conversational agent that answers natural language questions about Salesforce data using LangChain’s agent framework with Groq as the LLM provider. Pattern: user asks question → agent generates SOQL → queries Salesforce → formats response. Demonstrates the baseline but uses username/password auth (not production-appropriate).

Orchestrating Salesforce with LangGraph + OpenAI (Agentic Trails, September 2025) Production-oriented example using LangGraph’s StateGraph to manage a multi-step opportunity update workflow. Key contribution: showing how to handle Salesforce’s UNABLE_TO_LOCK_ROW errors (record-level locking conflicts common in high-concurrency automation) with graph-level retry nodes rather than tight loops.

Claude Code + Salesforce API patterns (SyncGTM, 2026) A series of practitioner posts documenting Claude Code connected to Salesforce via MCP for pipeline management tasks: enriching contact records, running lead scoring queries, generating pipeline reports from SOQL in plain English. Demonstrates the MCP pattern without requiring the operator to write any Salesforce API code.

Salesforce Headless 360 + RevOps Automation (MindStudio, 2026) Documents how RevOps teams are using the Headless 360 MCP servers to automate tasks that previously required Salesforce admins: reassigning leads from departed reps, bulk-updating account tiers from enrichment data, creating follow-up tasks from email thread analysis.

6.2 GitHub Repositories of Note

  • salesforcecli/mcp — Official Salesforce DX MCP server. The canonical reference for the official tool schema.
  • jpmonette/salesforce-mcp — Minimal jsforce-based MCP; good starting point for CRM-only use cases.
  • tsmztech/mcp-server-salesforce — Covers metadata + data; more complete than jpmonette for mixed workloads.
  • advancedcommunities/salesforce-mcp-server — CLI-backed MCP server; simplest auth story if you already have authenticated sf sessions.
  • forcedotcom/data-cloud-mcp — Official Data 360 MCP server (open-source Developer Preview).
  • forcedotcom/mobile-mcp-tools — Official mobile MCP tools; niche but maintained.

6.3 Emerging Pattern: Claude in Slack → Salesforce

The Anthropic–Salesforce partnership delivered a concrete artifact: Claude inside Slack with Salesforce context. As of February 2026:

  • Sales reps can ask Claude in Slack: “Summarize my ACME Corp account and list open tasks” — Claude queries Salesforce via MCP and returns a structured summary.
  • Managers can request pipeline reports in natural language inside Slack channels — Claude generates SOQL, runs it against the org, and formats the results.
  • Deal-room Slack channels can have Claude automatically log key decisions as Salesforce Activities.

This pattern matters because it eliminates the context switch between communication (Slack) and CRM (Salesforce). Practitioners are reporting that reps update Salesforce more consistently when the update happens via a Slack message to Claude than when it requires opening Salesforce directly.


7. Gotchas and Production Considerations

7.1 Connected App Setup Requirements

Every programmatic integration requires a Connected App. The most common automation breakdowns trace back to Connected App misconfiguration:

OAuth scopes must be explicitly granted. Common scopes needed for automation:

  • api — access to REST, SOAP, Bulk APIs.
  • refresh_token / offline_access — needed for refresh token grant flows (not JWT Bearer).
  • web — needed if you’re using the frontdoor.jsp session injection approach for browser automation.
  • full — supersets api; use cautiously (grants all permissions the running user has).
  • cdp_query_api / cdp_profile_api — needed for Data Cloud access.

Pre-authorization requirement for JWT Bearer. Without the “Admin approved users are pre-authorized” policy enabled and the integration user explicitly assigned to the Connected App, the JWT Bearer flow will return invalid_grant with no further explanation. This is the most common JWT setup failure.

IP Relaxation vs. Trusted IP Ranges. If you set Trusted IP Ranges on a Connected App (whitelisting specific IP blocks), the app will reject connections from any IP outside those ranges regardless of valid OAuth tokens. For agents running in cloud environments with dynamic IPs (Lambda, Cloud Run, GitHub Actions), either set IP Relaxation to “Relax IP Restrictions” or use Continuous IP Enforcement, which enforces IP ranges only at login, not on token refresh.

7.2 Connected App Usage Restrictions (September 2025)

Starting September 2025, uninstalled Connected Apps are blocked by default. Any integration using a Connected App that isn’t installed in the org will fail with INACTIVE_APP errors. Resolution:

  1. Install the Connected App in the org (go to AppExchange listing or install from package URL).
  2. Or assign the “Approve Uninstalled Connected Apps” or “Use Any API Client” permission to the integration user.

This change silently broke a large number of legacy integrations that were using Connected Apps from third-party tools without explicitly installing them.

7.3 API Governor Limits

The shared pool problem. REST API, SOAP API, Bulk API, and Connect API draw from the same daily call pool. An Apex trigger that makes an outbound callout per-record consumes the same pool as your agent pipeline. Audit all API consumers in the org before deploying agents that make high volumes of calls.

Identify limit consumption. Use the Limits endpoint:

curl -H "Authorization: Bearer <token>" \
  "https://yourorg.salesforce.com/services/data/v62.0/limits" | \
  jq '.DailyApiRequests'
# {"Max": 750000, "Remaining": 642000}

Alert at 20% remaining — not the standard 10% — to give time to throttle before hitting zero. At zero, all API calls return 403 until the 24-hour window resets.

Composite and Collections APIs are the primary mitigation. An agent that updates 500 records one at a time consumes 500 API calls. The same operation via composite/sobjects consumes 3 calls (200 records per call). Enforce this in the tool implementation layer, not in the prompt.

7.4 Record-Locking Conflicts

Salesforce uses row-level locking on record updates. In high-concurrency automation (multiple agents or pipeline stages touching the same records simultaneously), you’ll encounter UNABLE_TO_LOCK_ROW errors. This is not a transient network error — it is a deliberate lock that will not resolve by immediately retrying.

Pattern: implement exponential backoff with jitter specifically for this error code. A 2–10 second random sleep between retries (up to 3 attempts) resolves the conflict in most cases.

7.5 Session Timeout and Token Management

Salesforce access tokens expire based on the Connected App’s Session Timeout setting (default: 2 hours; configurable from 15 minutes to 24 hours or “until logout”). For long-running agent pipelines:

  • Re-authenticate before each batch run rather than caching tokens across runs.
  • For very long jobs (multi-hour Bulk API operations), implement a token refresh check before each batch step.
  • The JWT Bearer flow makes refresh trivial — re-running the JWT flow takes under 200ms and always returns a fresh token.

Web session tokens (used by the frontdoor.jsp browser automation pattern) follow the org’s Session Settings timeout, which may be shorter than the Connected App timeout. For browser automation tasks exceeding 30 minutes, check session validity and re-inject if needed.

7.6 MFA and IP Restrictions for Browser Automation

Salesforce’s security posture in 2026 defaults to MFA for all human users. Browser automation against human user accounts will hit MFA prompts. Mitigations:

  1. Use a dedicated integration user with an integration-specific profile. Set the profile’s Session Security Level to bypass MFA for API/automation access. This requires a System Administrator to configure.
  2. Use the frontdoor.jsp injection pattern (described in Section 3.2) — this skips the login page entirely, including MFA. The token obtained via JWT Bearer flow is already authenticated; the session injection trades it for a web session cookie without triggering MFA.
  3. Never automate against a named human user account. This creates audit trail contamination (all automated actions show under the human’s name) and will break when the human user rotates their password or enables personal MFA.

7.7 Data Residency Considerations

Salesforce’s default instance assigns data to a regional pod (NA, EU, AP). For orgs in specific data residency configurations (Hyperforce on AWS/GCP/Azure in specific geographies), the instance_url returned by OAuth will reflect the regional endpoint — e.g., https://yourorg.eu.salesforce.com. Automation that hardcodes login.salesforce.com as the base URL may fail for:

  • Sandbox logins (use test.salesforce.com)
  • My Domain orgs (use the My Domain URL, obtained from the OAuth response instance_url)
  • Government Cloud / Military Cloud (custom login endpoints)

Always use the instance_url from the OAuth response as the base for subsequent API calls, not a hardcoded domain.

For third-party integrations (Composio, LangChain Salesforce toolkit, community MCP servers), verify that tool calls route through the correct regional endpoint and that the integration vendor’s data processing practices comply with your org’s data residency commitments before sending production CRM data.

7.8 Salesforce CLI Security Changes (Summary)

The May 2026 security update requires changes to any script that:

  • Parses accessToken or sfdxAuthUrl from sf org display --json
  • Uses environment variables to capture credentials from CLI output
  • Passes --auth-url arguments containing tokens inline

The new pattern: credentials are only retrievable via explicit, interactive commands. CI/CD pipelines must authenticate from scratch per run using stored secrets (JWT private key + consumer key), not reuse captured credentials.


8. Decision Framework: Which Approach for Which Task

Task Best Approach Notes
Deploy metadata to sandbox sf project deploy start Fastest; version-controllable
One-time large data load (>10K records) sf data upsert bulk or Bulk API 2.0 directly Don’t use REST for this
LLM querying CRM in plain English Salesforce DX MCP Server Official; 60+ tools; best tool for devs
Read-only CRM context for Claude Desktop jpmonette/salesforce-mcp Minimal setup; no DevOps overhead
Multi-step pipeline automation LangGraph + Salesforce tools Stateful; checkpointable; handles errors
UI-only configuration task Playwright + frontdoor.jsp injection No API equivalent; inject session to skip MFA
Reactive pipeline (event-triggered) Streaming API / CDC + agent webhook Real-time; avoids polling
Bulk record enrichment LangGraph + Bulk API 2.0 Checkpoint state; respect 100M/day limit
End-to-end Agentforce testing sf agent preview commands GA in Summer '26; scriptable in CI
Exploratory data analysis by non-developer Claude in Slack via Salesforce MCP No SOQL required; works in existing workflow

9. Forward-Looking Notes (Summer '26 and Beyond)

Agentforce Experience Layer (AXL) decouples agent logic from rendering surface. An agent built against the Salesforce platform can now render its output natively in Slack, Teams, Claude, or any MCP-compatible client without surface-specific code. This will accelerate agent deployment for teams who currently maintain separate Slack bot + Salesforce flow implementations.

SOAP login() retirement (Summer '27). Any integration still using SOAP username/password authentication has approximately 12 months to migrate to OAuth. The sf CLI, all MCP servers, and the REST API already default to OAuth. The stragglers are legacy middleware integrations (Mulesoft flows, older iPaaS connectors) that were set up before OAuth was mandatory.

Native Shadow DOM migration in Lightning components will progressively tighten browser automation. Teams relying on Playwright or browser-use for Salesforce UI automation should audit their locator strategies and switch to semantic locators (get_by_role, get_by_label, AgentQL queries) ahead of their org’s migration window.

Data Cloud MCP server (Developer Preview → GA). The facade architecture handling ~200 API operations is the model for how future Salesforce platform surface will be exposed to LLMs. Expect the same pattern applied to Einstein Analytics and Revenue Cloud in H2 2026.


Sources