Connecting an AI sales agent to Salesforce is less about prompt engineering and more about disciplined tool boundaries. In this tutorial you’ll build a minimal agent that authenticates to Salesforce, creates a lead, and queries opportunities using function calling. We’ll use Python, simple-salesforce, and an OpenAI-compatible endpoint to connect AI sales agent Salesforce without a heavy framework.
Prerequisites
- Python 3.11 or newer
- A Salesforce org with API access (Developer Edition or sandbox is fine)
simple-salesforceandopenaiPython packages- Environment variables for Salesforce credentials and an LLM API key
pip install simple-salesforce openai python-dotenv
Create a .env file:
SF_USERNAME=you@domain.com
SF_PASSWORD=yourpassword
SF_SECURITY_TOKEN=XXXXXX
SF_DOMAIN=test # use "login" for production
LLM_API_KEY=sk-...
LLM_BASE_URL=https://api.openai.com/v1 # or your gateway
The security token is required unless your IP is allowlisted. For production, set SF_DOMAIN=login.
Step 1: Authenticate to Salesforce
Use simple-salesforce to obtain a client. Fail fast if credentials are wrong.
import os
from dotenv import load_dotenv
from simple_salesforce import Salesforce
load_dotenv()
sf = Salesforce(
username=os.environ["SF_USERNAME"],
password=os.environ["SF_PASSWORD"],
security_token=os.environ["SF_SECURITY_TOKEN"],
domain=os.environ["SF_DOMAIN"],
)
org_id = sf.query("SELECT Id FROM Organization LIMIT 1")["records"][0]["Id"]
print("Connected to org:", org_id)
Expected output:
Connected to org: 00D5f0000012345ABC
If you see SalesforceAuthenticationFailed, verify the token and that you’re hitting the right domain. A common mistake is using test for a production org.
Step 2: Define Salesforce tools
The agent should only touch well-scoped functions. Below are three minimal operations that map to standard objects.
def create_lead(first_name: str, last_name: str, company: str, email: str) -> dict:
result = sf.Lead.create({
"FirstName": first_name,
"LastName": last_name,
"Company": company,
"Email": email,
})
return {"id": result["id"], "success": result["success"]}
def get_open_opportunities(min_amount: float) -> list:
query = f"SELECT Id, Name, Amount FROM Opportunity WHERE IsClosed = false AND Amount > {min_amount}"
records = sf.query(query)["records"]
return [{"id": r["Id"], "name": r["Name"], "amount": r["Amount"]} for r in records]
def update_lead_status(lead_id: str, status: str) -> dict:
sf.Lead.update(lead_id, {"Status": status})
return {"updated": lead_id, "status": status}
These wrappers return plain dicts so the LLM can parse them without SOAP envelopes or nested attributes. Never return the raw simple_salesforce response; it carries metadata the model doesn’t need.
Step 3: Describe tools for the LLM
OpenAI-style function calling expects JSON schemas. Keep descriptions imperative and parameter lists tight.
[
{
"type": "function",
"function": {
"name": "create_lead",
"description": "Create a new lead in Salesforce with name, company, email",
"parameters": {
"type": "object",
"properties": {
"first_name": {"type": "string"},
"last_name": {"type": "string"},
"company": {"type": "string"},
"email": {"type": "string"}
},
"required": ["last_name", "company"]
}
}
},
{
"type": "function",
"function": {
"name": "get_open_opportunities",
"description": "List open opportunities with amount greater than min_amount",
"parameters": {
"type": "object",
"properties": {"min_amount": {"type": "number"}},
"required": ["min_amount"]
}
}
}
]
Only expose what the agent needs. If you add update_lead_status, include it here as well.
Step 4: Build the agent loop
We use the openai client. When you connect AI sales agent Salesforce to an LLM gateway, point the client at a single OpenAI-compatible base URL. If you point it at n4n.ai’s OpenAI-compatible endpoint, you get automatic fallback when a provider is rate-limited, which matters for unattended sales agents.
import json
from openai import OpenAI
client = OpenAI(
api_key=os.environ["LLM_API_KEY"],
base_url=os.environ.get("LLM_BASE_URL"),
)
tools = json.load(open("tools.json")) # from Step 3
functions = {"create_lead": create_lead, "get_open_opportunities": get_open_opportunities}
messages = [{"role": "user", "content": "Create a lead for Jane Doe at Acme, jane@acme.com, then list open opportunities above 10000"}]
while True:
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
tools=tools,
tool_choice="auto",
)
msg = resp.choices[0].message
if not msg.tool_calls:
print("Agent:", msg.content)
break
messages.append(msg)
for call in msg.tool_calls:
fn = functions[call.function.name]
args = json.loads(call.function.arguments)
output = fn(**args)
messages.append({
"role": "tool",
"tool_call_id": call.id,
"content": json.dumps(output),
})
This loop terminates when the model returns text instead of a tool call. Keep the model small (gpt-4o-mini or equivalent) for deterministic orchestration.
Step 5: Run the task and verify
Execute the script. Expected intermediate tool output (truncated):
Agent: Created lead 00Q5f000001abcDEF. Open opportunities above 10000: [{'id': '0065f00000xyz', 'name': 'Acme Expansion', 'amount': 25000}]
If the agent skips the lead creation, tighten the prompt or set tool_choice to force the first call. Function calling is not guaranteed to be ordered unless you constrain it.
Step 6: Error handling and idempotency
Salesforce throws on duplicate emails if you configured a unique rule. Wrap calls:
def create_lead_safe(**kwargs):
try:
return create_lead(**kwargs)
except Exception as e:
return {"error": str(e)}
The agent should surface "error" fields to the user rather than hallucinate success. When you connect AI sales agent Salesforce to live data, treat every write as a side effect that needs confirmation in high-value objects like Opportunity. Use Id lookups before updates.
Step 7: Smoke test the integration
Mock Salesforce to verify the agent calls tools in the right order without hitting the API.
from unittest.mock import patch
def test_agent_calls_lead_first():
with patch("__main__.create_lead") as mock_lead, \
patch("__main__.get_open_opportunities") as mock_opp:
mock_lead.return_value = {"id": "00Q", "success": True}
mock_opp.return_value = []
# run loop with a mocked client.chat.completions.create
# assert mock_lead called before mock_opp
A real test would mock client.chat.completions.create to return a scripted tool call sequence. This catches prompt regressions before they touch CRM data.
Production considerations
- Cache Salesforce object metadata (fields, picklists) to avoid repeated
describecalls. - Use a named integration user with least-privilege permission sets; never embed admin creds in the agent process.
- For asynchronous agents, queue tool calls and reconcile with Salesforce Change Data Capture events.
- Honor client routing directives if your gateway supports them; n4n.ai forwards provider cache-control hints, which can reduce redundant token spend on long system prompts that repeat the Salesforce schema.
- Log every tool call with the Salesforce record Id and the model’s rationale. Auditing is non-negotiable in sales contexts.
Building the connection is straightforward; keeping it safe is the real work.