Building Custom Function Tools with @function_tool Decorator
Master the @function_tool decorator in the OpenAI Agents SDK. Learn how to create sync and async tools, handle complex parameters, and wire multiple custom tools into your agents.
Why Custom Function Tools?
Hosted tools cover common capabilities like web search and code execution, but real-world agents need to interact with your systems — databases, APIs, business logic, and external services. The @function_tool decorator lets you turn any Python function into a tool that an agent can call.
The SDK automatically generates the JSON schema for the tool from your function's type hints and docstring. The agent sees the tool's name, description, and parameter schema, then decides when and how to call it.
Your First Function Tool
The simplest function tool is a decorated Python function with type hints:
flowchart TD
START["Building Custom Function Tools with @function_too…"] --> A
A["Why Custom Function Tools?"]
A --> B
B["Your First Function Tool"]
B --> C
C["Async Function Tools"]
C --> D
D["Complex Parameters with Pydantic Models"]
D --> E
E["Customizing Tool Name and Description"]
E --> F
F["Wiring Multiple Tools Into an Agent"]
F --> G
G["Accessing RunContext in Tools"]
G --> H
H["Key Takeaways"]
H --> DONE["Key Takeaways"]
style START fill:#4f46e5,stroke:#4338ca,color:#fff
style DONE fill:#059669,stroke:#047857,color:#fff
from agents import Agent, Runner, function_tool
@function_tool
def get_weather(city: str) -> str:
"""Get the current weather for a given city."""
# In production, call a real weather API here
return f"The weather in {city} is 72F and sunny."
agent = Agent(
name="Weather Agent",
instructions="You help users check the weather. Use the get_weather tool when asked about weather conditions.",
tools=[get_weather],
)
result = Runner.run_sync(agent, "What's the weather like in Tokyo?")
print(result.final_output)
The decorator reads the function name (get_weather), the docstring (used as the tool description), and the parameter types to build the tool schema automatically.
Async Function Tools
For tools that call external APIs or databases, use async functions to avoid blocking the event loop:
import httpx
from agents import function_tool
@function_tool
async def fetch_stock_price(symbol: str) -> str:
"""Fetch the latest stock price for a given ticker symbol."""
async with httpx.AsyncClient() as client:
response = await client.get(
f"https://api.example.com/stocks/{symbol}"
)
data = response.json()
return f"{symbol}: ${data['price']:.2f}"
The SDK handles both sync and async tools seamlessly. Async tools are awaited during the agent loop, while sync tools are run in a thread pool so they do not block.
See AI Voice Agents Handle Real Calls
Book a free demo or calculate how much you can save with AI voice automation.
Complex Parameters with Pydantic Models
For tools with structured inputs, use Pydantic models to define complex parameter schemas:
flowchart TD
CENTER(("Core Concepts"))
CENTER --> N0["Use @function_tool to turn any Python f…"]
CENTER --> N1["Add type hints and docstrings — the SDK…"]
CENTER --> N2["Use Pydantic models for complex paramet…"]
CENTER --> N3["Access shared state via RunContextWrapp…"]
CENTER --> N4["Combine multiple tools to build capable…"]
style CENTER fill:#4f46e5,stroke:#4338ca,color:#fff
from pydantic import BaseModel, Field
from agents import function_tool
class FlightSearch(BaseModel):
origin: str = Field(description="Departure airport code (e.g., SFO)")
destination: str = Field(description="Arrival airport code (e.g., NRT)")
date: str = Field(description="Travel date in YYYY-MM-DD format")
max_stops: int = Field(default=1, description="Maximum number of stops")
@function_tool
def search_flights(params: FlightSearch) -> str:
"""Search for available flights between two airports."""
return f"Found 3 flights from {params.origin} to {params.destination} on {params.date} with up to {params.max_stops} stop(s)."
The Pydantic model's field descriptions become part of the JSON schema the agent sees, helping the model fill in parameters correctly.
Customizing Tool Name and Description
You can override the auto-generated name and description:
@function_tool(
name_override="lookup_customer",
description_override="Search for a customer by email address or customer ID. Returns the customer's name, plan, and account status.",
)
def find_customer(identifier: str) -> str:
"""Internal: look up customer record."""
# The description_override is what the agent sees,
# not this docstring
return f"Customer {identifier}: Pro plan, active"
This is useful when your internal function name doesn't match what you want the agent to see, or when you need a more detailed description than the docstring provides.
Wiring Multiple Tools Into an Agent
Agents become truly useful when they have access to several tools. The model chooses which tool to call based on the user's request:
from agents import Agent, Runner, function_tool
@function_tool
def create_ticket(title: str, priority: str, description: str) -> str:
"""Create a support ticket in the ticketing system."""
return f"Ticket created: '{title}' with {priority} priority."
@function_tool
def list_open_tickets(customer_id: str) -> str:
"""List all open support tickets for a customer."""
return f"Customer {customer_id} has 3 open tickets."
@function_tool
def escalate_ticket(ticket_id: str, reason: str) -> str:
"""Escalate a support ticket to a senior agent."""
return f"Ticket {ticket_id} escalated. Reason: {reason}"
agent = Agent(
name="Support Agent",
instructions="You are a customer support agent. Help users manage their support tickets. Use the appropriate tool for each request.",
tools=[create_ticket, list_open_tickets, escalate_ticket],
)
result = Runner.run_sync(agent, "Create a high-priority ticket about a billing error on my last invoice.")
print(result.final_output)
Accessing RunContext in Tools
Sometimes your tools need access to shared state — a database connection, the current user ID, or configuration. The SDK passes a RunContextWrapper as the first argument if your function accepts it:
from dataclasses import dataclass
from agents import Agent, Runner, RunContextWrapper, function_tool
@dataclass
class AppContext:
user_id: str
db_connection: object # your DB connection
@function_tool
async def get_user_orders(ctx: RunContextWrapper[AppContext]) -> str:
"""Retrieve the current user's recent orders."""
user_id = ctx.context.user_id
# Use ctx.context.db_connection to query the database
return f"User {user_id} has 5 recent orders."
agent = Agent(
name="Order Agent",
instructions="You help users check their order history.",
tools=[get_user_orders],
)
app_ctx = AppContext(user_id="user_123", db_connection=None)
result = Runner.run_sync(agent, "Show me my recent orders.", context=app_ctx)
print(result.final_output)
The RunContextWrapper is typed generically, so you get full IDE autocompletion and type checking on your context object. The context is passed once when you call Runner.run_sync() and is available to every tool call during that run.
Key Takeaways
- Use
@function_toolto turn any Python function into an agent tool - Add type hints and docstrings — the SDK auto-generates the JSON schema
- Use Pydantic models for complex parameter structures
- Access shared state via
RunContextWrapper - Combine multiple tools to build capable, domain-specific agents
Written by
CallSphere Team
Expert insights on AI voice agents and customer communication automation.
Try CallSphere AI Voice Agents
See how AI voice agents work for your industry. Live demo available -- no signup required.