Introduction to Function Calling with Claude Code
Function calling represents one of the most powerful capabilities in modern AI development, allowing language models to interact with external systems, APIs, and tools in a structured, reliable way. When combined with Claude Code, Anthropic's command-line interface for Claude, developers can orchestrate complex workflows that leverage Claude's reasoning abilities alongside deterministic code execution. This guide walks through everything you need to know to implement function calling at scale using Claude Code.
What Is Function Calling?
Function calling is a mechanism that enables a language model to request the execution of specific functions defined by the developer. Instead of generating free-form text responses, the model outputs structured data that specifies which function to call, along with the arguments to pass. The host application then executes that function and returns the result back to the model for further processing.
With Claude, this is implemented through tool use. You define a set of tools — each with a name, description, and input schema — and Claude decides when and how to invoke them based on the user's request. Claude Code extends this by providing a CLI environment where these tool calls can be executed against your actual codebase, file system, and shell commands.
Key Concepts
- Tool Definition: A JSON schema describing a function's name, purpose, and expected parameters.
- Tool Use Request: Claude's structured output requesting a function call with specific arguments.
- Tool Result: The output returned to Claude after the function executes.
- Agentic Loop: The cycle of Claude requesting tool calls, the system executing them, and Claude processing results until the task is complete.
Why Function Calling at Scale Matters
Building a single function-calling interaction is straightforward. But when you need to handle hundreds or thousands of concurrent requests, manage state across multi-step tool chains, handle failures gracefully, and keep costs under control, the complexity grows significantly. Function calling at scale matters because it unlocks real production use cases: automated code review pipelines, batch refactoring across large repositories, continuous documentation generation, and autonomous debugging systems.
Without a deliberate architecture for scale, you risk hitting rate limits, exhausting context windows, losing track of intermediate state, and producing inconsistent results. The strategies in this guide address these challenges directly.
Setting Up Your Environment
Before diving into implementation, ensure your environment is properly configured. You need Node.js 18 or higher, an Anthropic API key, and the Claude Code CLI installed globally.
# Install Claude Code globally
npm install -g @anthropic-ai/claude-code
# Set your API key
export ANTHROPIC_API_KEY=sk-ant-your-key-here
# Verify installation
claude --version
For programmatic access, install the Anthropic SDK as well:
npm install @anthropic-ai/sdk
Defining Tools for Claude
The foundation of function calling is the tool definition. Each tool must have a clear name, a descriptive purpose, and a well-structured input schema. Claude uses the description to decide when a tool is appropriate, so invest time in writing precise, unambiguous descriptions.
const tools = [
{
name: "search_codebase",
description: "Search the codebase for files matching a query string. Returns file paths and matching line numbers. Use this when you need to find where code is defined or referenced.",
input_schema: {
type: "object",
properties: {
query: {
type: "string",
description: "The search term or pattern to look for"
},
file_extension: {
type: "string",
description: "Optional file extension filter, e.g. 'ts', 'py', 'js'"
},
max_results: {
type: "integer",
description: "Maximum number of results to return",
default: 20
}
},
required: ["query"]
}
},
{
name: "read_file",
description: "Read the full contents of a file at the given path. Use this to inspect file contents before making changes.",
input_schema: {
type: "object",
properties: {
path: {
type: "string",
description: "Relative or absolute path to the file"
}
},
required: ["path"]
}
},
{
name: "write_file",
description: "Write content to a file, creating it if it does not exist or overwriting if it does. Use this to create or update files.",
input_schema: {
type: "object",
properties: {
path: {
type: "string",
description: "Relative or absolute path to the file"
},
content: {
type: "string",
description: "The full content to write to the file"
}
},
required: ["path", "content"]
}
},
{
name: "run_tests",
description: "Execute the test suite and return the output. Use this after making changes to verify correctness.",
input_schema: {
type: "object",
properties: {
test_path: {
type: "string",
description: "Optional specific test file or pattern to run"
}
}
}
}
];
Building the Core Function Calling Loop
The heart of any function calling system is the agentic loop. Claude receives a message, potentially requests tool calls, the system executes them, feeds results back, and the cycle continues until Claude produces a final response without any tool call requests.
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
async function runAgent(userMessage, tools, toolHandlers) {
const messages = [
{ role: "user", content: userMessage }
];
const maxIterations = 20;
for (let i = 0; i < maxIterations; i++) {
const response = await client.messages.create({
model: "claude-sonnet-4-20250514",
max_tokens: 4096,
tools: tools,
messages: messages
});
// Check if Claude wants to use tools
if (response.stop_reason === "tool_use") {
// Add Claude's response to conversation
messages.push({ role: "assistant", content: response.content });
// Process all tool use requests in this response
const toolResults = [];
for (const block of response.content) {
if (block.type === "tool_use") {
const handler = toolHandlers[block.name];
if (!handler) {
toolResults.push({
type: "tool_result",
tool_use_id: block.id,
content: `Error: Unknown tool "${block.name}"`,
is_error: true
});
continue;
}
try {
const result = await handler(block.input);
toolResults.push({
type: "tool_result",
tool_use_id: block.id,
content: typeof result === "string" ? result : JSON.stringify(result)
});
} catch (err) {
toolResults.push({
type: "tool_result",
tool_use_id: block.id,
content: `Error executing tool: ${err.message}`,
is_error: true
});
}
}
}
// Feed tool results back to Claude
messages.push({ role: "user", content: toolResults });
} else {
// No tool use — extract final text response
const textBlocks = response.content.filter(b => b.type === "text");
return textBlocks.map(b => b.text).join("\n");
}
}
return "Max iterations reached without completion.";
}
Implementing Tool Handlers
Tool handlers are the bridge between Claude's requests and your actual system. Each handler receives the parsed input from Claude and returns a string or serializable object. Here is a practical set of handlers for a code manipulation agent:
import { execSync } from "child_process";
import { readFileSync, writeFileSync, existsSync } from "fs";
import path from "path";
const toolHandlers = {
search_codebase: (input) => {
const { query, file_extension, max_results = 20 } = input;
let cmd = `grep -rn "${query}" .`;
if (file_extension) {
cmd += ` --include="*.${file_extension}"`;
}
cmd += ` | head -n ${max_results}`;
try {
const output = execSync(cmd, { encoding: "utf-8", maxBuffer: 1024 * 1024 });
return output || "No matches found.";
} catch (err) {
return "No matches found.";
}
},
read_file: (input) => {
const { path: filePath } = input;
if (!existsSync(filePath)) {
return `Error: File not found at ${filePath}`;
}
return readFileSync(filePath, "utf-8");
},
write_file: (input) => {
const { path: filePath, content } = input;
const dir = path.dirname(filePath);
execSync(`mkdir -p ${dir}`);
writeFileSync(filePath, content, "utf-8");
return `Successfully wrote ${content.length} characters to ${filePath}`;
},
run_tests: (input) => {
const { test_path } = input;
const cmd = test_path
? `npm test -- ${test_path}`
: `npm test`;
try {
const output = execSync(cmd, {
encoding: "utf-8",
maxBuffer: 1024 * 1024 * 10,
timeout: 120000
});
return output;
} catch (err) {
return `Tests failed:\n${err.stdout || err.message}`;
}
}
};
Scaling Up: Concurrency and Batching
When you need to process many tasks — for example, reviewing 500 pull requests or refactoring 200 modules — running them sequentially is too slow. The key is controlled concurrency that respects API rate limits while maximizing throughput.
Implementing a Concurrency Pool
class ConcurrencyPool {
constructor(maxConcurrency) {
this.maxConcurrency = maxConcurrency;
this.active = 0;
this.queue = [];
}
async run(task) {
return new Promise((resolve, reject) => {
this.queue.push({ task, resolve, reject });
this.process();
});
}
process() {
while (this.active < this.maxConcurrency && this.queue.length > 0) {
const { task, resolve, reject } = this.queue.shift();
this.active++;
task()
.then(resolve)
.catch(reject)
.finally(() => {
this.active--;
this.process();
});
}
}
}
async function processBatch(tasks, maxConcurrency = 5) {
const pool = new ConcurrencyPool(maxConcurrency);
const results = await Promise.all(
tasks.map(task => pool.run(() => runAgentSafe(task)))
);
return results;
}
async function runAgentSafe(task) {
try {
const result = await runAgent(task.prompt, tools, toolHandlers);
return { task: task.id, success: true, result };
} catch (err) {
return { task: task.id, success: false, error: err.message };
}
}
Rate Limit Handling with Exponential Backoff
Even with concurrency control, you will encounter rate limits. Implementing exponential backoff with jitter ensures your system degrades gracefully rather than failing hard.
async function callWithRetry(fn, maxRetries = 5) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn();
} catch (err) {
if (err.status === 429 || err.status === 529) {
const baseDelay = Math.pow(2, attempt) * 1000;
const jitter = Math.random() * 500;
const delay = baseDelay + jitter;
console.warn(`Rate limited. Retrying in ${Math.round(delay)}ms (attempt ${attempt + 1})`);
await new Promise(r => setTimeout(r, delay));
} else {
throw err;
}
}
}
throw new Error("Max retries exceeded");
}
// Wrap your API call
const response = await callWithRetry(() =>
client.messages.create({
model: "claude-sonnet-4-20250514",
max_tokens: 4096,
tools: tools,
messages: messages
})
);
Managing Context at Scale
One of the biggest challenges at scale is context window management. Long agentic loops accumulate tokens rapidly. A single agent session that performs 15 tool calls can easily consume 50,000+ tokens. Here are strategies to keep context manageable.
Summarizing Tool Results
Raw tool outputs — especially file contents and test logs — can be enormous. Summarize or truncate them before feeding back to Claude:
function truncateResult(result, maxChars = 5000) {
if (result.length <= maxChars) {
return result;
}
const half = Math.floor(maxChars / 2);
const head = result.slice(0, half);
const tail = result.slice(result.length - half);
const omitted = result.length - maxChars;
return `${head}\n\n... [${omitted} characters omitted] ...\n\n${tail}`;
}
function summarizeTestOutput(output) {
const lines = output.split("\n");
const summaryLines = lines.filter(line =>
line.includes("PASS") ||
line.includes("FAIL") ||
line.includes("Tests:") ||
line.includes("✓") ||
line.includes("✗") ||
line.includes("Error")
);
return summaryLines.join("\n");
}
// Use in your handler
const result = truncateResult(rawOutput, 5000);
Checkpointing Long Sessions
For very long tasks, periodically summarize the conversation and restart with a compressed context. This prevents hitting the context limit while preserving essential information:
async function checkpointConversation(messages, client) {
const summaryResponse = await client.messages.create({
model: "claude-sonnet-4-20250514",
max_tokens: 2048,
messages: [
...messages,
{
role: "user",
content: "Summarize the work done so far, key decisions made, files modified, and remaining tasks. Be concise but complete."
}
]
});
const summary = summaryResponse.content
.filter(b => b.type === "text")
.map(b => b.text)
.join("\n");
return [
{
role: "user",
content: `Previous work summary:\n\n${summary}\n\nContinue from where you left off.`
}
];
}
// In your main loop, checkpoint every 10 iterations
if (i > 0 && i % 10 === 0) {
messages = await checkpointConversation(messages, client);
}
Using Claude Code CLI for Batch Operations
Claude Code's CLI supports headless execution, making it ideal for batch operations. You can script it to process multiple repositories or tasks in parallel:
#!/bin/bash
# batch-review.sh — Run code review across multiple repositories
REPOS=("api-gateway" "auth-service" "payment-processor" "notification-service" "user-service")
MAX_PARALLEL=3
RUNNING=0
for repo in "${REPOS[@]}"; do
(
cd "/projects/$repo" || exit 1
claude --print \
--output-format json \
"Review the recent git changes in this repository. Identify potential bugs, security issues, and improvement opportunities. Output a structured report." \
> "/reports/${repo}-review.json"
echo "Completed review for $repo"
) &
RUNNING=$((RUNNING + 1))
if [ $RUNNING -ge $MAX_PARALLEL ]; then
wait -n
RUNNING=$((RUNNING - 1))
fi
done
wait
echo "All reviews complete."
Programmatic Claude Code Invocation
For more control, invoke Claude Code programmatically from Node.js:
import { exec } from "child_process";
import { promisify } from "util";
const execAsync = promisify(exec);
async function runClaudeCode(prompt, cwd) {
const escapedPrompt = prompt.replace(/'/g, "'\\''");
const cmd = `claude --print --output-format json '${escapedPrompt}'`;
try {
const { stdout } = await execAsync(cmd, {
cwd,
maxBuffer: 1024 * 1024 * 50,
timeout: 300000
});
return JSON.parse(stdout);
} catch (err) {
console.error(`Claude Code failed: ${err.message}`);
throw err;
}
}
// Process multiple repos concurrently
const repos = [
{ name: "api-gateway", task: "Add input validation to all endpoints" },
{ name: "auth-service", task: "Migrate callback patterns to async/await" },
{ name: "payment-processor", task: "Add error handling for payment retries" }
];
const pool = new ConcurrencyPool(3);
const results = await Promise.all(
repos.map(repo =>
pool.run(() =>
runClaudeCode(repo.task, `/projects/${repo.name}`)
.then(result => ({ repo: repo.name, success: true, result }))
.catch(err => ({ repo: repo.name, success: false, error: err.message }))
)
)
);
console.log(JSON.stringify(results, null, 2));
Best Practices for Function Calling at Scale
Design Clear, Focused Tools
Each tool should do one thing well. Avoid creating "god tools" that accept dozens of parameters and branch internally. Claude makes better decisions when tools have narrow, well-defined purposes. If you find a tool description growing beyond three sentences, consider splitting it into multiple tools.
Validate Tool Inputs
Never trust that Claude will always produce perfectly valid inputs. Add validation in every handler:
function validateInput(input, schema) {
const errors = [];
for (const [key, rule] of Object.entries(schema.properties)) {
if (schema.required?.includes(key) && !(key in input)) {
errors.push(`Missing required field: ${key}`);
continue;
}
if (key in input) {
const val = input[key];
if (rule.type === "string" && typeof val !== "string") {
errors.push(`${key} must be a string`);
}
if (rule.type === "integer" && !Number.isInteger(val)) {
errors.push(`${key} must be an integer`);
}
}
}
if (errors.length > 0) {
throw new Error(`Validation failed: ${errors.join(", ")}`);
}
}
// Usage in handler
read_file: (input) => {
validateInput(input, tools.find(t => t.name === "read_file").input_schema);
// ... proceed with validated input
}
Log Everything for Observability
At scale, debugging individual failures requires detailed logging. Log every tool call, its inputs, outputs, timing, and token usage:
const toolCallLog = [];
function logToolCall(name, input, output, durationMs, success) {
const entry = {
timestamp: new Date().toISOString(),
tool: name,
input: input,
outputLength: typeof output === "string" ? output.length : JSON.stringify(output).length,
durationMs,
success
};
toolCallLog.push(entry);
console.log(`[TOOL] ${name} | ${durationMs}ms | ${success ? "OK" : "FAIL"}`);
}
// Wrap handlers with logging
function withLogging(name, handler) {
return async (input) => {
const start = Date.now();
try {
const result = await handler(input);
logToolCall(name, input, result, Date.now() - start, true);
return result;
} catch (err) {
logToolCall(name, input, err.message, Date.now() - start, false);
throw err;
}
};
}
// Apply to all handlers
const loggedHandlers = Object.fromEntries(
Object.entries(toolHandlers).map(([name, handler]) =>
[name, withLogging(name, handler)]
)
);
Use the Right Model for the Task
Not every task needs Claude's most capable model. For simple, repetitive operations like generating boilerplate or running standard refactors, a faster model reduces cost and latency. Reserve the most capable models for complex reasoning tasks. You can route dynamically:
function selectModel(task) {
if (task.complexity === "simple" || task.type === "boilerplate") {
return "claude-3-5-haiku-20241022";
}
if (task.complexity === "moderate" || task.type === "refactor") {
return "claude-sonnet-4-20250514";
}
return "claude-opus-4-02025050514"; // Most capable for complex reasoning
}
Implement Circuit Breakers
When running at scale, cascading failures are a real risk. A circuit breaker prevents your system from continuously calling an API or tool that is failing:
class CircuitBreaker {
constructor(threshold = 5, resetTimeout = 60000) {
this.failures = 0;
this.threshold = threshold;
this.resetTimeout = resetTimeout;
this.lastFailureTime = null;
this.state = "CLOSED"; // CLOSED, OPEN, HALF_OPEN
}
async call(fn) {
if (this.state === "OPEN") {
if (Date.now() - this.lastFailureTime > this.resetTimeout) {
this.state = "HALF_OPEN";
} else {
throw new Error("Circuit breaker is open — refusing to call");
}
}
try {
const result = await fn();
this.failures = 0;
this.state = "CLOSED";
return result;
} catch (err) {
this.failures++;
this.lastFailureTime = Date.now();
if (this.failures >= this.threshold) {
this.state = "OPEN";
}
throw err;
}
}
}
const apiBreaker = new CircuitBreaker(5, 60000);
const response = await apiBreaker.call(() => client.messages.create(params));
Cache Repeated Tool Results
When processing many similar tasks, tool results often repeat. Caching file reads, search results, and test outputs saves time and tokens:
const cache = new Map();
function cachedHandler(key, handler) {
return async (input) => {
const cacheKey = `${key}:${JSON.stringify(input)}`;
if (cache.has(cacheKey)) {
return cache.get(cacheKey);
}
const result = await handler(input);
cache.set(cacheKey, result);
return result;
};
}
// Apply caching to read-only operations
const cachedHandlers = {
...toolHandlers,
read_file: cachedHandler("read_file", toolHandlers.read_file),
search_codebase: cachedHandler("search_codebase", toolHandlers.search_codebase)
};
Putting It All Together: A Complete Example
Here is a complete, production-ready example that combines all the patterns discussed — a batch code documentation generator that processes an entire project:
import Anthropic from "@anthropic-ai/sdk";
import { execSync } from "child_process";
import { readFileSync, writeFileSync, existsSync } from "fs";
import path from "path";
const client = new Anthropic();
const docTools = [
{
name: "list_files",
description: "List all files in a directory matching an extension.",
input_schema: {
type: "object",
properties: {
directory: { type: "string", description: "Directory path" },
extension: { type: "string", description: "File extension to filter" }
},
required: ["directory"]
}
},
{
name: "read_file",
description: "Read a file's contents.",
input_schema: {
type: "object",
properties: {
path: { type: "string", description: "File path" }
},
required: ["path"]
}
},
{
name: "write_docs",
description: "Write documentation for a file.",
input_schema: {
type: "object",
properties: {
source_path: { type: "string", description: "Original source file" },
doc_content: { type: "string", description: "Markdown documentation" }
},
required: ["source_path", "doc_content"]
}
}
];
const docHandlers = {
list_files: (input) => {
const { directory, extension } = input;
let cmd = `find ${directory} -type f`;
if (extension) cmd += ` -name "*.${extension}"`;
return execSync(cmd, { encoding: "utf-8" }).trim();
},
read_file: (input) => {
return readFileSync(input.path, "utf-8");
},
write_docs: (input) => {
const docPath = input.source_path.replace(/\.\w+$/, ".md");
const fullPath = path.join("docs", docPath);
writeFileSync(fullPath, input.doc_content, "utf-8");
return `Documentation written to ${fullPath}`;
}
};
async function generateDocsForFile(filePath) {
const messages = [
{
role: "user",
content: `Analyze the file at ${filePath} and generate comprehensive documentation. Read the file, understand its purpose, exports, and key functions, then write documentation using the write_docs tool.`
}
];
for (let i = 0; i < 15; i++) {
const response = await callWithRetry(() =>
client.messages.create({
model: "claude-sonnet-4-20250514",
max_tokens: 4096,
tools: docTools,
messages
})
);
if (response.stop_reason === "tool_use") {
messages.push({ role: "assistant", content: response.content });
const toolResults = [];
for (const block of response.content) {
if (block.type === "tool_use") {
const handler = docHandlers[block.name];
try {
const result = handler(block.input);
toolResults.push({
type: "tool_result",
tool_use_id: block.id,
content: truncateResult(result)
});
} catch (err) {
toolResults.push({
type: "tool_result",
tool_use_id: block.id,
content: `Error: ${err.message}`,
is_error: true
});
}
}
}
messages.push({ role: "user", content: toolResults });
} else {
return response.content.filter(b => b.type === "text").map(b => b.text).join("");
}
}
return "Max iterations reached.";
}
// Run across all source files
const sourceFiles = execSync("find src -name '*.ts' -type f", { encoding: "utf-8" })
.trim()
.split("\n");
const pool = new ConcurrencyPool(4);
const results = await Promise.all(
sourceFiles.map(file =>
pool.run(() =>
generateDocsForFile(file)
.then(() => ({ file, success: true }))
.catch(err => ({ file, success: false, error: err.message }))
)
)
);
const succeeded = results.filter(r => r.success).length;
const failed = results.filter(r => !r.success).length;
console.log(`Documentation generation complete: ${succeeded} succeeded, ${failed} failed.`);
Conclusion
Function calling at scale with Claude Code transforms Claude from a conversational assistant into a reliable, autonomous agent capable of processing large workloads across entire codebases. By combining well-designed tool definitions, a robust agentic loop, controlled concurrency, rate limit handling, context management strategies, and observability practices, you can build systems that handle hundreds or thousands of tasks reliably and cost-effectively. The patterns in this guide — concurrency pools, circuit breakers, caching, checkpointing, and structured logging — form a toolkit that scales from prototype to production. Start with a single well-defined tool, validate the loop works end to end, then incrementally add concurrency, caching, and resilience features as your workload grows. With thoughtful architecture, Claude Code becomes a force multiplier for any development team.