Advanced AutoGen: Empowering Agents with Custom Tools and Functions unlocks the real power of multi-agent systems — moving beyond conversation into genuine action. Out of the box, AutoGen agents are skilled reasoners, but they cannot fetch live data, call external APIs, or execute domain-specific logic without you explicitly providing those capabilities. This guide shows you exactly how to bridge that gap: defining Python functions as tools, registering them with the right agents, and prompting your system so tools are called reliably and safely.
If you are new to multi-agent orchestration in general, compare how AutoGen’s tool model differs from single-agent frameworks covered in Introduction to LangChain: Build Your First AI Agent before diving in.
Why Custom Tools Are Essential for Advanced Agents
A conversational agent that can only produce text is, at best, a sophisticated search engine. Production-grade systems need to take actions: query a live database, validate a credit card number, post a message to Slack, or run a numerical simulation. Custom tools are the mechanism AutoGen uses to expose these real-world operations to an LLM.
There are three reasons custom tools matter more as systems grow:
- Specialization — Each agent in a group chat can own a distinct tool set. A
DataAnalystAgentgets database read access; anExecutorAgentgets shell commands. Permissions stay scoped. - Reliability — Hard-coded Python logic runs deterministically. Asking the LLM to “calculate compound interest” in prose is fragile; calling a
calculate_compound_interest()function is not. - Auditability — Every tool call is a discrete, logged event. You know exactly when it fired, with what arguments, and what it returned.
This separation of reasoning (LLM) from action (tool) is the same design philosophy used in agentic frameworks like Getting Started with CrewAI: Multi-Agent Workflows in Python, but AutoGen’s approach gives you fine-grained control over which agent can invoke which tool.
Defining a Tool: From Python Function to Agent Capability
AutoGen v0.4+ uses a decorator-based API for tool registration. The central requirement is a well-typed, well-documented Python function — the docstring becomes the description the LLM reads when deciding whether to call the tool.
Prerequisites
pip install pyautogen>=0.4.0 httpx python-dotenv
A minimal tool: current UTC time
from datetime import datetime, timezone
def get_current_utc_time() -> str:
"""Return the current UTC date and time as an ISO 8601 string.
Use this tool when the user asks about the current time or date.
"""
return datetime.now(timezone.utc).isoformat()
That is all AutoGen needs: a Python callable with a clear docstring and type annotations.
A realistic tool: weather lookup
import httpx
import os
def get_weather(city: str, country_code: str = "US") -> dict:
"""Fetch current weather for a given city using the Open-Meteo API.
Args:
city: The city name, e.g. 'San Francisco'.
country_code: ISO 3166-1 alpha-2 country code, defaults to 'US'.
Returns:
A dict with keys: temperature_c, wind_speed_kmh, weather_description.
"""
# Geocode the city first
geo_url = "https://geocoding-api.open-meteo.com/v1/search"
geo_resp = httpx.get(geo_url, params={"name": city, "count": 1, "language": "en"})
geo_resp.raise_for_status()
results = geo_resp.json().get("results", [])
if not results:
return {"error": f"City '{city}' not found."}
lat = results[0]["latitude"]
lon = results[0]["longitude"]
# Fetch weather
weather_url = "https://api.open-meteo.com/v1/forecast"
params = {
"latitude": lat,
"longitude": lon,
"current": "temperature_2m,wind_speed_10m,weather_code",
"wind_speed_unit": "kmh",
}
w_resp = httpx.get(weather_url, params=params)
w_resp.raise_for_status()
current = w_resp.json()["current"]
return {
"temperature_c": current["temperature_2m"],
"wind_speed_kmh": current["wind_speed_10m"],
"weather_code": current["weather_code"],
}
Key design rules for any tool function:
| Rule | Why it matters |
|---|---|
| Use precise type annotations | AutoGen generates a JSON schema from them |
| Write a clear, imperative docstring | This is what the LLM reads to decide when to call the tool |
| Return simple, serializable types | dict, str, int, list — avoid custom objects |
| Raise exceptions for real errors | AutoGen surfaces them to the agent so it can retry or report |
Registering Your Custom Tool with an Agent
In AutoGen v0.4, tools are registered on the AssistantAgent (or any ConversableAgent subclass) using the register_for_llm and register_for_execution decorators, or via the convenience method register_function.
Full working example
import os
from autogen import AssistantAgent, UserProxyAgent
from dotenv import load_dotenv
load_dotenv()
# --- LLM configuration -------------------------------------------------------
llm_config = {
"config_list": [
{
"model": "gpt-4o",
"api_key": os.environ["OPENAI_API_KEY"],
}
],
"temperature": 0,
"timeout": 120,
}
# --- Define tools ------------------------------------------------------------
from datetime import datetime, timezone
import httpx
def get_current_utc_time() -> str:
"""Return the current UTC date and time as an ISO 8601 string."""
return datetime.now(timezone.utc).isoformat()
def get_weather(city: str, country_code: str = "US") -> dict:
"""Fetch current weather for a given city using the Open-Meteo API.
Args:
city: The city name, e.g. 'San Francisco'.
country_code: ISO 3166-1 alpha-2 country code, defaults to 'US'.
"""
geo_resp = httpx.get(
"https://geocoding-api.open-meteo.com/v1/search",
params={"name": city, "count": 1, "language": "en"},
)
geo_resp.raise_for_status()
results = geo_resp.json().get("results", [])
if not results:
return {"error": f"City '{city}' not found."}
lat, lon = results[0]["latitude"], results[0]["longitude"]
w_resp = httpx.get(
"https://api.open-meteo.com/v1/forecast",
params={
"latitude": lat,
"longitude": lon,
"current": "temperature_2m,wind_speed_10m,weather_code",
"wind_speed_unit": "kmh",
},
)
w_resp.raise_for_status()
current = w_resp.json()["current"]
return {
"temperature_c": current["temperature_2m"],
"wind_speed_kmh": current["wind_speed_10m"],
"weather_code": current["weather_code"],
}
# --- Build agents ------------------------------------------------------------
assistant = AssistantAgent(
name="WeatherAssistant",
system_message=(
"You are a helpful assistant. "
"Use the available tools to answer questions about time and weather. "
"Always call a tool when the user asks for live data."
),
llm_config=llm_config,
)
# UserProxyAgent executes the tool calls on behalf of the human
user_proxy = UserProxyAgent(
name="User",
human_input_mode="NEVER", # fully automated for this demo
max_consecutive_auto_reply=5,
code_execution_config=False,
)
# --- Register tools ----------------------------------------------------------
# register_for_llm → tells the LLM that this tool exists (adds it to the schema)
# register_for_execution → tells the UserProxyAgent to actually run it
from autogen import register_function
register_function(
get_current_utc_time,
caller=assistant, # the agent that decides to call the tool
executor=user_proxy, # the agent that runs the tool
name="get_current_utc_time",
description="Return the current UTC date and time as an ISO 8601 string.",
)
register_function(
get_weather,
caller=assistant,
executor=user_proxy,
name="get_weather",
description=(
"Fetch current weather for a city. "
"Args: city (str), country_code (str, default 'US')."
),
)
The caller / executor split is important: the caller is the reasoning agent that decides when to invoke the tool; the executor is the agent that has permission to actually run arbitrary Python. Keeping these separate is a key security boundary in multi-agent systems.
Prompting and Triggering Tool Use in Conversation
Registering the tool is only half the job. If the agent’s system message does not explicitly instruct it to prefer tools over free-form reasoning, it may ignore them for simple questions. Follow these prompting principles:
- State the obligation — “Always call a tool when you need live data.” Not “you may use tools.”
- Name the tools — List them explicitly in the system message so the LLM knows they exist without guessing.
- Forbid hallucination — “Do not make up values. If you do not have a tool for this, say so.”
Initiating the conversation
# Kick off the two-agent chat
result = user_proxy.initiate_chat(
assistant,
message="What's the current UTC time? Also, what is the weather like in Tokyo right now?",
)
# The full conversation history
for msg in result.chat_history:
print(f"[{msg['role'].upper()}] {msg['content']}\n")
Handling tool errors gracefully
When a tool raises an exception, AutoGen returns the traceback as a tool result string. You can prompt the agent to handle this:
assistant = AssistantAgent(
name="WeatherAssistant",
system_message=(
"You are a helpful assistant with access to time and weather tools. "
"If a tool returns an error, report it clearly to the user and suggest alternatives. "
"Never fabricate data."
),
llm_config=llm_config,
)
Testing tool invocation in isolation
Before wiring a tool into a full agent conversation, test it independently:
if __name__ == "__main__":
# Unit test the tools before attaching to agents
print("UTC time:", get_current_utc_time())
print("Tokyo weather:", get_weather("Tokyo", "JP"))
python weather_agent.py
# UTC time: 2026-04-07T08:42:11.034521+00:00
# Tokyo weather: {'temperature_c': 14.2, 'wind_speed_kmh': 18.7, 'weather_code': 3}
Adding a tool to a group chat
In a multi-agent group chat, you assign different tools to different specialist agents, then combine them under a GroupChatManager. The manager routes messages so each agent only sees prompts relevant to its specialty — a pattern explored in depth in the Mastering AutoGen Group Chat for Collaborative AI Workflows article in this series.
from autogen import GroupChat, GroupChatManager
data_agent = AssistantAgent(
name="DataAgent",
system_message="You retrieve live data using your tools. Never answer without calling a tool.",
llm_config=llm_config,
)
summary_agent = AssistantAgent(
name="SummaryAgent",
system_message="You summarize data provided by other agents into user-friendly reports.",
llm_config=llm_config,
)
register_function(
get_weather,
caller=data_agent,
executor=user_proxy,
name="get_weather",
description="Fetch current weather for a city.",
)
group_chat = GroupChat(
agents=[user_proxy, data_agent, summary_agent],
messages=[],
max_round=8,
)
manager = GroupChatManager(groupchat=group_chat, llm_config=llm_config)
user_proxy.initiate_chat(
manager,
message="Get the weather for London, then give me a one-paragraph travel briefing.",
)
This pattern — one agent fetches, another synthesizes — keeps responsibilities clean and makes each agent easier to test and replace independently. For a broader comparison of how different frameworks handle this delegation pattern, see LangChain vs LlamaIndex: Which Should You Use in 2026?.
Frequently Asked Questions
Can I register the same tool on multiple agents simultaneously?
Yes. Call register_function once per (caller, executor) pair. If two agents both need get_weather, register it twice — once for each caller — using the same executor. Each agent will independently decide when to call it.
What happens if a tool call exceeds the timeout set in llm_config?
The timeout in llm_config applies to the LLM API call, not to tool execution. Tool execution timeout is controlled by Python itself. Wrap long-running tools in concurrent.futures.ThreadPoolExecutor with an explicit timeout, and raise a TimeoutError that AutoGen can surface back to the agent.
How do I prevent the agent from calling a tool with unsafe arguments?
Validate inputs at the top of your function and raise a ValueError with a descriptive message. AutoGen will return that message to the agent as a tool error, and the agent can report it or try a corrected call. Never silently swallow bad arguments — the agent needs the feedback to self-correct.
Is code_execution_config=False required on the executor agent when using registered tools?
Not strictly, but it is recommended in production. Setting code_execution_config=False on the UserProxyAgent disables arbitrary LLM-generated code execution while still allowing registered tool functions to run. This significantly reduces the attack surface of your system.
Can I use async functions as tools?
AutoGen v0.4 supports async tool functions through register_function when used inside an async event loop. Define your tool with async def, ensure your executor agent runs in an async context (asyncio.run()), and AutoGen handles the await chain automatically. Mixing sync and async tools in the same agent is supported but requires care around event loop management.