AI tool calling in production.

The model chooses the call; your code decides whether it runs, how often, and with what authority. This guide covers tool contracts, retry semantics and the failure modes that matter once tools change real records.

Engineering guide8 min readUpdated 19 October 2026

Short answer

AI tool calling is when a language model returns a structured request to run one of your functions, and your application decides whether and how to execute it. In production, each tool needs a narrow contract: typed arguments, a declared side-effect class, an idempotency key for writes, bounded results, errors the model can act on, and an approval rule for anything irreversible or external.

  • Classify every tool as read, reversible write, or irreversible or external effect.
  • A timeout on a write is ambiguous: check status before retrying.
  • Derive idempotency keys from run ID, tool name and canonical arguments.
  • Accept record IDs only if an earlier tool result in the run returned them.
  • Keep fewer than about 20 tools visible per turn, as OpenAI suggests.

What a production tool contract contains

A tool is an API whose caller is a probabilistic model, so its contract has to carry more than a name and a JSON Schema. The model will sometimes pick the wrong tool, invent an argument, or call the same thing twice. The contract decides what happens next.

Name
A narrow verb and object, such as create_draft_invoice, never a generic call_api(method, path, body).
Purpose
One sentence on what it does and one on when not to use it.
Arguments
Typed, with enums for closed sets. Nothing the code already knows (tenant, user, current record) is a model argument.
Side-effect class
Read, reversible write, or irreversible or external effect. This drives retries and approvals.
Idempotency
How the key is derived, and where the outcome is stored.
Timeout
A per-tool limit, inside an overall run budget.
Result
IDs plus the few fields the next step needs, with a size cap and a pagination cursor.
Errors
A machine-readable code, a plain message, a hint for what to do next, and whether a retry can help.

OpenAI’s function calling guide puts one of these rules plainly: don’t make the model fill arguments you already know. If the session already has an order ID, submit_refund() takes no ID and your code supplies it. Every argument you remove is one the model cannot get wrong.

Turn on strict schemas and keep the tool list short

Strict mode makes tool arguments conform to the schema during generation instead of on a best-effort basis. As documented on 19 October 2026:

  • OpenAI recommends always enabling strict: true. If you omit it, the Responses API tries to normalise your schema to strict and silently falls back to best effort when it cannot; the response then shows strict: false, so assert on it in tests. Strict mode inherits the structured output schema rules: every field required, additionalProperties: false.
  • Anthropic supports strict: true on custom tools, with a combined budget per request of 20 strict tools, 24 optional parameters and 16 union-typed parameters.

Tool count is a quality setting, not just a cost. OpenAI suggests fewer than 20 functions available at the start of a turn, with tool search to defer the rest, and notes that definitions are injected into the context and billed as input tokens. Keep definitions byte-stable between requests so prompt caching keeps working, and version them like any API.

Classify side effects before you design retries and approvals

The side-effect class of a tool decides how it may be retried and who must approve it. Get this table agreed with the business owner before any code is written; it is the core of the permission model.

Class Examples Automatic retry Approval Idempotency key
Read search_contacts, get_invoice, list_open_jobs Yes, bounded, with backoff None Optional (useful for caching)
Reversible internal write create_draft_invoice, add_crm_note, set_task_status Only with the same key None, or sampled review Required
Irreversible or external effect send_email, approve_payment, delete_record, post_reply_to_customer Never blindly; check status first A person approves each action or batch Required, plus downstream dedupe

In the systems we run, the agent can draft almost anything, and a person or a deterministic rule releases anything that leaves the building. That split keeps the agent useful while the expensive mistakes stay behind a human decision.

Which failures are safe to retry

A timeout on a write is ambiguous: the call may have succeeded after your client gave up. Retrying it without a key is how customers get two emails and suppliers get paid twice.

