How to Create and Use FunctionTool with Custom Python Functions in openai-agents-python

You can expose any Python function to an LLM as a tool by applying the @function_tool decorator from src/agents/tool.py, which automatically generates JSON schemas and wraps your callable for asynchronous runtime execution.

The openai-agents-python SDK provides a first-class mechanism for converting standard Python functions into LLM-callable tools through the FunctionTool abstraction. By using the built-in decorator or factory methods, the library handles schema generation, type validation, and runtime invocation automatically according to the OpenAI function calling specification.

Understanding the FunctionTool Architecture

The transformation from user function to LLM tool follows a specific pipeline implemented across three core modules:

  1. Decoration Phase: The @function_tool decorator in src/agents/tool.py (lines 1650–1690) inspects your callable's signature, docstring, and type hints
  2. Schema Generation: The function_schema() utility in src/agents/function_schema.py (lines 44–78) constructs a Pydantic model and JSON schema for parameter validation
  3. Runtime Execution: The tool execution engine in src/agents/run_internal/tool_execution.py matches LLM function calls to your FunctionTool instance and invokes on_invoke_tool

When decorated, your function becomes a FunctionTool instance (defined in src/agents/tool.py lines 81–120) containing fields for name, description, params_json_schema, strict_json_schema, and the async on_invoke_tool method that serves as the runtime entry point.

Creating Basic Function Tools

Simple Synchronous Function

The minimal setup requires only the @function_tool decorator and a descriptive docstring:

from agents import function_tool, Agent

@function_tool
def greet() -> str:
    """Return a friendly greeting to the user."""
    return "👋 Hello from the function tool!"

# The decorator returns a FunctionTool instance, not a raw function

print(greet.name)  # "greet"

print(greet.params_json_schema)  # {"properties": {}, "type": "object"}

# Attach to an Agent

agent = Agent(tools=[greet])

The decorator implementation in src/agents/tool.py automatically derives the tool name from the function name and the description from the docstring using generate_func_documentation in src/agents/function_schema.py.

Tools with Typed Arguments

Add parameters with type hints to generate structured JSON schemas:

import json
from agents import function_tool, ToolContext

@function_tool
def calculate_bmi(weight_kg: float, height_m: float) -> dict:
    """Calculate BMI given weight in kilograms and height in meters.
    
    Args:
        weight_kg: Weight in kilograms
        height_m: Height in meters
    """
    bmi = weight_kg / (height_m ** 2)
    return {"bmi": round(bmi, 2), "category": "normal" if 18.5 <= bmi < 25 else "other"}

# Simulate runtime invocation (as the agent would do)

ctx = ToolContext(context=None, tool_name="calculate_bmi", 
                  tool_call_id="call_123", tool_arguments="")
result = await calculate_bmi.on_invoke_tool(
    ctx, 
    json.dumps({"weight_kg": 70, "height_m": 1.75})
)

The _on_invoke_tool_impl method (lines 1802–1840 in src/agents/tool.py) handles parsing the JSON input, validating against the Pydantic model generated by function_schema(), and calling your function.

Advanced FunctionTool Configuration

Accessing ToolContext in Async Functions

When your tool needs access to runtime context or dependencies, accept ToolContext as the first parameter:

import asyncio
from agents import function_tool, ToolContext

@function_tool
async def fetch_user_profile(ctx: ToolContext, user_id: str) -> dict:
    """Fetch user profile from database using the request-scoped context.
    
    The ToolContext provides access to per-run data and dependencies.
    """
    # Access context data if available

    db_client = ctx.context.get("db") if ctx.context else None
    
    await asyncio.sleep(0.1)  # Simulate async I/O

    return {"id": user_id, "name": "Alice", "cached": False}

# The decorator detects ToolContext automatically via takes_context property

assert fetch_user_profile.takes_context is True

The decorator inspects the first parameter type to set FunctionTool.takes_context, ensuring the runtime injects the context object before other arguments.

Custom Names, Descriptions, and Error Handling

Override defaults and configure error handling for production resilience:

from agents import function_tool, RunContextWrapper

