AI Tool Calling Pattern for Salesforce Apex
Overview
AI tool calling (also called function calling) allows an LLM to request structured actions -- such as creating records or querying data -- rather than just returning text. Implementing this in Salesforce Apex introduces a unique challenge: the platform prohibits HTTP callouts after DML operations within the same transaction. This article presents the requiresFollowUp pattern that cleanly splits tool-calling flows across multiple transactions.
- Why it matters: Without this pattern, any AI integration that needs to both modify Salesforce data and communicate results back to the LLM will fail with a
CalloutException. This is a blocking architectural constraint for agentic AI in Salesforce. - What you will learn: How to orchestrate multi-transaction tool calling between an LWC client and Apex controller, handle multi-tool chaining, and work within Salesforce's governor limits rather than against them.
When integrating LLMs like Claude or GPT into Salesforce via Apex callouts, tool calling (also called function calling) lets the AI request structured actions — creating records, querying data, or updating fields — instead of just returning text. However, Salesforce's transaction model creates a unique challenge that requires a specific architectural pattern.
The Problem: DML-Before-Callout
Salesforce enforces a strict rule: you cannot make an HTTP callout after performing DML in the same transaction. This throws System.CalloutException: You have uncommitted work pending.
In a typical tool-calling flow, the AI returns a tool_use stop reason, your code executes the tool (which often involves DML), and then you need to send the tool result back to the API for a natural-language response. That second callout violates the DML-before-callout rule.
The Solution: requiresFollowUp Pattern
Split the tool-calling flow across two separate transactions, orchestrated by the client (LWC):
LWC → sendMessage() → API callout → stop_reason: 'tool_use'
→ executeToolAndBuildResult() → DML → return {requiresFollowUp: true, toolResult}
LWC detects requiresFollowUp → calls sendMessage() again with toolResult
→ API callout → returns natural language summary → LWC displays response
Apex Controller
public class ChatController {
@AuraEnabled
public static Map<String, Object> sendMessage(
List<Map<String, Object>> messages,
List<Map<String, Object>> tools
) {
// First call: send conversation + tools to the LLM API
HttpResponse res = makeApiCallout(messages, tools);
Map<String, Object> parsed = parseResponse(res);
String stopReason = (String) parsed.get('stop_reason');
if (stopReason == 'tool_use') {
// Extract the tool call from the response
Map<String, Object> toolCall = extractToolCall(parsed);
String toolName = (String) toolCall.get('name');
Map<String, Object> toolInput = (Map<String, Object>) toolCall.get('input');
// Execute the tool (may perform DML)
Map<String, Object> toolResult = executeToolAndBuildResult(toolName, toolInput);
// Cannot call API again — DML already happened
// Return to LWC for a follow-up transaction
return new Map<String, Object>{
'requiresFollowUp' => true,
'toolCallId' => (String) toolCall.get('id'),
'toolResult' => JSON.serialize(toolResult),
'assistantMessage' => JSON.serialize(parsed.get('content'))
};
}
// Normal text response — return directly
return new Map<String, Object>{
'requiresFollowUp' => false,
'response' => extractTextContent(parsed)
};
}
private static Map<String, Object> executeToolAndBuildResult(
String toolName, Map<String, Object> input
) {
switch on toolName {
when 'create_case' {
Case c = new Case(
Subject = (String) input.get('subject'),
Description = (String) input.get('description'),
Priority = (String) input.get('priority')
);
insert c; // DML — no more callouts allowed after this
return new Map<String, Object>{
'success' => true,
'caseId' => c.Id,
'caseNumber' => [SELECT CaseNumber FROM Case WHERE Id = :c.Id].CaseNumber
};
}
when else {
return new Map<String, Object>{ 'error' => 'Unknown tool: ' + toolName };
}
}
}
}
LWC Controller (Client-Side Orchestration)
async handleSend() {
let result = await sendMessage({ messages: this.messages, tools: this.tools });
if (result.requiresFollowUp) {
// Build the follow-up messages: assistant tool_use + user tool_result
this.messages.push(
{ role: 'assistant', content: JSON.parse(result.assistantMessage) },
{ role: 'user', content: [{
type: 'tool_result',
tool_use_id: result.toolCallId,
content: result.toolResult
}]}
);
// Second transaction — no DML happened, so callout succeeds
result = await sendMessage({ messages: this.messages, tools: this.tools });
}
this.displayResponse(result.response);
}
Multi-Tool Chaining
Some workflows require multiple tools in sequence — for example, "search for the account, then create a case under it." Each tool that performs DML triggers a follow-up. The LWC loop handles this naturally:
async handleSend() {
let result = await sendMessage({ messages: this.messages, tools: this.tools });
// Loop until the AI returns a final text response
while (result.requiresFollowUp) {
this.messages.push(
{ role: 'assistant', content: JSON.parse(result.assistantMessage) },
{ role: 'user', content: [{
type: 'tool_result',
tool_use_id: result.toolCallId,
content: result.toolResult
}]}
);
result = await sendMessage({ messages: this.messages, tools: this.tools });
}
this.displayResponse(result.response);
}
Each iteration is a fresh Apex transaction: callout first, then DML if needed, then return to the client. The chain continues until the AI produces a stop_reason of end_turn instead of tool_use.
Why This Pattern Exists
Salesforce's governor limits treat each @AuraEnabled call as an isolated transaction. The DML-before-callout restriction exists to prevent partial commits from leaving the database in an inconsistent state if a subsequent callout fails. By splitting across transactions, each one completes cleanly: the first transaction does its DML and commits, and the second transaction makes a fresh callout with the result.
This pattern is not a workaround — it is the correct way to implement agentic AI behavior within Salesforce's transaction model.