Function Calling & Tool Use
Function calling does not let a model run a function directly. The model emits a structured request; the application validates arguments, checks policy, executes the tool, and returns the result. That boundary determines whether the system is controllable.
The complete data path
user request
→ model decides whether a tool is needed
→ tool name + arguments + call ID
→ application validates schema, policy, and business rules
→ tool executes inside a boundary
→ application returns output for that call ID
→ model answers or requests another tool
| Responsibility | Model | Application code |
|---|---|---|
| Interpret natural-language intent | Yes | Supplies business context |
| Select a candidate tool | Yes | Controls the tools exposed for this run |
| Generate arguments | Yes | Enforces schema and business validation |
| Execute API, SQL, or refund | No | Yes |
| Decide permissions and approval | May propose | Must decide deterministically |
| Verify the final side effect | May explain | Must read real system state |
Treat model output as an untrusted execution proposal, not an authorised command.
When tool calling is useful
| Requirement | Tool call | Text response |
|---|---|---|
| Current orders, weather, or inventory | Appropriate | Model knowledge may be stale |
| Create ticket, update CRM, or refund | Appropriate with policy and idempotency | Text cannot create the real action |
| Return a machine-consumed business object | Structured output or tool | Free text is harder to parse reliably |
| Explain a stable concept | Usually unnecessary | Direct response is faster |
| Required user information is missing | Ask first | Do not invent arguments |
For a task with two or three deterministic steps, ordinary application code may be enough. Do not add an autonomous loop only to make the system look agentic.
A schema should express business boundaries
{
"type": "function",
"name": "create_support_ticket",
"description": "Create a ticket after the customer and issue are identified. Do not use for status lookup.",
"parameters": {
"type": "object",
"properties": {
"customer_id": {
"type": "string",
"description": "Verified internal customer ID"
},
"category": {
"type": "string",
"enum": ["billing", "account", "technical"]
},
"summary": {
"type": "string",
"minLength": 10,
"maxLength": 500
},
"evidence_ids": {
"type": "array",
"items": { "type": "string" },
"minItems": 1
}
},
"required": ["customer_id", "category", "summary", "evidence_ids"],
"additionalProperties": false
}
}
A useful description says when to use the tool and when not to use it. Put enums, length, and format constraints in the schema. Keep permissions, balances, and state-machine rules in server-side validation.
A controlled executor
async function executeToolCall(call: ToolCall, actor: Actor) {
const tool = registry.get(call.name);
if (!tool) return toolError(call.id, 'TOOL_NOT_AVAILABLE');
const parsed = tool.schema.safeParse(call.arguments);
if (!parsed.success) {
return toolError(call.id, 'INVALID_ARGUMENTS', parsed.error.issues);
}
const policy = await authorize(actor, call.name, parsed.data);
if (!policy.allowed) return toolError(call.id, 'FORBIDDEN');
if (policy.requiresApproval) {
return toolPendingApproval(call.id, await createApproval(call, actor));
}
try {
const result = await withTimeout(
tool.execute(parsed.data, {
idempotencyKey: `${actor.taskId}:${call.id}`
}),
tool.timeoutMs
);
return toolSuccess(call.id, sanitizeForModel(result));
} catch (error) {
return mapToolError(call.id, error);
}
}
Return concise field errors to the model for one repair attempt. Do not place stack traces, secrets, or internal network details in model-visible context.
Errors should drive a safe next step
| Error | Retry? | Model-visible result | System action |
|---|---|---|---|
INVALID_ARGUMENTS | Once after repair | Invalid fields | Do not execute |
NOT_FOUND | Usually no | Resource does not exist | Ask or change query |
FORBIDDEN | No automatic retry | Insufficient access | Stop or request approval |
RATE_LIMITED | Limited backoff | Suggested wait | Exponential backoff plus jitter |
DEPENDENCY_DOWN | Limited retry | Service unavailable | Circuit breaker |
ALREADY_APPLIED | Do not repeat | Existing evidence ID | Read back state and accept idempotent success |
“Tool failed” is not diagnostic. Use stable error codes, brief messages, and internal-only raw exceptions.
Separate reads from writes
Do not create one manage_customer tool for lookup, update, and deletion. Split it into:
get_customer_profileupdate_customer_contactrequest_customer_deletion
This allows dynamic tool exposure by user state. Before identity verification, expose only reads. Load updates after verification. Always require approval for deletion.
Mutation tools also need an idempotency key, dry-run diff, provider request ID, rollback semantics, and a final state read-back.
Parallel calls are not always faster
Inventory queries across three independent warehouses can run in parallel. Looking up an order and then using its payment ID must be sequential. Before parallelising, confirm:
- Calls have no data dependency.
- The tool and downstream service support the concurrency.
- Result order does not change business meaning.
- Partial failure has a defined merge policy.
Do not ask the model to generate dozens of parallel mutations. Let code control batch size, rate, idempotency, and recovery.
Security and data boundaries
| Boundary | Minimum control |
|---|---|
| Tool exposure | Narrow by task and actor permissions |
| Arguments | Schema validation followed by business validation |
| Credentials | Short-lived credentials inside the tool, never model context |
| Files | Validate MIME, size, source, and isolated workspace |
| Network | Allow required endpoints; restrict arbitrary egress |
| Logs | Redact arguments; retain call ID and business evidence |
| High-risk action | Approval, idempotency, and provider read-back |
Web pages and documents returned by tools are data, not higher-priority instructions. An injected sentence cannot grant itself refund permission.
Evaluate more than schema compliance
Your golden set should cover correct tool use, direct answers without tools, missing information, similar tool names, business-rule rejection, timeouts, rate limits, prompt injection, and replayed mutations.
| Metric | Meaning |
|---|---|
| Tool selection accuracy | Correct tool when needed and restraint when not needed |
| Argument validity | Calls passing schema on the first attempt |
| Business-rule rejection | Valid JSON rejected by domain rules |
| Side-effect precision | Necessary and unique mutations among all writes |
| Recovery success | Correct final state after timeout or retry |
Practice task
Implement order lookup, ticket creation, and refund request tools:
- State inclusion and exclusion conditions for each tool.
- Add amount, reason, approval ID, and evidence IDs to the refund schema.
- Simulate missing fields, forbidden access, 429, and provider timeout.
- Replay one refund call and confirm only one side effect occurs.
- Record call ID, redacted arguments, latency, error code, and read-back evidence.
Definition of done
- The model proposes calls; the application retains execution authority.
- Schema and business rules are validated separately.
- Reads and writes are separated; risky writes require approval.
- Every tool has a timeout, stable error codes, and retry limit.
- Mutations are idempotent and verified against real state.
Related reading
Official references
📚 Related resources
❓ Common questions
Open a question to review the practical answer.
When should I use function calling instead of letting the model generate free text?
Consider function calling when an answer depends on current data, an external system, an executable action, or machine-readable output. If the task only requires explaining known context, a direct answer is usually simpler. If required information is missing, ask for it instead of guessing arguments. The boundary is: the model proposes a call; the application validates, authorizes, executes, and records it.
How should I design a tool schema?
Give each tool a single, explicit action. Its description should say when to use it and when not to. Define parameter types, required fields, enums, and format constraints. Do not rely on JSON Schema for permissions, pricing, inventory, or other business rules: validate and authorize those server-side. Keep sensitive defaults under application control so the model cannot silently invent high-risk arguments.
What does a healthy tool execution loop look like?
Send the available tools and conversation context to the model. When it returns a call, validate the arguments, permission, and call ID before execution, then return a structured result. Cap total steps, time, and cost. Match isolation to risk: ordinary APIs need authorization and rate limits; code, shell commands, and untrusted file processing need an isolated execution environment.
How do I handle tool errors — should I retry?
Return stable error codes such as `VALIDATION_ERROR`, `PERMISSION_DENIED`, `NOT_FOUND`, `RATE_LIMITED`, and `UPSTREAM_TIMEOUT`, plus a short actionable message. Use capped retries only for transient failures such as timeouts or rate limits; do not blindly retry invalid arguments or denied permissions. Give writes an idempotency key, confirm high-risk actions before execution, and read state back afterwards when confirmation matters.
How do I test a tool-using system?
Use replayable cases to test both calling and not calling a tool, asking for missing information, choosing between similar tools, and server-side rejection of unauthorized, replayed, or invalid calls. Inject timeouts, rate limits, empty results, and prompt injection inside tool output. Track tool-selection accuracy, valid-argument rate, blocked unauthorized actions, latency, and duplicate writes.