# How AutoGen Agents Exchange Messages: 4 Flow Patterns

> Learn how AutoGen agents exchange messages through direct chats, group routing, handoffs, and shared memory, then debug loops, stalls, and bad recipients.

- Author: Swarnava Dutta (https://swarnava.dev)
- Published: 2026-08-20
- Tags: Autogen Multi Agent Example, Autogen Agent Messaging, Autogen Group Chat
- Reading time: 8 min (1846 words)
- Canonical: https://swarnava.dev/blogs/autogen-agents-exchange-messages

---

![Illustration of how to autogen agents exchange messages: A wide switchboard table: two telephone handsets connected by a](/images/blogs/autogen-agents-exchange-messages-hero.jpg)

I had two AutoGen agents that worked perfectly in isolation - an analyst and a coder - but the moment I wired them into a group chat, the coder kept answering its own questions while the analyst sat idle. The issue wasn't the LLM. It was speaker selection defaulting to the last speaker, with my handoff logic missing entirely. I spent most of a Saturday tracing message objects before the routing clicked.

## Quick answer

AutoGen agents exchange messages through four patterns: direct request/response between two agents, typed message handlers with publish/subscribe topics, group chat with automatic speaker selection, and explicit handoffs naming the next agent. Direct chats use `run()`, while multi-agent flows rely on group chat with a speaker-selection policy or `Handoff` objects. Debugging failures requires inspecting message history on each agent.

## How AutoGen Agents Exchange Messages: The 4 Core Flows

Every message follows the same lifecycle: an agent creates a typed object, the runtime routes it to a recipient or topic, the handler processes it and generates a response, and the framework appends both to history. The routing step is where the four patterns diverge.

| Criteria | Direct | Group Chat | Handoff | Broadcast |
|---|---|---|---|---|
| Recipient control | Explicit agent ref | Speaker-selection policy | Named in `Handoff` | Topic string |
| Agent count | 2 | 3+ | 2+ | Any subscribers |
| Response expected | Yes | From selected speaker | From target | None guaranteed |
| Context ownership | Shared between pair | Team-level history | Transfers with control | Per-subscriber |
| Best use case | Simple Q&A loop | Multi-role collaboration | Pipeline stages | Fanout notifications |

One distinction trips people up constantly: **AgentChat API vs. Core API**. The current AgentChat layer (`autogen_agentchat`) gives you `AssistantAgent`, `Handoff`, and `RoundRobinGroupChat`. The Core layer (`autogen_core`) exposes typed message handlers and publish/subscribe topics. Legacy `ConversableAgent` from AutoGen 0.2 uses entirely different imports. Mixing them produces silent failures - agents register but never receive messages.

### Agent Messages vs. Internal Events

Not everything in a streamed run is a message another agent receives. Chat messages (`TextMessage`, `MultiModalMessage`) route to other agents and are model-visible. Tool-call events and execution results stay internal unless explicitly forwarded. Handoff events carry a `target` field naming the next agent. Termination signals (`StopMessage`) end the loop without producing a reply.

Each message carries `source`, a recipient or topic, a concrete type for handler dispatch, and a `content` payload. The `source` field matters more than you'd expect - my group chat bug traced to the speaker selector reading `source` to decide turns, and my custom messages had the wrong value set.

## Direct Messaging: A Two-Agent AutoGen Example

```python
import asyncio
from autogen_agentchat.agents import AssistantAgent
from autogen_agentchat.conditions import MaxMessageTermination
from autogen_ext.models.openai import OpenAIChatCompletionClient

async def main():
    model = OpenAIChatCompletionClient(model="gpt-4o-mini")
    analyst = AssistantAgent("analyst", model_client=model,
        system_message="You analyze data and ask clarifying questions.")

    result = await analyst.run(task="Write a pandas snippet to pivot sales by region")
    for msg in result.messages:
        print(f"{msg.source}: {msg.content}")

asyncio.run(main())
```

The `run()` method creates the initial `TextMessage`, the runtime delivers it, and the agent's handler produces a reply in `result.messages`. No group manager involved.

**Version note:** Legacy AutoGen 0.2 used `ConversableAgent` with `initiate_chat()`. The current `AssistantAgent.run()` replaces both. Swap `initiate_chat` for `run(task=...)` and move termination config to explicit `TerminationCondition` objects.

### Request/Response and Multi-Turn Replies

Extending to follow-up turns means letting the runtime loop. Pass `MaxMessageTermination(6)` and each agent sees the full prior message list as context every turn.

This differs fundamentally from [group chat routing](/blogs/build-agent-orchestration). Sending a direct message guarantees the target responds. In a group chat, the manager might route your message to a different agent based on its selection policy. Bound your turns explicitly - open-ended loops burn tokens fast as each turn feeds the entire history back into the model.

## Message Handlers, Typed Routing, and Broadcast Topics

The Core API routes messages by Python type, not string matching. You define a handler decorated with `@message_handler`, specifying the message type it accepts. The runtime dispatches to the matching handler deterministically - a `CodeReviewRequest` always hits the `CodeReviewRequest` handler, regardless of content.

```python
from dataclasses import dataclass
from autogen_core import MessageContext, RoutedAgent, message_handler

@dataclass
class CodeReviewRequest:
    code: str
    language: str
    correlation_id: str

@dataclass
class CodeReviewResult:
    approved: bool
    comments: str

class ReviewerAgent(RoutedAgent):
    @message_handler
    async def handle_review(self, message: CodeReviewRequest, ctx: MessageContext) -> CodeReviewResult:
        return CodeReviewResult(approved=True, comments="Looks good")
```

For fanout - logging a transaction while also auditing it - publish/subscribe topics replace direct delivery. An agent publishes to a `TopicId`, and every subscriber receives a copy.

When two handlers accept the same base type, use distinct dataclass types per intent or add a `correlation_id` field. Relying on natural-language recipient names inside `content` breaks the moment someone edits a prompt. For handlers with side effects, stamp each message with a unique ID and check before executing - I've seen agents double-post Slack notifications because a handler assumed every delivery was unique.

## AutoGen Group Chat: Speaker Selection and Agent Handoffs

When a task hits a `SelectorGroupChat`, the team manager wraps it in a `TextMessage` and picks the first speaker. Each reply appends to a shared list every participant sees. The manager then selects the next speaker - round-robin rotates mechanically, while selector-based choice prompts the model using agent names and descriptions.

Deterministic handoffs work better for pipelines. A `Handoff(target="reviewer")` tells the runtime exactly who goes next - no model decision involved. I had a "code_writer" and "code_reviewer" with near-identical descriptions, and the selector treated them as interchangeable until I made each description state what the agent should *never* do.

![Comparison diagram of selector based speaker selection versus deterministic handoff routing that decides which AutoGen agents exchange messages next in a group chat](/images/blogs/autogen-agents-exchange-messages-diagram-1.jpg "Selector-based choice vs deterministic handoff")

The most common failure: an agent addresses the group chat manager instead of the intended specialist. This happens when the model sees the manager's name in the participant list. Constrain `allowed_speaker_transitions` to exclude it.

### Prevent Wrong Speakers, Endless Turns, and Handoff Loops

- **Unique names and non-overlapping descriptions.** `analyst_financial` beats `agent_2`. State boundaries: "Only answers SQL questions. Never writes Python."
- **Constrained candidate sets.** Pass `allowed_speaker_transitions` to limit which agents can follow which. For handoff loops (A hands to B, B immediately hands back), check the last two speakers before accepting.
- **Stacked termination conditions.** Combine `MaxMessageTermination(20)` with `TextMentionTermination("TASK_COMPLETE")`. Without these, a confused selector burns your token budget - I've watched a three-agent team generate 80+ turns before I killed the process.

## AutoGen Message History, Shared Context, and Agent Memory

AutoGen teams maintain a shared message list that every participant reads during a run. It's transient - dies when `run()` returns. Per-agent model context is separate: each `AssistantAgent` builds its own prompt from team history plus its system message. Application state lives entirely outside both.

Nothing persists automatically between runs. Extract structured facts and store them yourself - database, cache, or vector store. Inject only the relevant slice back into the next run's `task` or system message.

![Diagram of session, user, agent, and team memory scopes AutoGen agents use to exchange messages, feeding into one authoritative owner of shared mutable state](/images/blogs/autogen-agents-exchange-messages-diagram-2.jpg "Four scopes of memory ownership")

Context-window growth drives up costs faster than most teams expect. For long sessions, summarize earlier turns into a compact state object and reset the message list. Isolate sessions by keying state on `(user_id, session_id)` to prevent agents leaking context across users.

### Choose Memory Scope and Ownership

Five scopes cover most designs: **session** (one `run()` call), **user** (preferences persisted across sessions), **agent** (tool credentials scoped to one instance), **team** (shared task state in a group chat), and **application** (global config and rate limits). Assign one authoritative owner for any mutable shared state. I hit this with a planner and executor both writing to the same task-status key - the result matched neither agent's intent. Designate one writer; others read only. Store durable facts as structured records (`{"region": "EMEA", "q2_revenue": 4200000}`), not raw transcript fragments that drift when summarized.

## Trace How AutoGen Agents Exchange Messages at Runtime

Instrument every message with timestamp, `source`, destination, message class name, turn number, and `correlation_id`. Without this, [debugging a multi-agent system](/blogs/evaluate-multi-agent-systems) is guessing.

Start by verifying your API version - confirm you're importing from `autogen_agentchat` or `autogen_core`, not the legacy `autogen` package. Mixed imports produce agents that silently never receive messages. Next, log the raw message object before and after handler dispatch; if a message appears in logs but never reaches model context, the handler returned before appending it. Finally, check speaker selection by logging the candidate list and chosen speaker each turn - wrong-speaker bugs hide here.

Silent stalls almost always trace to blocking synchronous work inside an `async` handler (the event loop freezes) or a swallowed exception where a broad `try/except` returns `None` instead of a reply. The runtime interprets the missing reply as "conversation over." For repeatable tests, replace live model clients with deterministic test doubles that return fixed responses, and assert on message order, recipient, and payload type rather than generated text.

## How to Deploy AutoGen Agents Without Losing Message State

Moving from `asyncio.run(main())` to production means splitting four concerns: agent definitions (version-controlled config), model clients (keys from environment variables), runtime execution (API handler or background worker), and persistent state (database or cache behind its own connections).

After a process restart, restore session state by loading structured records keyed on `(session_id, correlation_id)` - not by replaying the prior transcript. Replaying re-triggers tool calls, double-posts notifications, and burns tokens reconstructing what you already know.

For production hardening, isolate one runtime instance per session to avoid shared-state overwrites. Wrap model client calls with timeouts and retry on transient 429/503 errors with exponential backoff. Thread a `correlation_id` from the inbound API request through every message, log line, and database write - without it, correlating a user complaint to a specific agent turn across workers is nearly impossible. Emit structured JSON logs with `source`, destination, and message type to whatever aggregator your team runs.

The simplest production path: a FastAPI endpoint that accepts a task, spins up agents, calls `run()`, streams results via SSE, and writes final state to Postgres. When volume grows, move execution behind a task queue so the API returns immediately. The [agent definitions stay identical](/blogs/autogen-vs-agents-sdk) - only the execution wrapper changes.

## FAQ

### How do AutoGen agents exchange messages?

Through typed Python objects routed by the runtime across four patterns: direct request/response via `run()`, typed message handlers dispatched by class, group chat with speaker selection, and explicit `Handoff` objects naming the next agent. Each message carries `source`, recipient, type, and content that handlers dispatch on.

### What is AutoGen?

An open-source multi-agent framework from Microsoft. It provides AgentChat for high-level team orchestration with assistants, group chats, and handoffs, and Core for typed message routing with publish/subscribe topics. Agents coordinate through structured message passing rather than shared memory.

### How to handle agent memory?

Store structured facts from conversations in a database or vector store - not raw transcripts. AutoGen's shared message list resets after each `run()` call. Inject only relevant prior state into the next run's task or system message. Key records on `(user_id, session_id)` to prevent context leaking across users.

### How to deploy AutoGen agents?

Wrap agent execution behind an API endpoint that accepts a task, instantiates agents, calls `run()`, and writes structured results to persistent storage. Pull API keys from environment variables, isolate one runtime per session, and thread a `correlation_id` through every message and log for end-to-end tracing.
