Guide

Anthropic's engineering guidance frames this as an agent–computer interface: the same care you put into a human CLI or API, but optimized for a model that is good at language and bad at guessing your internals.

Concept

Before you start: the model only sees the schema and the results you return. A 40 KB ORM dump wastes tokens and hides the one field it needs.

Principles that pay off

  • Fewer, higher-level tools. Prefer search_orders({query, status, limit}) over five micro-tools. The model spends fewer turns stitching steps.
  • Token-efficient outputs. Return the fields the model needs to continue, not raw dumps. Truncate long bodies; offer get_order_line_items(order_id) for detail.
  • Pagination. Always support limit / cursor. Cap defaults (for example 20). Tell the model how to fetch the next page: "next_cursor": "abc", "hint": "Pass next_cursor to continue".
  • Helpful errors. Include the invalid input, the constraint, and a fix. "status must be one of: open, shipped, cancelled" beats "InvalidArgument".
  • Stable shapes. Do not change field names between versions without a new tool name or a versioned schema. Prompt caches and evals depend on stability.

Trade-off

Low-level tools (run_sql, http_request) maximize flexibility and maximize damage. High-level tools (refund_order) are safer and easier to eval, but every new product capability needs a new tool. Default to high-level for user-facing agents. Reserve low-level for sandboxed coding agents with strong isolation.

Python
    # Bad: dumps 40 KB of JSON the model mostly ignores
    # return order.to_dict()

    # Better: compact, task-shaped payload
payload = {
    "order_id": "ORD-12345",
    "status": "processing",
    "eta": "2026-10-16",
    "tracking_url": None,
    "item_count": 2,
    "total_usd": "48.00",
}
print(payload)

Output:

{'order_id': 'ORD-12345', 'status': 'processing', 'eta': '2026-10-16', 'tracking_url': None, 'item_count': 2, 'total_usd': '48.00'}

Walking through a cancel

User: "Cancel ORD-12345 if it hasn't shipped, otherwise tell me the tracking link."

A competent model reads first.

Turn 1 — model

JSON
{"name": "get_order", "input": {"order_id": "ORD-12345"}}

Turn 1 — your result

JSON
{"order_id": "ORD-12345", "status": "processing", "cancellable": true, "tracking_url": null}

Turn 2 — model (this should hit an approval gate in your executor)

JSON
{"name": "cancel_order", "input": {"order_id": "ORD-12345", "reason": "customer_request"}}

If status had been shipped, the model should skip cancel and answer with tracking_url — no second tool. That branch is exactly what you put in an eval case: same prompt, fixture returning shipped, expect zero calls to cancel_order and a final answer containing the URL.

Common mistakes

  • Five micro-tools the model must chain when one search tool would do.
  • Returning the whole row. Project the fields the next turn needs.
  • "InvalidArgument" with no allowed values and no hint.

Check yourself

  1. Why include "cancellable": true on get_order instead of making the model infer it? The model then has an explicit signal and does not invent a rule from status prose.
  2. The fixture returns shipped. What must the eval assert? Zero cancel_order calls, and a final answer that contains the tracking URL.

Key takeaways

  • Prefer a small set of high-level, paginated, token-efficient tools.
  • Return compact, task-shaped JSON and recoverable errors.
  • Low-level escape hatches belong in sandboxes, not user-facing agents.
  • Eval the read-then-write branch, not only the happy path.

Further learning

Writing effective tools for agents (Anthropic)Writing effective tools for AI agents—using AI agentsanthropic.com

Next, how writes stay safe: safe tool execution.