Giving Agents Tools

Ma Mahalakshmi V Updated 16 Sep 2026
7 min read ·Lesson 49 of 224

Tools Work the Same Way, With Less Boilerplate

Unit 8 covered function calling in depth: defining a JSON Schema for a function's arguments, checking response.output for a function_call item, executing the actual Python function, and feeding the result back in. Every one of those underlying concepts still applies when an agent uses a tool — what changes is how much of that plumbing you have to write yourself. The Agents SDK turns an ordinary Python function into a usable tool with a single decorator, deriving the schema Unit 8, Lesson 2 taught you to write by hand directly from the function's own signature and docstring.

Defining a Tool

from agents import Agent, Runner, function_tool

@function_tool
def get_order_status(order_id: str) -> str:
    """Look up the current status of a customer's order.

    Args:
        order_id: The order identifier, e.g. "4471".
    """
    order_statuses = {"4471": "shipped", "5502": "processing"}
    return order_statuses.get(order_id, "order not found")

support_agent = Agent(
    name="Support Agent",
    instructions="You help customers check their order status.",
    model="gpt-5.6-terra",
    tools=[get_order_status],
)

result = Runner.run_sync(support_agent, "What's the status of order 4471?")
print(result.final_output)

Note: The exact decorator name, and how strictly it requires type hints and docstrings to build an accurate schema, can vary by SDK version. Confirm the current requirements against the current official documentation.

The @function_tool decorator is doing exactly the work Unit 8, Lesson 2 walked through by hand: inspecting get_order_status's parameter (order_id: str) and its docstring to build a JSON Schema tool definition equivalent to what you'd have written manually as a {"type": "function", "name": ..., "parameters": {...}} dictionary. This is the single biggest reduction in boilerplate the Agents SDK offers over the direct approach: the schema is derived from the function itself, rather than maintained as a separate, parallel description that has to be kept in sync with the function's actual signature by hand.

Why the Docstring Actually Matters Here

Unit 8, Lesson 2 emphasized writing clear, specific descriptions in a tool's schema, since the model relies entirely on that description to decide when and how to call it. With @function_tool, the docstring is that description — it's not documentation for other developers that happens to also be nice to have, it's the actual text the model sees when deciding whether this tool is relevant to a given request.

@function_tool
def issue_refund(order_id: str, amount: float) -> str:
    """Issue a refund for a specific order.

    Use this only after confirming the customer is eligible for a refund
    under the return policy. Do not use this for shipping cost adjustments.

    Args:
        order_id: The order identifier to refund.
        amount: The refund amount in US dollars.
    """
    return f"Refunded ${amount:.2f} for order {order_id}."

A vague docstring here — just "issues a refund" — would give the model far less to work with than the version above, which specifies exactly when the tool should and shouldn't be used, directly mirroring Unit 8, Lesson 2's guidance to write descriptions that disambiguate a tool from similar ones and specify its intended conditions of use, not just what it technically does.

Giving an Agent Several Tools

An agent can be given a list of tools, exactly as tools=[...] accepted a list of schemas in Unit 8, Lesson 4's multiple-tools pattern.

@function_tool
def get_return_policy() -> str:
    """Return the current return and refund policy text."""
    return "Items may be returned within 30 days of purchase for a full refund."

support_agent = Agent(
    name="Support Agent",
    instructions="You help customers with orders, refunds, and policy questions.",
    model="gpt-5.6-terra",
    tools=[get_order_status, issue_refund, get_return_policy],
)

The model chooses among these tools using exactly the same reasoning Unit 8, Lesson 4 described for multiple custom functions — comparing the user's request against each tool's name and description to decide which, if any, are relevant — the Agents SDK doesn't change this underlying decision process, only how the tools were defined and how the resulting calls get executed.

Handling a Tool That Fails

Unit 8, Lesson 5 covered treating a function's arguments as untrusted input and curating safe error messages rather than exposing raw exception details. The same discipline applies to a tool used by an agent, since an unhandled exception inside a tool function is just as much a real risk here as it was for a hand-written function-calling loop.

