Tools

A tool lets the model call your Python code, such as an API call, a database query or a calculation. The SDK describes the tool to the model, runs the function when the model calls it, and sends the result back.

Write a tool

Write a tool

Decorate a function with @agents.tool and pass it in tools=. Here the agent reads an order with it:

import asyncio

from mistralai import agents

@agents.tool
async def get_order(order_id: str) -> dict[str, str]:
    """Get the status of an order."""
    return {"order_id": order_id, "status": "shipped on Monday"}  # read from your database

agent = agents.Agent(
    model="mistral-large-4",
    instructions="Answer questions about orders in one plain sentence.",
    tools=[get_order],
)

async def main() -> None:
    state = await agent.run("Has order 1042 shipped?")
    if isinstance(state, agents.Completed):
        print(state.text)
        print(state.tool_calls)

if __name__ == "__main__":
    asyncio.run(main())
Output
Yes, order 1042 has shipped — its status is "shipped on Monday."
(ToolCall(name='client.get_order', arguments={'order_id': '1042'}),)

The model called get_order once, and the turn's record lists the call under the tool's id, client.get_order: client is the default namespace of your own tools.

The SDK builds what the model reads from the function itself. It takes the name from the function's name, the description from its docstring, and the input schema from its typed parameters. Annotate the return type too: it becomes the output schema.

The decorated function stays callable, so you can test it like any other function with await get_order("1042"). Define tools at module level, since the decorator raises ValueError for a nested function or a lambda.

Do not block in an async def tool, for example with time.sleep. Write a blocking tool as a plain def, which runs in its own thread.

Describe the arguments

Describe the arguments

Describe an argument to the model with Annotated and a Pydantic Field:

from typing import Annotated

from pydantic import Field

from mistralai import agents

@agents.tool
async def search_issues(
    query: Annotated[str, Field(description="Full-text query, such as 'login timeout'.")],
    limit: int = 10,
) -> list[str]:
    """Search the issue tracker."""
    return [f"#{n}: {query}" for n in range(limit)]  # call your tracker here

Every parameter needs a type annotation. Pass name=, description= or input_schema= to override what the function gives.

Report a failure to the model

Report a failure to the model

A failure you expect, such as an unknown invoice, is part of the tool's answer. Raise agents.ToolError with a message for the model. The model reads that message as written, and can act on it:

import asyncio

from mistralai import agents

INVOICES = {"A-1": {"id": "A-1", "status": "paid"}}

@agents.tool
async def get_invoice(invoice_id: str) -> dict[str, str]:
    """Fetch an invoice."""
    if invoice_id not in INVOICES:
        raise agents.ToolError(f"No invoice {invoice_id}. Ask the user to check the number.")
    return INVOICES[invoice_id]

agent = agents.Agent(model="mistral-large-4", instructions="Answer in one plain sentence.", tools=[get_invoice])

async def main() -> None:
    state = await agent.run("What is the status of invoice B-7?")
    if isinstance(state, agents.Completed):
        print(state.text)

asyncio.run(main())
Output
I wasn't able to find invoice B-7 in the system — the lookup returned "No invoice B-7" for that ID and several common variations (B7, B-007, INV-B-7, etc.). Could you double-check the invoice number? It may have a different format (e.g., a different prefix, more digits, or a typo).

The model asked the user to check the number, as the message told it to.

