Code review: CrewAI multi-agent email crew — why are my agents receiving each other's emails? #39
Replies: 3 comments
-
|
The routing fails sometimes because subject-line routing is fundamentally fragile. You've built a system where a single customer reply can silently break your entire routing. Let me explain the failure modes and the correct architecture. Root cause: subject-line routing breaks in all the common casesCase 1: Customer changes the subject You send: Now Case 2: Subject prefix accumulation After a few email rounds: Case 3: No subject at all Delivery status notifications (bounces, out-of-office replies, read receipts) often arrive with no subject or with subjects like Case 4: Ambiguous subjects A prospect replies to an outreach email with Architecture fix: one inbox per agentRoute by # Create one inbox per agent at startup or initialization
INBOX_IDS = {
"research": os.environ["RESEARCH_INBOX_ID"], # research@yourapp.commune.email
"outreach": os.environ["OUTREACH_INBOX_ID"], # outreach@yourapp.commune.email
"support": os.environ["SUPPORT_INBOX_ID"], # support@yourapp.commune.email
}
@app.post("/webhook")
def handle():
raw_body = request.get_data()
try:
verify_signature(
payload=raw_body,
signature=request.headers["x-commune-signature"],
secret=os.environ["COMMUNE_WEBHOOK_SECRET"],
timestamp=request.headers.get("x-commune-timestamp"),
)
except WebhookVerificationError:
return {"error": "invalid signature"}, 401
data = request.json
inbox_id = data["inbox"]["id"] # always present, always correct
if inbox_id == INBOX_IDS["research"]:
Thread(target=research_agent.handle, args=(data,), daemon=True).start()
elif inbox_id == INBOX_IDS["outreach"]:
Thread(target=outreach_agent.handle, args=(data,), daemon=True).start()
elif inbox_id == INBOX_IDS["support"]:
Thread(target=support_agent.handle, args=(data,), daemon=True).start()
else:
logger.warning(f"Unhandled inbox_id: {inbox_id}")
return {"status": "queued"}, 200Now when a customer changes the subject, replies from a different email client, or triggers a bounce — the routing still works because it's based on which inbox received the message, not what the subject says. Issue 2: Missing
|
| Issue | Root cause | Fix |
|---|---|---|
| Cross-agent routing failures | Subject-line matching breaks on reply/forward/bounce | One inbox per agent, route by inbox_id |
| New thread on every send | No thread_id in send_email |
Add thread_id parameter, pass from webhook context |
| Injection risk | No signature verification | Add verify_signature() before processing |
| Silent drops | else: pass swallows unmatched messages |
Log and alert on unmatched inbox_ids |
The single shared inbox architecture is the root problem — once you move to per-agent inboxes, the routing becomes trivial and reliable.
Beta Was this translation helpful? Give feedback.
-
|
Makes sense — the shared inbox routing is clearly the wrong approach. I'll move to one inbox per agent. But I have a follow-up: if I create one inbox per agent, how do I handle a CrewAI crew where agents can scale from 3 to 10 based on workload? The number of active agents changes at runtime. Do I create inboxes programmatically? And do I delete them when agents are decommissioned, or leave them? |
Beta Was this translation helpful? Give feedback.
-
|
Yes, inboxes are fully programmatic — create them at agent initialization, configure the webhook, and store the Crew initialization with dynamic inbox creationimport os
from commune import CommuneClient
from crewai import Agent, Crew
client = CommuneClient(api_key=os.environ["COMMUNE_API_KEY"])
def create_agent_inbox(role: str, crew_id: str) -> str:
"""Create a dedicated inbox for an agent. Returns the inbox_id."""
local_part = f"agent-{role}-{crew_id}" # e.g. "agent-outreach-crew42"
inbox = client.inboxes.create(
local_part=local_part,
webhook_url=f"{os.environ['APP_URL']}/webhook",
)
return inbox.id # store this — it's your routing key
def spin_up_crew(crew_id: str, agent_roles: list[str]) -> Crew:
agents = []
inbox_map = {} # role -> inbox_id
for role in agent_roles:
inbox_id = create_agent_inbox(role, crew_id)
inbox_map[role] = inbox_id
# Build tools closed over this agent's inbox_id
tools = make_tools_for_agent(inbox_id, client)
agent = Agent(
role=role,
goal=f"Handle {role} tasks for crew {crew_id}",
tools=tools,
# store inbox_id in metadata so the agent knows its own address
)
agents.append(agent)
# Store inbox_map somewhere persistent (Redis, DB) so webhook handler can route
store_inbox_map(crew_id, inbox_map)
return Crew(agents=agents, tasks=[])
def make_tools_for_agent(inbox_id: str, client: CommuneClient):
@tool
def send_email(to: str, body: str, thread_id: str | None = None) -> str:
"""Send an email from this agent's inbox."""
send_kwargs = dict(
inbox_id=inbox_id, # closed over — always correct
to=to,
text=body,
idempotency_key=f"send-{thread_id}-{hash(body)}",
)
if thread_id:
send_kwargs["thread_id"] = thread_id
client.messages.send(**send_kwargs)
return "Sent"
@tool
def check_emails() -> str:
"""Check this agent's inbox for new messages."""
threads = client.threads.list(inbox_id=inbox_id, last_direction="inbound", limit=10)
return str(threads)
return [send_email, check_emails]Webhook routing with the dynamic inbox map@app.post("/webhook")
def handle():
raw_body = request.get_data()
verify_signature(raw_body, ...) # always first
data = request.json
inbox_id = data["inbox"]["id"]
crew_id, role = lookup_crew_by_inbox(inbox_id) # reverse lookup from your DB/Redis
if crew_id is None:
logger.warning(f"Webhook for unknown inbox: {inbox_id}")
return {"status": "unknown_inbox"}, 200
Thread(target=dispatch_to_agent, args=(crew_id, role, data), daemon=True).start()
return {"status": "queued"}, 200On decommissioningLeave the inboxes — don't delete them. If a prospect replies to an outreach email days after the agent was decommissioned, you still want to receive that email. Route unmatched inboxes to a human review queue or a catch-all crew. Commune doesn't charge for inactive inboxes, so there's no cost to keeping them. If you do need to clean up (e.g., dev/test crews), call |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
I have a CrewAI crew with 3 specialized agents that all communicate via email. The problem: sometimes ResearchAgent receives emails meant for OutreachAgent, and vice versa. My routing logic should prevent this but it's not working reliably.
I've verified the webhook is receiving emails correctly. The routing just fails sometimes.
Beta Was this translation helpful? Give feedback.
All reactions