@function_tool
def get_order_status_safely(order_id: str) -> str:
    """Look up the current status of a customer's order.

    Args:
        order_id: The order identifier, e.g. "4471".
    """
    order_statuses = {"4471": "shipped", "5502": "processing"}
    if not order_id.isdigit():
        return "Invalid order ID format. Order IDs are numeric."
    return order_statuses.get(order_id, "No order found with that ID.")

Returning a clear, safe message for an invalid or missing order — rather than letting an unhandled KeyError or similar exception propagate — follows the same principle Unit 8, Lesson 5 established: a tool's failure should be curated into something the model (and, ultimately, the user) can act on sensibly, not a raw error that leaks implementation detail or produces a confusing final answer.

Combining Custom Tools With Built-In Tools

An agent isn't limited to @function_tool-decorated custom functions; it can also be given the platform's own built-in tools from Unit 9 — web search, file search, and Code Interpreter — in the same tools list.

from agents import WebSearchTool

research_agent = Agent(
    name="Research Agent",
    instructions="Answer questions using web search when they require current information.",
    model="gpt-5.6-terra",
    tools=[WebSearchTool()],
)

Note: The exact classes and names the Agents SDK uses to expose built-in tools (web search, file search, Code Interpreter) can vary by SDK version. Confirm the current interface against the current official documentation.

This is the same combination Unit 9, Lesson 4 covered for a single client.responses.create() call — custom functions and platform built-in tools registered together, with the model choosing the right one per sub-question — except here the combination is attached once to a reusable Agent rather than assembled fresh into a tools list on every individual call.

Testing a Tool Function Independently of the Agent

Following this course's dependency-injection testing pattern (used throughout Units 8 and 9), a @function_tool-decorated function's underlying logic can be tested directly, without running it through an agent or making any model call at all.

def test_get_order_status_returns_correct_status():
    order_statuses = {"4471": "shipped", "5502": "processing"}

    def get_order_status_logic(order_id: str) -> str:
        return order_statuses.get(order_id, "No order found with that ID.")

    assert get_order_status_logic("4471") == "shipped"
    assert get_order_status_logic("9999") == "No order found with that ID."
    print("PASS: order status lookup returns the correct result for known and unknown IDs")

test_get_order_status_returns_correct_status()

Decorating a function with @function_tool wraps it for use by an agent, but the function's actual business logic is still ordinary Python underneath — testing that logic directly, exactly as Unit 8's tests exercised the plain functions behind its own tool schemas, verifies correctness without needing a real agent run or a real model call for every test.

Common Mistakes

Writing a vague or missing docstring for a @function_tool-decorated function, when that docstring is the actual description the model uses to decide when and how to call the tool, not just documentation for other developers.

Letting a tool function raise an unhandled exception, exposing raw error details rather than returning a curated, safe message the model can act on sensibly.

Assuming a tool's schema needs to be written separately from the function itself, missing the point of @function_tool, which derives the schema directly from the function's signature and docstring.

Forgetting that everything learned about tool design in Unit 8 — clear descriptions, treating arguments as untrusted, least-privilege scoping — still applies, and treating the Agents SDK as though it makes careful tool design unnecessary.

Best Practices

Write a specific, unambiguous docstring for every @function_tool-decorated function, following the same tool-description guidance Unit 8, Lesson 2 established for hand-written schemas.

Return curated, safe error messages from a tool function rather than letting exceptions propagate, applying Unit 8, Lesson 5's error-handling discipline unchanged.

Combine custom tools and built-in tools in a single agent's tools list when a task genuinely needs both, following the same reasoning Unit 9, Lesson 4 established for combining tool categories in a direct API call.

Keep a tool function's actual implementation simple and focused, letting the function's type hints and docstring do the work of communicating its interface to the model, rather than requiring a separately maintained schema.

