Skip to content

Tool Use & Function Calling

How models call your code — schema design, validation, side effects, and why tool descriptions are the interface.

Intermediate17 minUpdated 27 July 2026

A model cannot query your database, send an email or check today's price. It generates text.

Tool calling bridges that: you describe functions the model may use, and instead of answering it can emit a structured request to call one. Your code executes it and returns the result, which the model then reasons over.

This is the mechanism behind every agent, and most of its reliability comes from decisions you make about the tool interface — not from the model.

The mechanics

You supply tool definitions with the request:

{
  "name": "get_order_status",
  "description": "Look up the current status of a customer order by its ID.",
  "parameters": {
    "type": "object",
    "properties": {
      "order_id": { "type": "string", "description": "Order ID, e.g. ord_8842" }
    },
    "required": ["order_id"]
  }
}

The model responds either with text, or with a call: get_order_status({ order_id: "ord_8842" }). Your code executes it — the model never runs anything — and you send the result back for the model to continue.

The critical point: a tool call is a request, not an instruction you must obey. It arrives as untrusted input and deserves the same treatment as a request from a browser.

The description is the interface

The single highest-leverage thing in this lesson.

The model chooses tools based almost entirely on their names and descriptions. A vague description produces wrong tool choices, and no amount of system-prompt instruction compensates.

// Poor — when would a model use this?
{ "name": "process", "description": "Processes data" }

// Good — states purpose, scope and boundaries
{
  "name": "search_orders",
  "description": "Search a customer's past orders by date range or status. Returns at most 20 orders, newest first. Use for questions about order history. Does NOT modify orders — use update_order for that."
}

Three things good descriptions do: say when to use it, state what it returns, and name what it does not do. That last one prevents a large share of misuse.

Parameter descriptions matter equally. "date" invites a dozen formats; "Start date in ISO 8601 format, e.g. 2026-07-27" gets one.

Designing the tool set

Few, well-chosen tools beat many. Selection accuracy degrades as the set grows, and every definition consumes context on every request. Six good tools outperform thirty overlapping ones.

Avoid overlap. If two tools could plausibly serve the same request, the model will sometimes pick the wrong one. Merge them or sharpen the boundary.

Match the task, not your API surface. Do not expose twelve CRUD endpoints and hope the model composes them. Expose reschedule_appointment, which internally does the reads and writes. Coarse, task-shaped tools produce far more reliable behaviour than fine-grained ones — fewer steps, fewer chances to compound errors.

Return what the model needs, and no more. A tool returning a 200-field object floods the context. Return the handful of fields relevant to the decision.

Validation is not optional

Conceptual diagramnoyesnoyesyesnoModel emits tool callSchema valid?Return structured error the model can readCaller authorised for this object?Deny, logWrite with side effects?Require idempotency keyExecuteReturn result as an observation
Diagram, as text
  • Model emits tool call leads to Schema valid?
  • Schema valid? leads to Return structured error the model can read (no)
  • Schema valid? leads to Caller authorised for this object? (yes)
  • Caller authorised for this object? leads to Deny, log (no)
  • Caller authorised for this object? leads to Write with side effects? (yes)
  • Write with side effects? leads to Require idempotency key (yes)
  • Write with side effects? leads to Execute (no)
  • Require idempotency key leads to Execute
  • Execute leads to Return result as an observation
  • Return structured error the model can read leads to Model emits tool call

The model produces arguments by generating text. They can be malformed, out of range, or entirely invented — a plausible-looking order_id for an order that never existed.

Validate every call as untrusted input, exactly as in the DTO lesson:

const schema = z.object({
  order_id: z.string().regex(/^ord_[a-z0-9]{4,}$/),
  limit: z.number().int().min(1).max(20).default(10),
});

const parsed = schema.safeParse(args);
if (!parsed.success) {
  return { error: "invalid_arguments", detail: parsed.error.issues };
}

Authorisation belongs here too. The tool must check that this user may access this order — the object-level authorisation from the security lesson. A model asked to only fetch the current user's orders is not an access control.

Errors are messages, not exceptions

When a tool fails, the model reads the result and decides what to do. So error text is a prompt, and its wording changes behaviour.

// Unhelpful — the model cannot act on this
{ "error": "Error 500" }

// Actionable — the model can correct itself
{ "error": "order_not_found",
  "message": "No order with ID ord_9999. Order IDs start with 'ord_' followed by digits. Use search_orders to find a valid ID." }

The second recovers; the first loops or hallucinates.

Distinguish the two kinds of failure. Retryable — a timeout or rate limit — should say so, and your code should apply backoff rather than letting the model retry in a tight loop. Terminal — invalid input, not found, forbidden — should say clearly that retrying will not help, or the model will try the same call repeatedly.

Side effects and idempotency

The dangerous category. A tool that only reads is recoverable; a tool that acts is not.

Every write tool needs an idempotency key, generated per logical action and passed through. Models retry, frameworks retry, and users resubmit — the reasoning from the API design lesson applies directly, and here the caller is less predictable than any client you have designed for.

Confirm before consequential actions. Return a proposed action for approval rather than performing it, for anything irreversible or externally visible.

Enforce limits in the tool. Refund caps, rate limits and permission checks live in code. Nothing in a prompt is a control.

Parallel calls and ordering

Models can request several tools at once. Where the calls are independent, executing them in parallel is a substantial latency win — three sequential lookups become one round.

But only parallelise reads, or writes you have proven independent. Two writes touching the same record concurrently is the race from the concurrency lesson, now initiated by a model that has no concept of transactions.

Testing