An unexpected error, any other exception, is a bug or an outage. The SDK logs it with its traceback (deployed, in your deployment's logs), and by default the model reads only The tool failed with an unexpected error., since a traceback can hold internal names or customer data. Set tool_errors="show" on the agent while you develop to show the model the exception, or tool_errors="raise" to fail the turn instead.

With "raise", your code handles the failure instead of the model:

import asyncio

from mistralai import agents

@agents.tool
async def get_region_price(region: str) -> int:
    """Price of the plan in a region."""
    return {"us-east": 10}[region]  # raises KeyError for any other region

agent = agents.Agent(model="mistral-large-4", tools=[get_region_price], tool_errors="raise")

async def main() -> None:
    match await agent.run("What does the plan cost in eu-west?"):
        case agents.Failed(error=error):
            print(error.code, error.message)
            #> tool_raised Tool 'get_region_price' raised KeyError
        case agents.Completed(text=text):
            print(text)

asyncio.run(main())

error.message gives the tool's name and the exception's class, here KeyError. It leaves out the exception's own text, 'eu-west', because that text can hold internal names or customer data. The full exception and its traceback are in the SDK's log.

A result that does not match the tool's output_schema= is not an unexpected error. The SDK logs a warning. With "hide" and "show", the model reads which field is wrong. With "raise", the turn ends Failed with error code tool_output_schema_mismatch.

How the model calls a tool

How the model calls a tool

By default, the model calls your tools from code it writes in the built-in run_typescript tool, as tools.client.<name>(...), so it can chain several calls in one step.

JSON TOOL CALLSModelmistral-large-4model steps: 0SENDSSEESHARNESS: RUNS EACH CALL↓ JSON callsJSON results ↑TOOLS≈ 1,000 tokens24 invoices, ≈ 36,000 tokens{"name": "list_invoices", "arguments": {"account": "FR76"}}▸ list_invoices {"account": "FR76"}list_invoices {"account": "FR76"}{"name": "read_invoice", "arguments": {"id": "INV-0401"}}{"name": "read_invoice", "arguments": {"id": "INV-0402"}}{"name": "read_invoice", "arguments": {"id": "INV-0403"}}… 24 calls in one send{"name": "read_invoice", "arguments": {"id": "INV-0424"}}▸ read_invoice {"id": …} × 24read_invoice {"id": …} × 24{"name": "screen_sanctions", "arguments": {"name": "Northwind Trading"}}{"name": "screen_sanctions", "arguments": {"name": "Northwind Trading GmbH"}}▸ screen_sanctions {"name": …} × 2screen_sanctions {"name": …} × 2["INV-0401", …]{"id": …, "text": "…"} × 24{"matches": true} × 2["INV-0401", …, "INV-0424"]{"id": "INV-0401", "payee": …, "text": "…"} × 24≈ 36,000{"name": "Northwind Trading", "matches": true} × 22 invoices to Northwind Trading; a sanctions matchlist_invoices× 0read_invoice× 0screen_sanctions× 0PROGRAMMATIC TOOL CALLSModelmistral-large-4model steps: 0SENDSSEESrun_typescript: RUNS THE PROGRAM↓ a programprinted lines ↑TOOLS≈ 1,000 tokens{"name": "run_typescript", "arguments": {"code": "async function main() { const c = tools.client; const ids = await c.list_invoices({account: "FR76"}); console.log(`invoices: ${ids.length}`); const docs = await Promise.all(ids.map( (id) => c.read_invoice({id}))); console.log(`read: ${docs.length}`); const hits = docs.filter( (d) => d.text.includes("Northwind")); console.log(`Northwind: ${hits.length}`); const checks = await Promise.all(hits.map( (h) => c.screen_sanctions({name: h.payee}))); return hits.map((h, i) => [h.id, checks[i].matches]);}"}}▸ run_typescript {"code": "async function main() {…}"}{"name": "run_typescript", "arguments": {"code": "async function main() { const c = tools.client; const ids = await c.list_invoices({account: "FR76"}); console.log(`invoices: ${ids.length}`); const docs = await Promise.all(ids.map( (id) => c.read_invoice({id}))); console.log(`read: ${docs.length}`); const hits = docs.filter( (d) => d.text.includes("Northwind")); console.log(`Northwind: ${hits.length}`); const checks = await Promise.all(hits.map( (h) => c.screen_sanctions({name: h.payee}))); return hits.map((h, i) => [h.id, checks[i].matches]);}"}}› invoices: 24› invoices: 24› read: 24› read: 24› Northwind: 2› Northwind: 2← 2 rows← [["INV-0412", true], ["INV-0419", true]]2 invoices to Northwind Trading; a sanctions matchlist_invoices× 0read_invoice× 0screen_sanctions× 0
Step 1 of 21. Same task in both lanes. Context: ≈ 1,000 tokens each.

Set model_access="direct" to put a tool in the model's tool list instead:

from mistralai import agents

@agents.tool(model_access="direct")
async def get_weather(city: str) -> str:
    """Weather for a city."""
    return f"Sunny in {city}."
How a tool runs when deployed

How a tool runs when deployed

Deployed with Studio (self-hosted or Mistral-managed), a tool runs at most once. After a lost call, such as a crash, the model reads that the call may or may not have taken effect, so a tool with side effects must be safe to call twice. Pass the service it calls an idempotency key built from the request, so that a second call carries the same key:

@agents.tool
async def refund_order(order_id: str, amount_cents: int) -> str:
    """Refund an order."""
    refund = await payments.refunds.create(  # your payment service's client
        order_id=order_id,
        amount_cents=amount_cents,
        idempotency_key=f"refund-{order_id}",  # the same key when the model calls again
    )
    return f"Refund {refund.id} for order {order_id}: {refund.status}."

When the service takes no key, check first: read the order's refunds, and return the one you find instead of refunding again. A tool that only reads needs neither.

Client tools

Client tools

A client tool is a tool answered through a callback, not within the session. Use one when the answer exists only where the agent is used, such as the user's device or your UI; otherwise use your own tool, since a client tool makes the turn wait on your program. Declare it on a stub and give it to the agent like any tool:

from typing import Literal

from pydantic import BaseModel

from mistralai import agents

class Location(BaseModel):
    city: str
    country: str

@agents.client_tool
def get_location(precision: Literal["city", "street"] = "city") -> Location:
    """The user's current location."""
    raise NotImplementedError  # answered through a callback

When the model calls it, the turn waits for your program's answer. See Answer a client tool, and Callbacks to answer it from a session app. API reference: agents.client_tool.

your appFind a bakery near me,then ask me whether toorder there.Location shared: Lyon, FranceBoulangerie Paul12 rue de la RépubliqueWould you like to order there?NoYesI found Boulangerie Paul, 12rue de la République, Lyon,France, and you confirmed.THE SESSIONmistral-large-4client.get_locationclient.find_shopclient.confirmCompletedThe turn waits on each call untilyour program sends the result.
Step 1 of 5. The user asks your app for a bakery, and your app runs the agent.
Tool, connector or skill

Tool, connector or skill

You haveUse
A Python functionA tool (@agents.tool)
A hosted service or an MCP serverA connector or an MCP server
A written procedureA skill
Common issues

Common issues

SymptomFix
ValueError: Decorated tools must be defined at module scopeMove the function to module level.
agents.SdkError when the tool is definedAnnotate every parameter, and add a docstring or description=.
The model reads The tool failed with an unexpected error.Your tool raised. Read the traceback in your logs, or set tool_errors="show" while you develop.
The agent has no bash or file toolsThey come from the agent's environment. Give the agent one with environment=; see Environment.

Approvals: choose which tool calls wait for a person.