A RoundRobinGroupChat is a team primitive in Microsoft’s AutoGen framework that hands the conversation to each registered agent in strict sequential order, one turn at a time, until a termination condition fires. This autogen roundrobingroupchat tutorial defines the primitive, walks the turn mechanics, and shows a runnable example so you can drop it into a pipeline without guessing.
What RoundRobinGroupChat Actually Is
RoundRobinGroupChat lives in autogen_agentchat.teams. It is the simplest deterministic orchestrator in the AutoGen agentchat layer. You give it a list of agents and a max_turns count. The team maintains a single shared message list. On each step it advances an internal cursor and asks the next agent in the list to produce a reply based on the full history so far.
It is not a scheduler that picks the “best” agent. It is not a load balancer. It is a fixed carousel. If you registered [A, B, C], the sequence of speakers is A, B, C, A, B, C, … until the turn budget expires or an agent signals termination.
The contrast with SelectorGroupChat is sharp: Selector uses an LLM to choose the next speaker from the roster. RoundRobin ignores model judgment for speaker selection entirely. That makes it cheaper and reproducible.
How The Turn Cycle Works
The team loop is straightforward:
- The task message is injected as the first user message.
- Cursor starts at index 0.
- The agent at cursor generates a response (or a tool call sequence) and appends to history.
- Cursor increments modulo list length.
- Repeat until
max_turnsis reached or a termination signal (StopMessageor agent returningNone) is observed.
Termination is local to the agent, not global policy. An AssistantAgent can return a StopMessage to end early. If no agent stops, the loop strictly respects max_turns. Each turn consumes one slot from max_turns, regardless of which agent spoke.
A subtle point: the agent sees the entire conversation, including its own prior turns. There is no isolated state per agent unless you build it. Memory and context grow linearly with turns.
Why It Matters For Multi-Agent Systems
Determinism is the headline benefit. When you are debugging a research pipeline, you want to know that agent B always reacts to agent A’s output, not to a stochastic selector’s mood. RoundRobin gives you a fixed interaction graph.
It also bounds cost and latency predictably. With a selector, you pay for an extra model call to decide who speaks. With RoundRobin, that overhead is zero. For two to five agents in a tight loop, the savings are real.
Finally, it forces explicit role design. Because order is fixed, you must think about who should ingest raw input first, who should refine, who should validate. That discipline improves agent prompts even if you later switch to a dynamic team.
A Concrete autogen roundrobingroupchat tutorial Example
Below is a minimal but real AutoGen 0.4 setup. Two agents — a researcher and a writer — take alternating turns on a technical summarization task. We point the model client at OpenAI, but the same client works against any OpenAI-compatible endpoint.
import os
import asyncio
from autogen_agentchat.agents import AssistantAgent
from autogen_agentchat.teams import RoundRobinGroupChat
from autogen_agentchat.events import MessageEvent
from autogen_ext.models.openai import OpenAIChatCompletionClient
model_client = OpenAIChatCompletionClient(
model="openai/gpt-4o-mini",
api_key=os.environ["OPENAI_API_KEY"],
# To route across 240+ models with one gateway and automatic fallback,
# set base_url to an OpenAI-compatible service such as n4n.ai.
# base_url="https://api.n4n.ai/v1",
)
researcher = AssistantAgent(
name="researcher",
model_client=model_client,
system_message="You gather facts and cite sources. Be concise. Do not write prose.",
)
writer = AssistantAgent(
name="writer",
model_client=model_client,
system_message="You turn the researcher's bullet points into clear prose. Add no new facts.",
)
team = RoundRobinGroupChat([researcher, writer], max_turns=4)
async def main():
stream = team.run_stream(task="Summarize the trade-offs of WebSocket vs SSE.")
async for event in stream:
if isinstance(event, MessageEvent):
print(f"{event.sender}: {event.message.content}")
asyncio.run(main())
With max_turns=4, the speaker order is researcher, writer, researcher, writer. The researcher gets the first crack at the task, the writer reacts, then they refine once more. If the researcher emits a StopMessage on turn 3, the loop ends early.
You can inspect the raw history after the run:
async def dump_history():
result = await team.run(task="Summarize the trade-offs of WebSocket vs SSE.")
for msg in result.messages:
print(msg.sender, "->", msg.content[:80])
asyncio.run(dump_history())
team.run (non-streaming) returns a TeamResult with .messages and .stop_reason. Use it in tests where you don’t want to await a stream.
Common Misconceptions
“It balances load across models.” No. RoundRobin balances speaking order across your agent list. If all agents share one model client, they hit the same model. If you want fallback when a provider is degraded, that logic belongs in the model client or gateway, not the team.
“It picks the right agent for each subtask.” It picks the next agent in a list. If your workflow needs a critic to intervene only when code fails, RoundRobin will still make the critic speak on its scheduled turn. Use SelectorGroupChat or a custom Team base class for conditional routing.
“max_turns is per agent.” It is per team step. Three agents with max_turns=3 means exactly three messages total, not nine. Size the budget for the whole carousel.
“Agents can see only the message before them.” They receive the full ChatMessage list. Long runs will blow context windows. Trim history or use agents with built-in memory compaction.
“It’s only for chat.” The shared message list can carry tool calls, function results, and structured JSON. We use it to pass extracted schema between a parser agent and a validator agent in fixed steps.
When To Reach For Something Else
If your agent count is dynamic, or the next speaker depends on intermediate output, RoundRobin is the wrong tool. SelectorGroupChat uses a small LLM call to score readiness. Swarm (in older AutoGen) or hand-rolled state machines fit open-ended coding sessions.
RoundRobin shines when the interaction pattern is known at design time: reviewer-after-writer, translator-after-extractor, validator-after-generator. In those cases the rigidity is a feature.
Practical Tips For Production
Set max_turns conservatively. A bug that causes an agent to loop on a tool call will otherwise burn tokens until the context limit. Log the stop_reason from TeamResult — "max_turns" vs "stop_message" tells you if the conversation completed or was truncated.
Give each agent a distinct system_message that acknowledges its position. The writer in our example is told not to invent facts because the researcher already ran. That constraint matters more in RoundRobin than in free chat, because the writer will be asked again.
If you need provider redundancy, configure the model client once with a gateway that honors routing directives. The team code stays identical. That separation of concerns is why AutoGen’s client abstraction is worth using instead of hardcoding SDKs per agent.
Finally, write a unit test that asserts speaker order. It is the cheapest regression check you can add:
def test_speaker_order():
agents = [AssistantAgent(name=f"a{i}", model_client=model_client,
system_message="") for i in range(3)]
team = RoundRobinGroupChat(agents, max_turns=6)
# Inspect internal cursor via run or mock; order must be 0,1,2,0,1,2
assert [a.name for a in team._agents] == ["a0", "a1", "a2"]
(The _agents attribute is an implementation detail; prefer testing via emitted MessageEvent.sender in a sandbox.)
RoundRobinGroupChat is boring by design. In multi-agent systems, boring is what lets you ship.