Failure Read tool Reversible write (keyed) External effect
Timeout, no response Retry with backoff Retry with the same key Query status by key, then decide
429 rate limit Wait for Retry-After, retry Same Same, within the approval window
5xx from the downstream system Retry with backoff Retry with the same key Status check first
4xx validation error Return error to the model Return error to the model Return error to the model
Model repeats an identical call Serve the stored result Replay the stored result Block and flag

Transport retries belong in your code, not the model’s reasoning. Model-level retries are for errors the model can fix, such as a malformed date. Anthropic’s documentation notes that Claude retries an invalid tool request two to three times with corrections, so cap attempts in your loop rather than relying on it to stop. Broader patterns for partial failure are in our guide to agent retry and failure recovery.

An idempotent tool wrapper

The wrapper below derives a key from the run, the tool and the canonical arguments, records a pending state before executing, and replays the stored outcome on repeats. Pass the key downstream when the target API accepts an idempotency header; otherwise store it in a reference field and look it up before writing.

import hashlib, json, time
from dataclasses import dataclass, asdict

MAX_RESULT_CHARS = 8000

@dataclass
class ToolError:
    code: str          # "not_found", "rate_limited", "in_doubt", "needs_approval"
    message: str       # what went wrong, in plain words
    hint: str          # what the model should do next
    retryable: bool

def idem_key(run_id: str, tool: str, args: dict) -> str:
    canonical = json.dumps(args, sort_keys=True, separators=(",", ":"))
    return hashlib.sha256(f"{run_id}|{tool}|{canonical}".encode()).hexdigest()

def run_tool(store, run_id, tool, args, side_effect, fn, timeout_s=20):
    key = idem_key(run_id, tool, args)
    prior = store.get(key)
    if prior and prior["state"] == "done":
        return prior["result"]                         # repeat call: replay, never re-execute
    if prior and prior["state"] == "pending" and side_effect != "read":
        return asdict(ToolError("in_doubt", "An earlier attempt may have completed.",
                                "Call get_action_status with this key first.", False))
    store.put(key, {"state": "pending", "started": time.time()})
    try:
        result = fn(**args, idempotency_key=key, timeout=timeout_s)
    except TimeoutError:
        return asdict(ToolError("timeout", f"{tool} did not respond in {timeout_s}s.",
                                "Check status before retrying.", side_effect == "read"))
    text = json.dumps(result, default=str)
    if len(text) > MAX_RESULT_CHARS:
        text = text[:MAX_RESULT_CHARS] + ' [truncated: narrow the query or use next_cursor]'
    store.put(key, {"state": "done", "result": text})
    return text

The error shape matters as much as the success path. Anthropic’s tool documentation recommends instructive errors (“Rate limit exceeded. Retry after 60 seconds.”) returned with is_error: true, because a bare “failed” gives the model nothing to recover with.

Parallel calls, timeouts and result size

Allow parallel calls for reads and serialise writes. Both major APIs let you turn parallelism off: parallel_tool_calls: false on OpenAI ensures zero or one call per turn, and disable_parallel_tool_use does the same on Claude. Two writes issued in the same turn have no defined order, and a later one may depend on an earlier one’s result.

Set a timeout per tool and a budget per run: maximum steps, maximum wall-clock time and maximum spend. A run that hits its budget stops and reports where it got to; it does not keep calling.

Keep results small. Return identifiers, the fields the next step needs and a cursor, not whole records or raw HTML. Large results crowd the context, raise cost and carry more untrusted text. Every tool result is untrusted input to the next model call, which is why tool outputs are the main entry point for indirect prompt injection.

Failure modes seen in production and how to detect them

Most tool-calling failures are visible in traces if you log the right fields: tool name, canonical arguments, idempotency key, side-effect class, outcome and latency per call.