def handle_tool_error(ctx: RunContextWrapper, exc: Exception) -> str:
    """Return formatted error instead of raising to prevent agent failure."""
    return f"TOOL_ERROR: {exc.__class__.__name__}: {str(exc)}"

@function_tool(
    name_override="weather_lookup",
    description_override="Get current weather conditions for a location.",
    strict_mode=False,  # Allows partial JSON for optional fields

    failure_error_function=handle_tool_error,
)
def get_weather(city: str, units: str = "metric") -> str:
    """Fetch weather data.
    
    Args:
        city: City name to look up
        units: Temperature units (metric or imperial)
    """
    return f"Weather in {city}: 22°C" if units == "metric" else "72°F"

# Non-strict mode excludes optional fields from required schema

assert "units" not in get_weather.params_json_schema.get("required", [])

When strict_mode=False, the generated schema in src/agents/function_schema.py omits default arguments from the required fields array, giving the LLM flexibility in parameter provision.

Deferred Loading for Large Tool Sets

For applications with hundreds of tools, use deferred loading to reduce context window usage:

@function_tool(defer_loading=True)
def expensive_analysis(query: str) -> dict:
    """Perform expensive computation only when explicitly selected.
    
    This tool description is hidden from the model until 
    a tool search is performed via the Responses API.
    """
    return {"result": f"Analysis of {query} complete", "tokens_used": 1500}

assert expensive_analysis.defer_loading is True

Setting defer_loading=True (defined in FunctionTool fields) prevents the tool description from being included in the initial system prompt, loading it only when the model performs a specific tool search.

Runtime Execution Flow

When an Agent runs with your custom tools, the execution flow follows this path according to src/agents/run_internal/tool_execution.py:

  1. The LLM emits a function call with name and JSON arguments
  2. execute_function_tool_calls matches the name to your FunctionTool instance
  3. The runtime calls on_invoke_tool(ctx, json_string), which internally:
    • Parses JSON via _parse_function_tool_json_input
    • Validates against schema.params_pydantic_model
    • Invokes your original function (sync or async)
    • Formats the return value as a string or JSON
  4. Results wrap into RunItem objects and return to the LLM conversation

Error handling occurs at the failure_error_function level if configured, otherwise exceptions propagate to the Agent's output.

Summary

  • Apply @function_tool from src/agents/tool.py to any Python function to create a FunctionTool instance automatically
  • Use type hints and docstrings to generate accurate JSON schemas via src/agents/function_schema.py
  • Accept ToolContext as the first argument when you need runtime context, request-scoped data, or dependency injection
  • Configure strict_mode=False for optional parameters and defer_loading=True for performance optimization with large tool sets
  • Implement failure_error_function handlers to gracefully manage runtime exceptions without breaking agent execution
  • Reference runtime execution in src/agents/run_internal/tool_execution.py to understand how LLM calls map to your Python functions

Frequently Asked Questions

What is the difference between @function_tool and creating a FunctionTool manually?

The @function_tool decorator is the recommended approach that handles inspection, schema generation, and wrapper creation automatically in src/agents/tool.py. While you could instantiate FunctionTool directly with the dataclass constructor (lines 81–120), you would need to manually construct the JSON schema, provide the async on_invoke_tool implementation, and handle context injection yourself.

How does the library handle synchronous vs asynchronous functions?

The decorator detects whether your function is a coroutine and wraps it appropriately in the _on_invoke_tool_impl method (lines 1802–1840 in src/agents/tool.py). Synchronous functions are called directly within the async wrapper, while async functions are awaited. Both return values are serialized to strings or JSON for the LLM context.

Can I use classes or instance methods as function tools?

Currently, the @function_tool decorator in src/agents/tool.py expects a standalone function or static method. For class-based tools, define the method as a static method or move the logic to a module-level function. The schema generation logic relies on inspectable function signatures that don't include self references, which would complicate JSON schema derivation.

What happens if my function raises an exception during execution?

If you provide a failure_error_function callback in the decorator, that function receives the RunContextWrapper and exception, returning a string that the LLM sees as the tool result. Without this handler, exceptions propagate up to the Agent runner. According to the implementation in tool.py, this allows you to return structured error messages or fallback values rather than crashing the agent loop.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →