Tools and function calling
Describing tools so the model can use them, validating arguments, and making writes safe.
A tool is a described function
The model does not run your code. It emits a request naming a tool and arguments; your code executes it. Everything the model knows about the tool comes from the schema and the description, so those are part of your prompt.
{
"name": "lookup_order",
"description": "Find one order by its id. Use this before answering anything about order status.",
"parameters": {
"type": "object",
"properties": {
"order_id": { "type": "string", "description": "Order id, e.g. 'A-1042'" }
},
"required": ["order_id"]
}
}- Say when to use it, not just what it does — that sentence drives selection.
- Constrain types and enums so invalid calls are impossible to express.
- Keep the number of tools small and their names unmistakable.
- Return structured results with stable keys; the model reads them like a document.
Read tools and write tools
| Class | Examples | Policy |
|---|---|---|
| Read | Search, fetch, query, list | Free to run; still validate and rate-limit |
| Write (reversible) | Create draft, add label | Allow, but log and make undo possible |
| Write (irreversible) | Send email, charge card, delete | Require confirmation or human approval |
| Privileged | Shell, arbitrary SQL, file write | Avoid; if unavoidable, sandbox and scope |
def send_email(to, subject, body):
if not re.fullmatch(r"[^@\s]+@[^@\s]+", to or ""):
return {"error": "invalid recipient"}
if len(body) > 5000:
return {"error": "body too long"}
# irreversible actions go through approval, never straight through
return {"status": "queued_for_approval", "to": to}⚠️
Never pass model-generated text straight into a shell, SQL string, filesystem path or URL. That is how prompt injection turns into remote code execution. Parameterise, allow-list, and treat every argument as untrusted input from a stranger.
Prompt injection
Anything an agent reads — a web page, an email, a file, a search result — can contain instructions aimed at the model. That is indirect prompt injection, and it is the defining security problem of tool-using agents.
- Assume fetched content is hostile. Never let it authorise actions.
- Keep the security decision outside the model: allow-lists, scopes, approvals enforced in code.
- Separate "data" from "instructions" in the prompt structure, and say explicitly that fetched content is data only.
- Log the provenance of every action so an incident can be traced to a document.
💡
A useful framing: the model is a user interface for your tools, and the interface is untrustworthy. Your authorisation layer must be correct even if the model is fully compromised.
FAQ
What should a tool return on failure?
A small structured object such as
{"error": "not found"} with a stable key, plus enough context to act on. Errors the model can read are recoverable; exceptions are not.Should tools be async?
Yes for I/O-bound work. Run independent calls concurrently, but keep write operations serialised so ordering stays predictable.
Related
The agent loop Memory and retrieval
Last refreshed 2026-09-18.