Claude Code + AutoGen: Build Multi-Agent Systems
Why AutoGen needs explicit CLAUDE.md constraints
Microsoft AutoGen is a framework for building multi-agent applications: systems where several LLM-backed agents converse, delegate, and call tools to solve a task together. It is powerful and it is also one of the most treacherous targets to generate code for, because AutoGen went through a ground-up API rewrite. The old API (v0.2) centred on ConversableAgent, UserProxyAgent, and GroupChat. The new API (v0.4 and later) splits the project into autogen-core (the actor runtime), autogen-agentchat (the high-level agent API), and autogen-ext (model clients and tool extensions), and it is async-first.
Claude Code does not know which one you want. Its training data is saturated with v0.2 tutorials, so by default it generates from autogen import ConversableAgent, builds a UserProxyAgent, wires a GroupChatManager, and writes synchronous code. That code installs against the wrong package, references classes that moved or were removed, and ignores the async runtime the new API requires. It looks plausible and fails immediately on import or on the first await that is not there.
This is not an edge case. It is the default output of a competent Claude Code session asked to "build a multi-agent system with AutoGen." The CLAUDE.md file is what pins it to the version you actually installed.
This guide covers the CLAUDE.md configuration for AutoGen 0.4+, the AgentChat patterns for single agents and teams, model client setup, tool integration, termination conditions, and the six most common mistakes Claude makes. For the broader pattern of orchestrating several agents, Claude Code subagents covers how Claude Code itself runs agents, which is a useful mental model for what AutoGen teams do.
The AutoGen CLAUDE.md template
Place this at your project root. Every Claude Code session reads it before generating any code.
# AutoGen rules
## Version (CRITICAL)
- We use AutoGen v0.4+ (the rewritten API), NOT v0.2
- Packages: autogen-agentchat, autogen-ext, autogen-core
- NEVER `pip install pyautogen` or `import autogen` (that is the old v0.2 API)
- Install: pip install -U "autogen-agentchat" "autogen-ext[openai]"
- Python 3.10+ required
## Package layout (do not mix)
- autogen_agentchat.agents -> AssistantAgent, UserProxyAgent
- autogen_agentchat.teams -> RoundRobinGroupChat, SelectorGroupChat
- autogen_agentchat.conditions -> TextMentionTermination, MaxMessageTermination
- autogen_agentchat.messages -> TextMessage
- autogen_ext.models.openai -> OpenAIChatCompletionClient
- autogen_ext.models.anthropic -> AnthropicChatCompletionClient
- autogen_core -> low-level runtime (only if building custom agents)
- NEVER import ConversableAgent, GroupChat, or GroupChatManager (those are v0.2)
## Async-first (MANDATORY)
- The v0.4 API is async. Agent runs are awaited.
- Entry point: asyncio.run(main())
- Single turn: result = await agent.run(task="...")
- Streaming: async for msg in agent.run_stream(task="..."): ...
- Teams: result = await team.run(task="...") OR team.run_stream(...)
- NEVER write synchronous initiate_chat(...) (that is v0.2)
- ALWAYS close the model client: await model_client.close()
## Model client
- model_client = OpenAIChatCompletionClient(model="gpt-4o")
- API key from env: it reads OPENAI_API_KEY automatically; NEVER hardcode keys
- For Anthropic: AnthropicChatCompletionClient(model="claude-...") reads ANTHROPIC_API_KEY
- Pass the SAME client instance to every agent that should share it
## Agents
- AssistantAgent(name="...", model_client=client, system_message="...", tools=[...])
- name must be a valid identifier (no spaces); used in conversation routing
- tools are plain async/sync Python functions with type hints + docstrings
- UserProxyAgent(name="user", input_func=...) for human-in-the-loop input
## Teams (multi-agent)
- RoundRobinGroupChat([agent_a, agent_b], termination_condition=...)
- SelectorGroupChat([...], model_client=client, termination_condition=...)
- ALWAYS set a termination_condition; without one the team can loop forever
- Common: TextMentionTermination("TERMINATE") | MaxMessageTermination(10)
- Combine conditions with | (OR) and & (AND)
## Hard rules
- NEVER generate v0.2 code (ConversableAgent, UserProxyAgent.initiate_chat, config_list)
- NEVER omit the termination condition on a team
- NEVER hardcode API keys; read from environment
- ALWAYS await model_client.close() in a finally block or after the run
- ALWAYS give tools type hints and docstrings (AutoGen builds the schema from them)
Three of these rules prevent the majority of breakage.
The version rule is the single most important line in the file. The pip install pyautogen / import autogen pattern is so heavily represented in training data that without an explicit instruction, Claude reaches for it almost every time. Pinning the package names to autogen-agentchat and autogen-ext redirects every generated import to the API you actually have installed.
The async-first rule matters because v0.2 code is synchronous (agent.initiate_chat(...)) and v0.4 code is not (await agent.run(...)). Claude will happily mix them, producing a script that defines async agents and then calls them synchronously, which raises a coroutine-was-never-awaited warning and returns a coroutine object instead of a result. Stating the entry point and the await pattern once fixes this across every generated function.
The termination rule prevents runaway cost. A team with no termination condition keeps passing messages between agents until something external stops it, which on a paid model means burning tokens in a loop. Requiring a termination condition on every team, with the common options listed, makes Claude set one by default.
Install and project setup
pip install -U "autogen-agentchat" "autogen-ext[openai]"
If you want the Anthropic model client as well:
pip install -U "autogen-ext[anthropic]"
Set your API key in the environment rather than in code:
# .env
OPENAI_API_KEY=sk-...
A minimal single-agent script confirms the install and the async pattern:
# main.py
import asyncio
from autogen_agentchat.agents import AssistantAgent
from autogen_ext.models.openai import OpenAIChatCompletionClient
async def main() -> None:
model_client = OpenAIChatCompletionClient(model="gpt-4o")
agent = AssistantAgent(
name="assistant",
model_client=model_client,
system_message="You are a concise, helpful assistant.",
)
result = await agent.run(task="Summarise what a multi-agent system is in two sentences.")
print(result.messages[-1].content)
await model_client.close()
if __name__ == "__main__":
asyncio.run(main())
Run it:
python main.py
The await model_client.close() at the end matters. The model client holds an HTTP connection pool, and leaving it open produces unclosed-session warnings and can hang the process on exit. Put the close in CLAUDE.md so Claude generates it every time.
Building a tool-using agent
The point of AutoGen is agents that do things, which means tools. In v0.4, a tool is a plain Python function with type hints and a docstring. AutoGen reads the signature and docstring to build the schema the model sees, so both are mandatory.
# main.py
import asyncio
from autogen_agentchat.agents import AssistantAgent
from autogen_ext.models.openai import OpenAIChatCompletionClient
async def get_weather(city: str) -> str:
"""Return the current weather for a given city.
Args:
city: The name of the city to look up.
"""
# In a real tool this would call a weather API.
return f"The weather in {city} is 18 degrees and clear."
async def main() -> None:
model_client = OpenAIChatCompletionClient(model="gpt-4o")
agent = AssistantAgent(
name="weather_agent",
model_client=model_client,
tools=[get_weather],
system_message="You answer weather questions. Use the get_weather tool.",
reflect_on_tool_use=True,
)
result = await agent.run(task="What is the weather in Lisbon?")
print(result.messages[-1].content)
await model_client.close()
if __name__ == "__main__":
asyncio.run(main())
Two details to encode in CLAUDE.md. First, tools need full type hints and a docstring, because AutoGen derives the function-calling schema from them; a tool with no docstring or untyped arguments produces a degraded or broken schema. Second, reflect_on_tool_use=True makes the agent send the tool result back through the model to produce a natural-language answer, rather than returning the raw tool output. Add a note about both so Claude does not generate bare, undocumented tool functions.
Building a multi-agent team
The framework earns its name when several agents collaborate. The simplest team is a round-robin: agents take turns until a termination condition fires.
# team.py
import asyncio
from autogen_agentchat.agents import AssistantAgent
from autogen_agentchat.teams import RoundRobinGroupChat
from autogen_agentchat.conditions import TextMentionTermination, MaxMessageTermination
from autogen_ext.models.openai import OpenAIChatCompletionClient
async def main() -> None:
model_client = OpenAIChatCompletionClient(model="gpt-4o")
writer = AssistantAgent(
name="writer",
model_client=model_client,
system_message=(
"You write short marketing copy. When the editor approves, "
"reply with the final copy and nothing else."
),
)
editor = AssistantAgent(
name="editor",
model_client=model_client,
system_message=(
"You review the writer's copy for clarity and concision. "
"Give specific feedback. When the copy is good, reply exactly with 'APPROVED'."
),
)
# Stop when the editor says APPROVED, or after 8 messages as a safety cap.
termination = TextMentionTermination("APPROVED") | MaxMessageTermination(8)
team = RoundRobinGroupChat([writer, editor], termination_condition=termination)
result = await team.run(task="Write a one-line tagline for a developer tool that runs in the terminal.")
for message in result.messages:
print(f"{message.source}: {message.content}")
await model_client.close()
if __name__ == "__main__":
asyncio.run(main())
The combined termination condition (TextMentionTermination("APPROVED") | MaxMessageTermination(8)) is the pattern to standardise: a semantic stop signal that ends the conversation when the work is done, ORed with a hard message cap that ends it if the agents loop without converging. The cap is the cost guardrail. Put this combined pattern in CLAUDE.md so Claude never ships a team with only one of the two.
For tasks where a fixed turn order is wrong and the next speaker should be chosen by context, SelectorGroupChat uses a model to pick which agent speaks next:
from autogen_agentchat.teams import SelectorGroupChat
team = SelectorGroupChat(
[planner, coder, reviewer],
model_client=model_client,
termination_condition=termination,
)
SelectorGroupChat needs its own model_client because it uses a model call to select the speaker. A common Claude mistake is to forget this argument; note it in CLAUDE.md.
Streaming output to the console
For anything interactive, streaming the conversation as it happens beats waiting for the whole run. The v0.4 API exposes run_stream, and AutoGen ships a console helper.
import asyncio
from autogen_agentchat.ui import Console
from autogen_agentchat.teams import RoundRobinGroupChat
# ... agents and termination as before ...
async def main() -> None:
team = RoundRobinGroupChat([writer, editor], termination_condition=termination)
await Console(team.run_stream(task="Write a tagline for a terminal-based dev tool."))
await model_client.close()
if __name__ == "__main__":
asyncio.run(main())
Console(...) consumes the async stream and prints each message as it arrives, including token usage. This is the right default for development. Add it to CLAUDE.md so Claude reaches for run_stream plus Console rather than the blocking run when you ask for visible progress.
Permission hooks for agent scripts
A multi-agent project accumulates scripts: ones that just chat, and ones whose tools touch real systems (send email, write to a database, call a paid API in a loop). Permission rules in .claude/settings.local.json keep the dangerous ones from running unattended.
{
"permissions": {
"allow": [
"Bash(python main.py*)",
"Bash(python team.py*)",
"Bash(pytest*)"
],
"deny": [
"Bash(python run_production_agents.py*)",
"Bash(python send_emails_agent.py*)"
]
}
}
Scripts that run a local demo or the test suite are safe to auto-execute. Scripts that put a team to work against production systems or send real messages should surface as a prompt, not run automatically during a "try the agents" task. For more on configuring this safely, Claude Code setup covers the permission model in full.
Common AutoGen mistakes Claude makes by default
Six patterns Claude generates without CLAUDE.md constraints, each with the correct replacement.
1. Installing and importing the v0.2 package
Claude generates: pip install pyautogen and from autogen import ConversableAgent.
Correct: pip install -U "autogen-agentchat" "autogen-ext[openai]" and from autogen_agentchat.agents import AssistantAgent. The old autogen package is the pre-rewrite API and its classes do not exist in v0.4.
2. Synchronous initiate_chat instead of async run
Claude generates: user_proxy.initiate_chat(assistant, message="...") with no await.
Correct: result = await agent.run(task="...") inside an async def main() launched with asyncio.run(main()). The v0.4 API is async-first.
3. No termination condition on a team
Claude generates: RoundRobinGroupChat([a, b]) with no termination, which can loop until it runs out of budget.
Correct: RoundRobinGroupChat([a, b], termination_condition=TextMentionTermination("TERMINATE") | MaxMessageTermination(10)). Always set both a semantic stop and a hard cap.
4. Hardcoding the API key in the model client
Claude generates: OpenAIChatCompletionClient(model="gpt-4o", api_key="sk-...") with a literal key.
Correct: omit api_key and let the client read OPENAI_API_KEY from the environment, or load it from a secrets manager. Never commit a key.
5. Forgetting to close the model client
Claude generates: a script that runs the agent and exits without closing the client, leaving an open connection pool.
Correct: await model_client.close() after the run, ideally in a finally block so it runs even on error.
6. Tools without type hints or docstrings
Claude generates: def my_tool(x): return do_something(x) with no annotations.
Correct: async def my_tool(x: str) -> str: with a docstring describing the argument. AutoGen builds the function-calling schema from the signature and docstring, so both are required for the tool to work.
Add all six pairs to CLAUDE.md as a common mistakes section. The before/after contrast lets Claude match the pattern it would otherwise generate to the correct form.
Building reliable multi-agent systems
The AutoGen CLAUDE.md in this guide produces code that installs the right packages, uses the async-first v0.4 API, sets a termination condition on every team, reads API keys from the environment, closes the model client cleanly, and gives every tool the type hints and docstring AutoGen needs to build its schema.
The underlying principle is the same for any fast-moving framework: a major version rewrite splits the training data between old and new, and without an explicit instruction Claude reaches for whichever is more common, which for AutoGen is the deprecated v0.2 API. The CLAUDE.md template pins it to the version you installed, turning AutoGen from a framework Claude gets subtly wrong into one it gets right by default.
For the mental model of how a coordinator delegates to specialised workers, Claude Code subagents shows the same pattern in Claude Code's own architecture. To keep your AutoGen project's conventions stable across sessions, the CLAUDE.md memory setup covers how to structure the file so the constraints persist.
Get Claudify. The AutoGen CLAUDE.md template, async team patterns, and permission rules are included, ready to drop into any project.
More like this
Ready to upgrade your Claude Code setup?
Get Claudify