Tools are ordinary code and should be tested as such — but you also need to test selection: given a realistic request, does the model choose the right tool with the right arguments?

Build a small labelled set of requests mapped to expected tool calls, and run it whenever you change a description, add a tool, or change model. Tool selection is remarkably sensitive to all three, and this suite catches regressions that end-to-end testing hides.

Check your understanding

Common mistakes

Vague descriptions. The most common cause of wrong tool choice, and the cheapest to fix.

Exposing raw CRUD. Task-shaped tools produce far more reliable behaviour than primitives the model must compose.

Trusting arguments. They are generated text. Validate and authorise.

Returning entire objects. Floods context and buries the useful field.

Opaque errors. The model reads them; they should tell it what to do.

No idempotency on writes. Retries duplicate side effects.

Parallelising writes. Concurrent conflicting actions with no coordination.

Not testing tool selection. Small description changes silently alter behaviour.

Practice

Design the tool set for an agent that manages a customer's subscription: view details, change plan, apply a discount, cancel. Specify granularity, validation and which need idempotency.

Write your own answer first, on paper or in a note. Then compare.

Show a hint

Decide what a single tool call should accomplish before writing any descriptions.

Show a strong answer

Granularity: one tool per business operation, not one per database table and not one giant manage_subscription(action, ...). A single generic tool pushes the decision into a string argument the model must get right without any schema help; separate tools let the schema enforce what each needs.

The set:

  • get_subscription(customer_id) — read-only. No approval, freely retried.
  • change_plan(customer_id, new_plan_id, effective) — write. new_plan_id constrained to an enumeration, not free text, so an invented plan name fails at validation rather than reaching the billing system.
  • apply_discount(customer_id, discount_code, idempotency_key) — write, with money implications.
  • cancel_subscription(customer_id, reason, effective_date) — write, high consequence.

Validation, which is not optional:

  • Every argument checked server-side against a schema before anything executes. The model's output is untrusted input; a plan ID it invented must be rejected by the tool, not caught later by billing.
  • Authorisation inside the tool. The tool must verify the acting user may modify this customer's subscription — the agent's context is not an authorisation decision, and an injected instruction can make it request anything.
  • Business rules enforced in the tool, not described in the prompt. "Discounts cannot exceed 50%" belongs in code where it is guaranteed.

Which need idempotency: all three writes. Agent loops retry, and a retried apply_discount that stacks is a real financial loss. Take an idempotency key derived from the task, not generated per call — a key created at call time changes on retry and protects nothing.

One tool I would deliberately not provide: execute_sql. It is tempting because it covers every case, and it hands the model unbounded capability with no schema, no validation and no authorisation boundary.

An agent has 25 tools available and its accuracy in choosing correctly has dropped noticeably. Give three fixes and the trade-off of each.

Write your own answer first, on paper or in a note. Then compare.

Show a hint

Twenty-five schemas is also a large amount of context.

Show a strong answer
  • Reduce the set by merging near-duplicates. Twenty-five tools frequently contain several that differ only in a parameter — search_by_name, search_by_email, search_by_phone become one search(field, value). Trade-off: a more general tool has a weaker schema, so validation must work harder, and the model can supply an invalid field. Usually worth it below about ten tools.
  • Select tools per turn rather than sending all of them. Use a cheap classifier or the conversation state to decide which subset is plausibly relevant and send only those — typically five or six. Trade-off: if the selector is wrong, the agent cannot do the task at all, and the failure is confusing because the capability exists but is invisible. Needs a fallback that widens the set on failure.
  • Group tools behind a hierarchy. A first call chooses a category, a second chooses within it. Trade-off: an extra round trip on every task, so latency and cost rise for the common case to help the rare one.

What I would do first, before any of these: check whether the problem is tool count or tool descriptions. Twenty-five well-differentiated tools can work better than eight ambiguous ones. Look at the confusion pairs in the logs — which tool was chosen when which was correct — and rewrite those descriptions. It is the cheapest fix and often sufficient.

The context cost worth noting: 25 tool schemas can be several thousand tokens on every single call, which is both a bill and a dilution of everything else in the window. Even if accuracy were fine, per-turn selection would pay for itself.

Key takeaways

  • The model requests a call; your code executes it. Treat every call as untrusted input.
  • Descriptions are the interface — state when to use the tool, what it returns, and what it does not do.
  • Few, task-shaped tools beat many fine-grained ones: better selection, fewer steps, less compounding error.
  • Validate and authorise inside the tool, including object-level permission checks.
  • Error messages are prompts — make them actionable, and distinguish retryable from terminal.
  • Write tools need idempotency keys, confirmation for consequential actions, and limits enforced in code.
  • Parallelise reads only; concurrent writes reintroduce races with no coordination.
  • Test tool selection with a labelled set — it is sensitive to descriptions, tool count and model version.

Further reading

  • Timo Schick et al., "Toolformer: Language Models Can Teach Themselves to Use Tools" (NeurIPS 2023).
  • Shunyu Yao et al., "ReAct: Synergizing Reasoning and Acting in Language Models" (ICLR 2023).
  • Anthropic, "Building effective agents" (2024) — tool design as the main lever.
  • Pat Helland, "Idempotence Is Not a Medical Condition" (ACM Queue, 2012) — why retried tool calls need keys.
  • OWASP, Top 10 for Large Language Model Applications (2023) — excessive agency and insecure output handling.

Check yourself

3 questions in this lesson. Answer each once to complete it.

No problem practices this lesson yet. Browse the practice problems

Next lessonAI Harness Engineering
Lessons in this track