Introduction to Tool Use Patterns with OpenAI Agents SDK
The OpenAI Agents SDK provides a powerful framework for building AI agents that can interact with external systems through tools. Tool use is the mechanism that allows agents to extend their capabilities beyond text generation, enabling them to fetch data, perform calculations, call APIs, and execute custom logic. Understanding the various tool use patterns is essential for building robust, production-ready agents that can handle complex, multi-step tasks reliably.
In this complete guide, we will explore the core tool use patterns supported by the OpenAI Agents SDK, including function tools, hosted tools, computer use tools, and advanced composition strategies. Each pattern comes with practical code examples and best practices to help you implement them effectively.
What Is Tool Use in the Agents SDK?
Tool use refers to the ability of an AI agent to invoke external functions or services during a conversation. Instead of relying solely on its pretrained knowledge, the agent can call tools to retrieve real-time information, perform computations, manipulate files, or interact with third-party APIs. The Agents SDK abstracts much of the complexity involved in defining tools, routing tool calls, and handling responses.
The SDK supports several categories of tools:
- Function tools: Custom Python functions decorated and exposed to the agent as callable tools.
- Hosted tools: Built-in tools provided by OpenAI, such as web search and code execution.
- Computer use tools: Tools that allow the agent to interact with a virtual computer environment.
- Agent-as-tool: Using one agent as a tool that another agent can call.
Why Tool Use Patterns Matter
Tool use patterns matter because they determine how effectively an agent can solve real-world problems. A well-designed tool use pattern can dramatically improve accuracy, reduce hallucinations, and enable agents to complete tasks that would otherwise be impossible. Conversely, poorly designed tools can lead to infinite loops, incorrect results, and frustrating user experiences.
Key benefits of mastering tool use patterns include:
- Grounding in real data: Tools let agents access current information rather than relying on stale training data.
- Composability: Multiple tools can be combined to solve complex, multi-step problems.
- Extensibility: New capabilities can be added without retraining the model.
- Safety and control: Tools provide explicit boundaries on what an agent can and cannot do.
Getting Started: Installation and Setup
Before diving into patterns, ensure you have the Agents SDK installed and your environment configured. The SDK is available as a Python package and requires an OpenAI API key.
pip install openai-agents
Set your API key as an environment variable:
export OPENAI_API_KEY="your-api-key-here"
Now let's import the core components we will use throughout this guide:
from agents import Agent, Runner, function_tool
import asyncio
Pattern 1: Basic Function Tools
The simplest and most common tool use pattern is the function tool. You define a regular Python function, decorate it with @function_tool, and pass it to an agent. The SDK automatically inspects the function signature and docstring to generate the tool schema that the model uses.
from agents import Agent, Runner, function_tool
@function_tool
def get_weather(city: str) -> str:
"""Get the current weather for a given city.
Args:
city: The name of the city to get weather for.
Returns:
A string describing the current weather.
"""
# Simulated weather data
weather_data = {
"San Francisco": "Sunny, 65°F",
"New York": "Cloudy, 50°F",
"London": "Rainy, 45°F",
}
return weather_data.get(city, f"Weather data not available for {city}")
weather_agent = Agent(
name="WeatherAssistant",
instructions="You are a helpful weather assistant. Use the get_weather tool to answer weather questions.",
tools=[get_weather],
)
async def main():
result = await Runner.run(weather_agent, "What's the weather in San Francisco?")
print(result.final_output)
asyncio.run(main())
In this example, the get_weather function is automatically converted into a tool schema. The agent decides when to call it based on the user's question and the function's docstring. The SDK handles the entire call-and-response cycle, including passing the tool result back to the model so it can formulate a final answer.
How Function Tool Schemas Are Generated
The SDK uses Python type hints and docstrings to generate the JSON schema for each tool. This means you should always:
- Use clear, descriptive parameter names.
- Include type annotations for all parameters and return values.
- Write detailed docstrings that explain what the tool does and what each argument means.
The model relies heavily on these descriptions to decide when and how to use a tool. A vague docstring can cause the agent to misuse or ignore the tool entirely.
Pattern 2: Tools with Complex Parameters
Function tools are not limited to simple string parameters. You can use Pydantic models to define complex, structured parameters. This is particularly useful when a tool requires multiple related fields.
from pydantic import BaseModel
from agents import Agent, Runner, function_tool
class SearchQuery(BaseModel):
query: str
max_results: int
include_snippets: bool
@function_tool
def search_database(search: SearchQuery) -> str:
"""Search the internal knowledge database.
Args:
search: A structured search query containing the search text,
maximum number of results, and whether to include snippets.
Returns:
A formatted string of search results.
"""
results = [
f"Result {i+1} for '{search.query}'"
for i in range(min(search.max_results, 5))
]
if search.include_snippets:
results = [r + " [snippet: ...]" for r in results]
return "\n".join(results)
search_agent = Agent(
name="SearchAssistant",
instructions="You help users search the internal database. Use search_database for all queries.",
tools=[search_database],
)
async def main():
result = await Runner.run(
search_agent,
"Search for 'machine learning best practices', show 3 results with snippets."
)
print(result.final_output)
asyncio.run(main())
Using Pydantic models for parameters gives you automatic validation, clear documentation, and type safety. The SDK translates the model into a nested JSON schema that the model can populate correctly.
Pattern 3: Async Function Tools
Many real-world tools need to perform I/O operations such as calling external APIs or querying databases. The SDK fully supports asynchronous function tools. Simply define your function with async def and the SDK will await it automatically.
import aiohttp
from agents import Agent, Runner, function_tool
@function_tool
async def fetch_api_data(url: str) -> str:
"""Fetch JSON data from a given URL.
Args:
url: The URL to fetch data from.
Returns:
The response body as a string.
"""
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
return await response.text()
api_agent = Agent(
name="APIAssistant",
instructions="You fetch data from APIs when asked. Always use fetch_api_data.",
tools=[fetch_api_data],
)
async def main():
result = await Runner.run(
api_agent,
"Fetch the data from https://jsonplaceholder.typicode.com/todos/1"
)
print(result.final_output)
asyncio.run(main())
Async tools are especially valuable when an agent needs to call multiple external services. They prevent blocking the event loop and allow for concurrent operations when combined with other async patterns.
Pattern 4: Hosted Tools
The Agents SDK provides access to OpenAI-hosted tools that run on OpenAI's infrastructure. These include web search and code execution. Hosted tools require no additional setup and are ideal for tasks that benefit from OpenAI's optimized implementations.
from agents import Agent, Runner, WebSearchTool
research_agent = Agent(
name="ResearchAssistant",
instructions=(
"You are a research assistant. Use web search to find current "
"information and provide well-sourced answers."
),
tools=[WebSearchTool()],
)
async def main():
result = await Runner.run(
research_agent,
"What are the latest developments in quantum computing in 2024?"
)
print(result.final_output)
asyncio.run(main())
Hosted tools are powerful because they are maintained and updated by OpenAI. The web search tool, for example, returns real-time results with citations, making it ideal for research and fact-checking agents. Note that hosted tools may incur additional costs beyond standard token usage.
Pattern 5: Agent-as-Tool Pattern
One of the most powerful patterns in the Agents SDK is using one agent as a tool for another agent. This enables hierarchical agent architectures where specialized agents handle specific subtasks while a coordinator agent orchestrates the overall workflow.
from agents import Agent, Runner, function_tool
# Specialist agent for code review
code_reviewer = Agent(
name="CodeReviewer",
instructions=(
"You are an expert code reviewer. Analyze code for bugs, "
"security issues, and style problems. Provide specific, actionable feedback."
),
)
# Specialist agent for writing tests
test_writer = Agent(
name="TestWriter",
instructions=(
"You are a test engineer. Write comprehensive unit tests "
"for the given code using pytest."
),
)
@function_tool
async def review_code(code: str) -> str:
"""Review code for bugs and issues.
Args:
code: The source code to review.
Returns:
A detailed code review with findings and recommendations.
"""
result = await Runner.run(code_reviewer, f"Review this code:\n\n{code}")
return result.final_output
@function_tool
async def write_tests(code: str) -> str:
"""Write unit tests for the given code.
Args:
code: The source code to test.
Returns:
Complete unit test code using pytest.
"""
result = await Runner.run(test_writer, f"Write tests for this code:\n\n{code}")
return result.final_output
# Coordinator agent
dev_agent = Agent(
name="DevAssistant",
instructions=(
"You are a development assistant. When given code, first review it "
"for issues, then write tests for it. Present both results clearly."
),
tools=[review_code, write_tests],
)
async def main():
sample_code = """
def divide(a, b):
return a / b
"""
result = await Runner.run(dev_agent, f"Help me with this code:\n\n{sample_code}")
print(result.final_output)
asyncio.run(main())
This pattern promotes separation of concerns and allows each sub-agent to have its own specialized instructions, model configuration, and toolset. The coordinator agent does not need to know the details of how each sub-task is performed; it simply calls the appropriate tool and incorporates the result.
Pattern 6: Multi-Tool Agents
Real-world agents often need access to many tools simultaneously. The SDK supports passing multiple tools to a single agent. The model then decides which tool to use based on the user's request and the tool descriptions.
from agents import Agent, Runner, function_tool
@function_tool
def calculate(expression: str) -> str:
"""Evaluate a mathematical expression safely.
Args:
expression: A mathematical expression as a string, e.g. "2 + 3 * 4".
Returns:
The result of the calculation.
"""
allowed = set("0123456789+-*/.() ")
if not all(c in allowed for c in expression):
return "Error: Invalid characters in expression"
try:
result = eval(expression, {"__builtins__": {}}, {})
return str(result)
except Exception as e:
return f"Error: {str(e)}"
@function_tool
def format_currency(amount: float, currency: str) -> str:
"""Format a number as currency.
Args:
amount: The numeric amount to format.
currency: The currency code, e.g. "USD", "EUR", "JPY".
Returns:
A formatted currency string.
"""
symbols = {"USD": "$", "EUR": "€", "JPY": "¥", "GBP": "£"}
symbol = symbols.get(currency.upper(), "")
return f"{symbol}{amount:,.2f}"
@function_tool
def convert_currency(amount: float, from_currency: str, to_currency: str) -> float:
"""Convert an amount from one currency to another.
Args:
amount: The amount to convert.
from_currency: The source currency code.
to_currency: The target currency code.
Returns:
The converted amount.
"""
rates = {"USD": 1.0, "EUR": 0.92, "JPY": 150.0, "GBP": 0.79}
usd_amount = amount / rates.get(from_currency.upper(), 1.0)
return usd_amount * rates.get(to_currency.upper(), 1.0)
finance_agent = Agent(
name="FinanceAssistant",
instructions=(
"You are a finance assistant. You can perform calculations, "
"convert currencies, and format amounts. Use the appropriate "
"tool for each task and combine results when needed."
),
tools=[calculate, format_currency, convert_currency],
)
async def main():
result = await Runner.run(
finance_agent,
"Convert 100 USD to EUR and format the result nicely."
)
print(result.final_output)
asyncio.run(main())
When providing multiple tools, ensure each tool has a distinct purpose and clear documentation. If two tools have overlapping functionality, the model may struggle to choose the right one, leading to inconsistent behavior.
Pattern 7: Error Handling in Tools
Tools can fail for many reasons: network errors, invalid inputs, permission issues, and more. Robust error handling is critical for production agents. The SDK allows tools to return error messages that the agent can understand and act upon.
from agents import Agent, Runner, function_tool
import json
@function_tool
def query_user_database(user_id: int) -> str:
"""Query user information from the database.
Args:
user_id: The unique identifier of the user.
Returns:
JSON string containing user information or an error message.
"""
try:
if user_id < 1:
return json.dumps({"error": "Invalid user ID. Must be a positive integer."})
# Simulated database lookup
database = {
1: {"name": "Alice", "email": "alice@example.com", "plan": "premium"},
2: {"name": "Bob", "email": "bob@example.com", "plan": "free"},
}
user = database.get(user_id)
if user is None:
return json.dumps({"error": f"User with ID {user_id} not found."})
return json.dumps({"success": True, "data": user})
except Exception as e:
return json.dumps({"error": f"Database error: {str(e)}"})
support_agent = Agent(
name="SupportAgent",
instructions=(
"You are a customer support agent. Use query_user_database to look up "
"user information. If a tool returns an error, explain it to the user "
"and suggest next steps."
),
tools=[query_user_database],
)
async def main():
result = await Runner.run(support_agent, "Look up user ID 3 and tell me their plan.")
print(result.final_output)
asyncio.run(main())
The key principle is to return structured error information rather than raising exceptions. This allows the agent to reason about the error and potentially try a different approach or ask the user for clarification.
Pattern 8: Tools with Context
The SDK supports passing a context object through the agent execution lifecycle. This context can be accessed inside tool functions, enabling tools to use shared state, authentication tokens, or request-specific configuration.
from dataclasses import dataclass
from agents import Agent, Runner, function_tool
@dataclass
class AppContext:
api_token: str
user_id: str
base_url: str
@function_tool
def get_user_profile(ctx, username: str) -> str:
"""Get a user's profile information.
Args:
username: The username to look up.
Returns:
Profile information as a string.
"""
# Access context for authentication
app_ctx: AppContext = ctx.context
# In a real app, use app_ctx.api_token and app_ctx.base_url
# to make an authenticated API call
return f"Profile for {username} (requested by {app_ctx.user_id})"
@function_tool
def update_user_profile(ctx, username: str, bio: str) -> str:
"""Update a user's bio.
Args:
username: The username to update.
bio: The new bio text.
Returns:
Confirmation message.
"""
app_ctx: AppContext = ctx.context
return f"Updated bio for {username} using token {app_ctx.api_token[:4]}..."
profile_agent = Agent(
name="ProfileAgent",
instructions="You manage user profiles. Use the tools to get and update profile information.",
tools=[get_user_profile, update_user_profile],
)
async def main():
context = AppContext(
api_token="sk-secret-token-12345",
user_id="admin_001",
base_url="https://api.example.com"
)
result = await Runner.run(
profile_agent,
"Get the profile for user 'johndoe' and then update their bio to 'Hello World'.",
context=context
)
print(result.final_output)
asyncio.run(main())
Using context objects is a clean way to share configuration and state across multiple tools without cluttering tool signatures with authentication parameters. It also makes tools easier to test since you can inject mock contexts during testing.
Pattern 9: Chaining Tools with Handoffs
While tools allow agents to perform actions, handoffs allow agents to transfer control to other agents. Combining tools with handoffs creates powerful workflows where specialized agents handle different stages of a task.
from agents import Agent, Runner, function_tool, handoff
@function_tool
def extract_text_from_pdf(file_path: str) -> str:
"""Extract text content from a PDF file.
Args:
file_path: Path to the PDF file.
Returns:
Extracted text content.
"""
return f"[Extracted text from {file_path}: Lorem ipsum dolor sit amet...]"
@function_tool
def summarize_text(text: str, max_words: int) -> str:
"""Summarize text to a specified length.
Args:
text: The text to summarize.
max_words: Maximum number of words in the summary.
Returns:
A summarized version of the text.
"""
words = text.split()
return " ".join(words[:max_words])
# First agent: handles document intake
intake_agent = Agent(
name="IntakeAgent",
instructions=(
"You handle document intake. Extract text from PDFs using "
"extract_text_from_pdf, then hand off to the summary agent."
),
tools=[extract_text_from_pdf],
handoffs=[], # Will be set below
)
# Second agent: handles summarization
summary_agent = Agent(
name="SummaryAgent",
instructions=(
"You are a summarization specialist. Use summarize_text to create "
"concise summaries of the text provided to you."
),
tools=[summarize_text],
)
# Connect the agents via handoff
intake_agent.handoffs = [summary_agent]
async def main():
result = await Runner.run(
intake_agent,
"Extract text from 'report.pdf' and summarize it in 20 words."
)
print(result.final_output)
asyncio.run(main())
This pattern is especially useful for complex workflows where different stages require different expertise, tools, or even different model configurations. The handoff mechanism ensures a clean transfer of context between agents.
Pattern 10: Dynamic Tool Selection
In some scenarios, you may want an agent to have access to different tools depending on runtime conditions. While the SDK does not dynamically change an agent's tool list mid-execution, you can achieve similar behavior by using a meta-tool that dispatches to the appropriate function internally.
from agents import Agent, Runner, function_tool
from typing import Optional
# Internal tool registry
_tool_registry = {}
def register_tool(name: str, func):
_tool_registry[name] = func
@function_tool
def dynamic_tool(action: str, params: str) -> str:
"""Execute a dynamic action from the available tool set.
Args:
action: The name of the action to perform. Available actions:
'create_task', 'list_tasks', 'complete_task'.
params: JSON string of parameters for the action.
Returns:
The result of the action as a string.
"""
import json
try:
kwargs = json.loads(params) if params else {}
except json.JSONDecodeError:
return "Error: Invalid JSON parameters"
func = _tool_registry.get(action)
if func is None:
available = ", ".join(_tool_registry.keys())
return f"Error: Unknown action '{action}'. Available: {available}"
return func(**kwargs)
# Register internal functions
def _create_task(title: str, priority: str = "medium") -> str:
return f"Created task '{title}' with priority '{priority}'"
def _list_tasks() -> str:
return "Tasks: [1] Buy groceries (high), [2] Call mom (low)"
def _complete_task(task_id: int) -> str:
return f"Completed task {task_id}"
register_tool("create_task", _create_task)
register_tool("list_tasks", _list_tasks)
register_tool("complete_task", _complete_task)
task_agent = Agent(
name="TaskAgent",
instructions=(
"You manage tasks. Use dynamic_tool to create, list, or complete tasks. "
"Always pass valid JSON for params."
),
tools=[dynamic_tool],
)
async def main():
result = await Runner.run(task_agent, "Create a high-priority task called 'Finish report'.")
print(result.final_output)
asyncio.run(main())
This approach gives you flexibility to add or modify internal functions at runtime without changing the agent's tool schema. However, use it judiciously, as it reduces the model's visibility into available actions compared to explicitly defined tools.
Best Practices for Tool Use
Write Excellent Docstrings
The model's ability to use a tool correctly depends almost entirely on the tool's name and docstring. Be specific about what the tool does, what each parameter means, and what the return value represents. Include examples in the docstring when the expected format is non-obvious.
Keep Tools Focused and Atomic
Each tool should do one thing well. Avoid creating tools that perform multiple unrelated operations. If a tool needs to do several things, consider splitting it into multiple tools or using the agent-as-tool pattern to delegate to a sub-agent.
Validate Inputs Inside Tools
Never trust that the model will always pass valid arguments. Validate all inputs inside your tool functions and return clear error messages when validation fails. This prevents crashes and helps the agent correct its approach.
Use Strict Types
Use specific type annotations rather than generic types. For example, use Literal for enumerated values instead of plain strings. This helps the model generate correct arguments.
from typing import Literal
@function_tool
def set_temperature(
room: str,
temperature: float,
unit: Literal["celsius", "fahrenheit"] = "celsius"
) -> str:
"""Set the target temperature for a room.
Args:
room: The name of the room.
temperature: The target temperature value.
unit: The temperature unit, either 'celsius' or 'fahrenheit'.
Returns:
Confirmation of the temperature setting.
"""
return f"Set {room} to {temperature}°{unit[0].upper()}"
Handle Rate Limits and Retries
If your tools call external APIs, implement retry logic and rate limiting. The agent may call a tool multiple times in quick succession, and you should handle transient failures gracefully.
import asyncio
from agents import function_tool
@function_tool
async def call_external_api(endpoint: str, max_retries: int = 3) -> str:
"""Call an external API with automatic retries.
Args:
endpoint: The API endpoint to call.
max_retries: Maximum number of retry attempts.
Returns:
The API response or an error message.
"""
for attempt in range(max_retries):
try:
# Simulated API call
await asyncio.sleep(0.1)
if attempt == 0:
return f"Success: Response from {endpoint}"
return f"Success after {attempt + 1} attempts: Response from {endpoint}"
except Exception as e:
if attempt == max_retries - 1:
return f"Error after {max_retries} attempts: {str(e)}"
await asyncio.sleep(2 ** attempt) # Exponential backoff
return "Error: Max retries exceeded"
Log Tool Usage for Debugging
In production, log every tool call including the input parameters and output. This is invaluable for debugging unexpected agent behavior and for auditing purposes.
import logging
from agents import function_tool
logger = logging.getLogger("tool_usage")
@function_tool
def search_products(query: str, category: str = "all") -> str:
"""Search for products in the catalog.
Args:
query: The search query.
category: The product category to filter by.
Returns:
A list of matching products.
"""
logger.info(f"Tool called: search_products(query={query!r}, category={category!r})")
result = f"Found 3 products matching '{query}' in '{category}'"
logger.info(f"Tool result: {result}")
return result
Limit the Number of Tools
While the SDK supports many tools per agent, having too many can confuse the model and increase token costs. As a general guideline, keep the number of tools under 10-15 per agent. If you need more, consider splitting responsibilities across multiple agents using the agent-as-tool or handoff patterns.
Test Tools Independently
Before integrating a tool with an agent, test it as a regular Python function. This ensures the logic is correct and helps you catch issues early. Once the tool works independently, you can confidently add it to an agent.
Conclusion
Tool use is the cornerstone of building capable AI agents with the OpenAI Agents SDK. By mastering the patterns covered in this guide — from basic function tools to complex agent-as-tool architectures — you can create agents that ground their responses in real data, perform meaningful actions, and handle sophisticated multi-step workflows. The key to success lies in writing clear tool descriptions, keeping tools focused and atomic, handling errors gracefully, and choosing the right pattern for each use case. As you build and iterate on your agents, remember that well-designed tools not only improve accuracy and reliability but also make your agents easier to debug, maintain, and extend over time.