Failure What it looks like How to detect it Control
Wrong tool search_suppliers used for a customer Evaluation set with the expected tool per case Sharper descriptions, fewer tools, allowed-tools lists
Hallucinated argument A plausible record ID never returned by any tool Provenance check: IDs must appear in earlier results in the run Reject unseen IDs with a hint to search first
Loop Same tool and arguments five times Count repeats per run; alert on the step budget Replay stored results, stop at the budget
Duplicate side effect Two identical emails after a timeout Idempotency store; daily reconciliation against the target system Keys plus status checks
Stale read A write based on data read many steps earlier Version or ETag mismatch on write Optimistic concurrency, re-read before writing
Misuse of a legitimate tool A CRM export followed by an external send Monitor chains such as sensitive read then external write Egress allowlists, approval on external sends

The last row is OWASP’s ASI02 Tool Misuse and Exploitation from the Top 10 for Agentic Applications 2026: an agent acting within its granted privileges but applying a legitimate tool unsafely. Its recommended controls (per-tool least-privilege profiles, action-level approval with a dry-run diff, budgets, and immutable logs of every invocation) map directly onto the contract above.

When the same tools need to serve several AI clients, publish them through an MCP server for your business systems with the same contracts. For non-technical staff, our lesson on understanding AI tool permissions explains the side-effect classes in plain terms. This is the core of how we approach AI agent development and AI integration work.

  • Every tool has a narrow verb name, a purpose line and a “do not use for” line
  • Arguments the code already knows are removed from the schema
  • Strict schemas are on, and tests assert the API did not fall back to non-strict
  • Each tool is classified as read, reversible write or external effect
  • Writes carry an idempotency key derived from run, tool and canonical arguments
  • Timeouts on writes trigger a status check, never a blind retry
  • Parallel calls are allowed for reads only
  • Results are capped in size and return IDs plus a cursor
  • Errors return a code, message, hint and retryable flag
  • Record IDs are accepted only if an earlier result in the run returned them
  • Runs have step, time and spend budgets
  • Traces record tool, arguments, key, class, outcome and latency for every call

Sources

  1. Function calling, OpenAI API documentation, accessed 19 October 2026
  2. Tool use with Claude, Claude API documentation (Anthropic), accessed 19 October 2026
  3. Handle tool calls, Claude API documentation (Anthropic), accessed 19 October 2026
  4. Strict tool use, Claude API documentation (Anthropic), accessed 19 October 2026
  5. OWASP Top 10 for Agentic Applications for 2026, OWASP GenAI Security Project, December 2025

Written by the Levy Street team in Wellington. Last updated 19 October 2026.

Questions people ask

What is AI tool calling?

Tool calling, also called function calling, lets a model respond with the name of a function and JSON arguments instead of text. Your application runs the function, returns the result, and the model continues. The model never executes anything itself, which is why the safety of a tool-using system depends on the code around the call: validation, permissions, retries and approvals.

How many tools should one agent have?

As few as the task needs. OpenAI’s function calling guide suggests aiming for fewer than 20 functions available at the start of a turn and deferring rarely used tools with tool search. Every definition also costs input tokens on every request. If an agent needs dozens of tools, split it by workflow or route requests to smaller, specialised agents.

Should the model retry failed tool calls by itself?

Only for errors it can fix, such as a missing or malformed argument. Return those as tool errors with a clear hint, and cap attempts. Transport failures, rate limits and timeouts should be retried by your code with backoff and idempotency keys, not by the model, because the model cannot tell whether a timed-out write already happened.

Do I need MCP to use tool calling?

No. Native function calling in the OpenAI, Anthropic and Gemini APIs is enough when one application owns its tools. MCP becomes useful when the same tools must serve several AI clients, or when a vendor already publishes an MCP server for its system. The contract rules in this guide apply either way.

How do I stop an agent sending the same email twice?

Give the send tool an idempotency key derived from the run and the canonical message, store the outcome before returning, and replay the stored result on any repeat call. Combine that with an approval step for external sends and a status check after timeouts. Prompt instructions alone will not prevent duplicates.

Building something that has to hold up?

We design, evaluate and run AI systems in production. Tell us what you are building and where it hurts.