Strict Tools

Guaranteed Valid Arguments

Without extra settings, the model's tool input usually matches your schema, but not always (a missing field, a string instead of a number). Add "strict": True to the tool and set "additionalProperties": False in the schema: the API then guarantees the input matches the schema exactly.

tools = [{
    "name": "create_return",
    "description": "Create a return request for a delivered order. Use only after the customer confirms the item and reason.",
    "strict": True,
    "input_schema": {
        "type": "object",
        "properties": {
            "order_id": {"type": "string"},
            "reason": {"type": "string", "enum": ["damaged", "wrong_item", "size_issue", "not_needed"]},
            "pickup": {"type": "boolean"},
        },
        "required": ["order_id", "reason", "pickup"],
        "additionalProperties": False,
    },
}]
response = client.messages.create(
    model="claude-sonnet-5-5", max_tokens=1000, tools=tools,
    messages=[{"role": "user", "content": "Return order SE-10423, wrong size. Use the create_return tool. Pickup please."}],
)
if not any(b.type == "tool_use" for b in response.content):
    ...   # auto does not guarantee a call: re-prompt or ask the customer

Note: On current models (Claude Opus 5.5, Sonnet 5.5) forcing a tool with tool_choice {"type": "any"} or {"type": "tool", ...} returns a 400 error. Keep the default auto, ask for the tool in the prompt, use strict: True, and check in code whether a tool_use block actually came back.

Business Checks Still Belong in Your Code

Example

A valid schema does not mean the action is allowed. Check the rules and return is_error results so the model can explain or ask again:

import json

ORDERS = {"SE-10423": {"status": "delivered", "days_since_delivery": 3},
          "SE-55555": {"status": "delivered", "days_since_delivery": 12}}

def create_return(order_id, reason, pickup):
    order = ORDERS.get(order_id)
    if order is None:
        return {"is_error": True, "content": f"Order {order_id} not found."}
    if order["days_since_delivery"] > 7:
        return {"is_error": True, "content": "Return window (7 days) has passed. Offer to connect a human agent."}
    return {"content": json.dumps({"return_id": "RET-" + order_id[-4:], "pickup": pickup})}

for args in [{"order_id": "SE-10423", "reason": "size_issue", "pickup": True},
             {"order_id": "SE-55555", "reason": "damaged", "pickup": True},
             {"order_id": "SE-00000", "reason": "damaged", "pickup": False}]:
    result = {"type": "tool_result", "tool_use_id": "toolu_x", **create_return(**args)}
    print(result)

Output

{'type': 'tool_result', 'tool_use_id': 'toolu_x', 'content': '{"return_id": "RET-0423", "pickup": true}'}
{'type': 'tool_result', 'tool_use_id': 'toolu_x', 'is_error': True, 'content': 'Return window (7 days) has passed. Offer to connect a human agent.'}
{'type': 'tool_result', 'tool_use_id': 'toolu_x', 'is_error': True, 'content': 'Order SE-00000 not found.'}