0 Comments

Reviewed before they appear

No comments yet.

OpenAI SDK
Introduction to the OpenAI SDK Setting Up Python Creating an API Key Your First Call — client.responses.create() and response.output_text Understanding Billing, Credits, and What a Request Costs Why Responses Replaced Chat Completions Anatomy of a Request: model, input, and instructions Anatomy of a Response: The Typed output Array, Not Just Text Roles: User, Assistant, and Developer/System Choosing a Model, and Reading the Models Page Instead of Memorizing Names Instructions vs. Input Writing Prompts That Get Consistent Results Few-Shot Examples Reasoning Models and the reasoning Parameter Debugging a Prompt That Misbehaves Why Streaming Matters for User Experience stream=True and Iterating Over Events Handling the Event Types You Actually Care About Background Mode for Long-Running Jobs Project — Add Live Streaming to Your Chatbot The Problem With Parsing Free Text JSON Schema and Strict Mode Pydantic Models With the SDK's Parse Helpers Handling Refusals and Validation Failures Project — A Resume-to-JSON Extractor Working With input_image input_file, PDFs, and the Files API Image Generation Speech-to-Text and Text-to-Speech Project: A PDF Question-Answering Script What Function Calling Is Defining a Tool Schema The Full Loop Multiple Tools Errors, Timeouts, and Untrusted Arguments Project: A Weather Assistant Web Search File Search and Vector Stores Code Interpreter Remote MCP Servers and Connectors Project: A Research Assistant What an Embedding Is, Without the Maths Generating and Storing Embeddings Similarity Search From Scratch Hosted Vector Stores vs. Rolling Your Own A Small RAG App Over a Folder of Notes Agents vs. a Single API Call — When You Need One pip install openai Giving Agents Tools Handoffs and Multi-Agent Triage Guardrails and Approvals Tracing and Observing What Your Agent Did A Multi-Agent Support Desk Error Codes and What Each One Means Retries, Timeouts, and Backoff Rate Limits and Spend Limits Prompt Caching and Cost Optimisation The Batch API for Bulk Work Async Clients and Concurrency Moderation and Safety Best Practices Designing the App Backend With FastAPI Streaming to a Simple Frontend Deploying and a Cost/Safety Checklist Why Web Search Is Useful for Current Information Using the Web Search Tool with the Responses API Configuring Search Behavior for Application Use Cases Understanding Citations and Source Attribution Where to Go Next Building a Research Assistant with Web Search Combining Web Search with Structured Outputs Handling Conflicting or Low-Quality Web Sources Reducing Unsupported Claims with Grounded Generation Testing Freshness-Sensitive AI Answers Production Considerations for Web-Grounded Applications Understanding File Search and Retrieval-Augmented Generation Creating and Organizing Vector Stores Uploading Documents for Retrieval Connecting Vector Stores to Responses API Requests Designing Document Metadata and Filtering Strategies Building a PDF Question-Answering Application Improving Retrieval Quality With Better Document Preparation Handling Missing Evidence and Retrieval Failures Combining File Search With Web Search Building a Production Knowledge-Base Assistant What the Code Interpreter Tool Is Designed For Running Python-Based Analysis Through the OpenAI SDK Uploading Datasets for Analysis Analyzing CSV and Spreadsheet Data Generating Charts and Data Summaries Handling Generated Files and Downloadable Artifacts Building a Data-Analysis Assistant Combining Code Execution with Structured Outputs Validating Generated Calculations and Results Security and Sandbox Considerations for Code Execution Understanding Multimodal Input with the OpenAI SDK Sending Images to a Model Image Analysis from URLs and Uploaded Files Extracting Text and Information from Screenshots Building an Image-Question-Answering Application Combining Image Input with Structured Output Analyzing Multiple Images in One Request Handling Image Quality and Input Limitations Designing Multimodal Prompts for Reliable Results Building a Practical Vision-Powered Python Application Understanding Speech-to-Text and Text-to-Speech Workflows Transcribing Audio with the OpenAI SDK Working with Uploaded Audio Files Handling Timestamps and Transcription Metadata Building a Meeting Transcription Workflow Generating Spoken Responses from Text Handling Long Audio and Processing Failures Combining Audio with Text and Tool Calling Building an End-to-End Python Voice Application What Embeddings Are and When to Use Them Generating Embeddings With the OpenAI API Preparing Text for Embedding Comparing Vectors With Cosine Similarity Building a Simple Semantic Search Engine in Python Storing Embeddings in a Database Metadata Filtering for Semantic Search Chunking Strategies for Better Retrieval Evaluating Semantic Search Quality Building a Document Similarity Application When Batch Processing Makes Sense Designing Large-Volume AI Processing Pipelines Using Asynchronous Python with the OpenAI SDK Running Concurrent Requests Safely Controlling Concurrency and Avoiding Rate Limits Tracking Batch Job Progress Handling Partial Failures in Bulk Workloads Retrying Failed Items Without Duplicating Successful Work Designing Resumable AI Processing Jobs Building a Production Batch-Processing Pipeline Batch Processing Makes Sense Large-Scale AI Processing Pipelines Async Python with OpenAI SDK Safe Concurrent Requests Concurrency & Rate Limits Batch Progress Tracking Partial Failure Handling Safe Retry Handling Resumable AI Jobs Production Batch Pipeline System–User Data Separation Reusable App Instructions Prompt Templates & Variables Extraction & Classification Prompts Summarization & Transformation Prompts Explicit Output Requirements Prompt Version Management Prompt Testing & Evaluation Reusable Python Prompt Library API Key Security Secure API Key Storage Secure Secret Management Prompt Injection Prevention Trusted vs. Untrusted Content Tool Argument Validation Sensitive Data Handling Secure Logging AI Action Authorization Production AI Security Checklist Why AI Applications Need Evaluation Beyond Unit Tests Unit Testing OpenAI SDK Integration Code Mocking API Responses in Python Tests Testing Structured Outputs Against Schemas Testing Tool-Calling Workflows Building a Small Evaluation Dataset Measuring Accuracy, Consistency, and Failure Rates Regression Testing Prompts and Model Changes Human Evaluation Versus Automated Evaluation Creating a Repeatable Evaluation Pipeline AI Request Monitoring Token Cost Management Usage Metrics Design Reducing Model Calls Prompt & Context Optimization Model Selection & Optimization AI Caching Strategies Interactive Latency Optimization Usage Dashboards & Budget Alerts Performance & Cost Checklist Every API Call Starts Fresh Fixing API Statelessness Server-Side Conversation Memory Limits of Response Chaining What We're Building Conversation Memory Challenges Preparing an OpenAI SDK Application for Deployment Environment-Specific Configuration for Development and Production Deploying a Python AI Service with Docker Container Health Checks and Startup Configuration Managing Secrets in Cloud Deployments Background Workers for Long-Running AI Tasks Queues and Asynchronous Job Architectures Scaling AI Workloads Horizontally Monitoring Production Incidents and Failures Production Deployment Checklist for OpenAI SDK Applications Reusable OpenAI Service Classes AI Client Dependency Injection Typed AI Responses Python Configuration Management AI Request Decorators Centralized AI Error Handling Clean SDK Abstractions Reusable OpenAI Utilities Internal AI Python Libraries SDK Integration Maintenance Production AI Chatbot Document Q&A System Web Research Assistant Customer Support Agent AI Data Analysis Assistant Image Analysis App Meeting Transcription & Summary Semantic Document Search Multi-Tool AI Agent Production OpenAI SDK App Why "It Looked Fine When I Tested It" Isn't Enough Timing Note Status Note Pre-Decision Status Note Current Availability Note
Ask about this post
AI Ask about this post

Ask questions about Giving Agents Tools and get answers drawn from it.

Signed-in readers only.