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 LR
INPUT(["User input"])
AGENT["Agent<br/>name plus instructions"]
HAND{"Handoff to<br/>another agent?"}
SUB["Sub-agent<br/>specialist"]
GUARD{"Guardrail<br/>passed?"}
TOOL["Tool call"]
SDK[("Tracing<br/>OpenAI dashboard")]
OUT(["Final output"])
INPUT --> AGENT --> HAND
HAND -->|Yes| SUB --> GUARD
HAND -->|No| GUARD
GUARD -->|Yes| TOOL --> AGENT
GUARD -->|Block| OUT
AGENT --> OUT
AGENT --> SDK
style AGENT fill:#4f46e5,stroke:#4338ca,color:#fff
style GUARD fill:#f59e0b,stroke:#d97706,color:#1f2937
style SDK fill:#ede9fe,stroke:#7c3aed,color:#1e1b4b
style OUT 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.
Hear it before you finish reading
Talk to a live CallSphere AI voice agent in your browser — 60 seconds, no signup.
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.
Complex Parameters with Pydantic Models
For tools with structured inputs, use Pydantic models to define complex parameter schemas:
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.
Still reading? Stop comparing — try CallSphere live.
CallSphere ships complete AI voice agents per industry — 14 tools for healthcare, 10 agents for real estate, 4 specialists for salons. See how it actually handles a call before you book a demo.
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
Try CallSphere AI Voice Agents
See how AI voice agents work for your industry. Live demo available -- no signup required.