Sitemap
Press enter or click to view image in full size
Building a Multi Agent System

Building Multi-Agent Systems: A Complete Guide with Email Triage Example

12 min readJul 7, 2025

--

Introduction

Multi-agent systems represent a powerful approach to solving complex problems by breaking them down into smaller, specialized components. A multiagent system (MAS) consists of multiple artificial intelligence (AI) agents working collectively to perform tasks on behalf of a user or another system.

In this guide, we’ll explore what multi-agent systems are, why they’re useful, and walk through building a practical email triage system using LangChain.js and LangGraph.

What Are Multi-Agent Systems?

Each agent can have its own prompt, LLM, tools, and other custom code to best collaborate with the other agents. Think of it like having a team of specialists, where each expert focuses on their domain of expertise rather than one generalist trying to handle everything.

Key Benefits of Multi-Agent Systems

Grouping tools/responsibilities can give better results. An agent is more likely to succeed on a focused task than if it has to select from dozens of tools. Here are the main advantages:

  • Modularity: Separate agents make it easier to develop, test, and maintain complex systems. You can update one agent without affecting others.
  • Specialization: Each agent becomes an expert in its domain, leading to better performance than a single general-purpose agent.
  • Scalability: Multiagent systems can adjust to varying environments by adding, removing or adapting agents.
  • Control: You have explicit control over how agents communicate and when they’re activated.

Multi-Agent Architectures

There are several ways to connect agents in a multi-agent system:

1. Supervisor Architecture

Each agent communicates with a single supervisor agent. Supervisor agent makes decisions on which agent should be called next. This is perfect for our email triage system where we need centralized decision-making.

2. Network Architecture

Each agent can communicate with every other agent. Any agent can decide which other agent to call next. This works well when there’s no clear hierarchy.

3. Hierarchical Architecture

You can define a multi-agent system with a supervisor of supervisors. This enables complex, nested team structures.

Building an Email Triage System

Let’s build a practical multi-agent email triage system that automatically categorizes and routes incoming emails to the right departments. Our system will use the supervisor architecture with specialized agents for different email types.

System Architecture

Our email triage system consists of:

  1. Supervisor Agent: Orchestrates the triage process and makes final routing decisions
  2. Customer Support Agent: Handles technical issues and customer inquiries
  3. Sales Agent: Manages business inquiries and lead qualification
  4. HR Agent: Processes recruitment and employee-related emails
  5. Spam Filter Agent: Identifies and filters low-priority communications

Step 1: Project Setup

First, let’s set up our TypeScript project with the necessary dependencies:

mkdir multi-agent-email-triage
cd multi-agent-email-triage
npm init -y

Install the required packages:

npm install @langchain/core @langchain/langgraph @langchain/openai dotenv zod
npm install -D typescript @types/node ts-node

Create a tsconfig.json :

{
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"strict": false,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"noEmitOnError": false,
"incremental": false,
"declaration": false,
"declarationMap": false,
"sourceMap": false
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist",
"**/*.test.ts",
"**/*.spec.ts"
],
"ts-node": {
"compilerOptions": {
"module": "CommonJS"
}
}
}

Step 2: Define Types

Create src/types.tsto define our data structures:

export interface Email {
id: string;
from: string;
to: string;
subject: string;
body: string;
timestamp: Date;
priority?: 'low' | 'medium' | 'high' | 'urgent';
category?: string;
}

export interface TriageResult {
emailId: string;
category: string;
priority: 'low' | 'medium' | 'high' | 'urgent';
assignedAgent: string;
summary: string;
suggestedActions: string[];
requiresHumanReview: boolean;
}

export interface AgentDecision {
shouldHandle: boolean;
confidence: number;
reasoning: string;
suggestedActions?: string[];
escalate?: boolean;
}

export interface EmailTriageState {
email: Email;
category?: string;
priority?: 'low' | 'medium' | 'high' | 'urgent';
assignedAgent?: string;
agentDecisions: Record<string, AgentDecision>;
finalResult?: TriageResult;
requiresEscalation: boolean;
}

Step 3: Implement Specialized Agents

Create src/agents/customerSupportAgent.ts:

import { ChatOpenAI } from "@langchain/openai";
import { Email, AgentDecision } from "../types";

/**
* Customer Support Agent - Specializes in handling customer support requests,
* technical issues, and service-related inquiries
*/
export class CustomerSupportAgent {
private llm: ChatOpenAI;
private name = "CustomerSupport";

constructor() {
this.llm = new ChatOpenAI({
model: "gpt-4o-mini",
temperature: 0.1,
});
}

/**
* Evaluate if this agent should handle the email
*/
async evaluate(email: Email): Promise<AgentDecision> {
const prompt = `
You are a Customer Support Agent specializing in handling customer support requests, technical issues, and service-related inquiries.

Analyze this email and determine if it should be handled by the Customer Support team:

FROM: ${email.from}
SUBJECT: ${email.subject}
BODY: ${email.body}

Consider these factors:
1. Is this a customer support request?
2. Does it involve technical issues or service problems?
3. Does the customer need assistance or resolution?
4. What is the urgency level?

Respond with your analysis in this format:
SHOULD_HANDLE: true/false
CONFIDENCE: 0-100
REASONING: Brief explanation
SUGGESTED_ACTIONS: List of recommended actions
ESCALATE: true/false (if requires immediate escalation)
`;

try {
const response = await this.llm.invoke(prompt);
const content = response.content as string;

// Parse the response
const shouldHandle = content.includes("SHOULD_HANDLE: true");
const confidenceMatch = content.match(/CONFIDENCE: (\d+)/);
const confidence = confidenceMatch ? parseInt(confidenceMatch[1]) : 50;

const reasoningMatch = content.match(/REASONING: (.+?)(?=\n|$)/);
const reasoning = reasoningMatch
? reasoningMatch[1].trim()
: "Analysis completed";

const escalateMatch = content.match(/ESCALATE: (true|false)/);
const escalate = escalateMatch ? escalateMatch[1] === "true" : false;

// Extract suggested actions
const actionsMatch = content.match(
/SUGGESTED_ACTIONS: (.+?)(?=\nESCALATE|$)/s
);
const suggestedActions = actionsMatch
? actionsMatch[1]
.split("\n")
.map((action) => action.trim())
.filter((action) => action.length > 0)
: [];

return {
shouldHandle,
confidence,
reasoning,
suggestedActions,
escalate,
};
} catch (error) {
console.error("Error in CustomerSupportAgent evaluation:", error);
return {
shouldHandle: false,
confidence: 0,
reasoning: "Error occurred during evaluation",
escalate: true,
};
}
}

getName(): string {
return this.name;
}
}

The other specialized agents follow the same schema, but have different prompts. Check the code here.

Step 4: Implement the Supervisor Agent

Create src/agents/supervisorAgent.ts . The goal of this agent is to supervise and hand of specific tasks to the specific agents.

import { ChatOpenAI } from '@langchain/openai';
import { z } from 'zod';
import { Email, EmailTriageState, TriageResult } from '../types';
import { CustomerSupportAgent } from './customerSupportAgent';
import { SalesAgent } from './salesAgent';
import { SpamFilterAgent } from './spamFilterAgent';
import { HRAgent } from './hrAgent';

/**
* Supervisor Agent - Orchestrates the email triage process by coordinating
* specialist agents and making final decisions about email routing
*/
export class SupervisorAgent {
private llm: ChatOpenAI;
private customerSupportAgent: CustomerSupportAgent;
private salesAgent: SalesAgent;
private spamFilterAgent: SpamFilterAgent;
private hrAgent: HRAgent;

constructor() {
this.llm = new ChatOpenAI({
model: 'gpt-4o-mini',
temperature: 0.2,
});

this.customerSupportAgent = new CustomerSupportAgent();
this.salesAgent = new SalesAgent();
this.spamFilterAgent = new SpamFilterAgent();
this.hrAgent = new HRAgent();
}

/**
* Main triage orchestration method
*/
async triageEmail(email: Email): Promise<TriageResult> {
console.log(`\n🎯 Supervisor: Starting triage for email "${email.subject}"`);

// Create initial state
const state: EmailTriageState = {
email,
agentDecisions: {},
requiresEscalation: false
};
// Step 1: Check for spam first (most efficient filter)
console.log('📧 Checking for spam...');
const spamDecision = await this.spamFilterAgent.evaluate(email);
state.agentDecisions['SpamFilter'] = spamDecision;

// If likely spam, filter out immediately
if (spamDecision.shouldHandle && spamDecision.confidence > 70) {
console.log('🚫 Email identified as spam/low-priority - filtering out');
return this.createTriageResult(state, 'Spam/Low-Priority', 'low', 'SpamFilter');
}

// Step 2: Evaluate with specialist agents in parallel
console.log('🔍 Evaluating with specialist agents...');
const [customerSupportDecision, salesDecision, hrDecision] = await Promise.all([
this.customerSupportAgent.evaluate(email),
this.salesAgent.evaluate(email),
this.hrAgent.evaluate(email)
]);

state.agentDecisions['CustomerSupport'] = customerSupportDecision;
state.agentDecisions['Sales'] = salesDecision;
state.agentDecisions['HR'] = hrDecision;

// Step 3: Make routing decision based on agent evaluations
const finalDecision = await this.makeRoutingDecision(state);

console.log(`✅ Supervisor: Final decision - Route to ${finalDecision.assignedAgent}`);
return finalDecision;
}
/**
* Make the final routing decision based on all agent evaluations
*/
private async makeRoutingDecision(state: EmailTriageState): Promise<TriageResult> {
const decisions = state.agentDecisions;

// Find agents that want to handle this email
const willingAgents = Object.entries(decisions)
.filter(([_, decision]) => decision.shouldHandle && decision.confidence > 50)
.sort(([_, a], [__, b]) => b.confidence - a.confidence);

console.log(`📊 Agents willing to handle: ${willingAgents.map(([name, decision]) =>
`${name}(${decision.confidence}%)`).join(', ')}`);

if (willingAgents.length === 0) {
// No agent wants to handle - escalate to general
return this.createTriageResult(state, 'General', 'medium', 'General', true);
}

if (willingAgents.length === 1) {
// Only one agent wants to handle
const [agentName, decision] = willingAgents[0];
const priority = this.determinePriority(decision);
return this.createTriageResult(state, agentName, priority, agentName, decision.escalate);
}

// Multiple agents want to handle - use LLM to decide
return await this.resolveConflict(state, willingAgents);
}
/**
* Resolve conflicts when multiple agents want to handle an email
*/
private async resolveConflict(
state: EmailTriageState,
willingAgents: [string, any][]
): Promise<TriageResult> {
const { email } = state;

const agentAnalysis = willingAgents.map(([name, decision]) =>
`${name}: ${decision.reasoning} (Confidence: ${decision.confidence}%)`
).join('\n');

const prompt = `
As a Supervisor Agent, you need to decide which specialist should handle this email.

EMAIL DETAILS:
FROM: ${email.from}
SUBJECT: ${email.subject}
BODY: ${email.body}

AGENT ANALYSES:
${agentAnalysis}

Based on the email content and agent analyses, decide which agent should handle this email.
Consider:
1. Which agent's expertise best matches the email content?
2. Which agent has the highest confidence and best reasoning?
3. What is the appropriate priority level?

Respond in this exact format:
ASSIGNED_AGENT: [AgentName]
PRIORITY: [low/medium/high/urgent]
REASONING: [Brief explanation]
`;

try {
const response = await this.llm.invoke(prompt);
const content = response.content as string;

const agentMatch = content.match(/ASSIGNED_AGENT: (\w+)/);
const priorityMatch = content.match(/PRIORITY: (low|medium|high|urgent)/);
const reasoningMatch = content.match(/REASONING: (.+?)(?=\n|$)/);

const assignedAgent = agentMatch ? agentMatch[1] : willingAgents[0][0];
const priority = priorityMatch ? priorityMatch[1] as any : 'medium';
const reasoning = reasoningMatch ? reasoningMatch[1].trim() : 'Conflict resolved by supervisor';

console.log(`🤔 Supervisor resolved conflict: ${assignedAgent} (${reasoning})`);

return this.createTriageResult(state, assignedAgent, priority, assignedAgent, false);
} catch (error) {
console.error('Error in conflict resolution:', error);
// Fallback to highest confidence agent
const [fallbackAgent] = willingAgents[0];
return this.createTriageResult(state, fallbackAgent, 'medium', fallbackAgent, false);
}
}
/**
* Helper method to determine priority based on agent decision
*/
private determinePriority(decision: any): 'low' | 'medium' | 'high' | 'urgent' {
if (decision.escalate) return 'urgent';
if (decision.confidence > 90) return 'high';
if (decision.confidence > 70) return 'medium';
return 'low';
}

/**
* Helper method to create triage result
*/
private createTriageResult(
state: EmailTriageState,
category: string,
priority: 'low' | 'medium' | 'high' | 'urgent',
assignedAgent: string,
requiresHumanReview: boolean = false
): TriageResult {
const assignedDecision = state.agentDecisions[assignedAgent];

return {
emailId: state.email.id,
category,
priority,
assignedAgent,
summary: assignedDecision?.reasoning || `Email categorized as ${category}`,
suggestedActions: assignedDecision?.suggestedActions || [`Route to ${assignedAgent} team`],
requiresHumanReview
};
}
}

Step 5: Create the Main Application

The main application starts the agents and processes incoming emails. You can build your own incoming logic here to provide email data to the agents.

import 'dotenv/config';
import { SupervisorAgent } from './agents/supervisorAgent';
import { TriageResult } from './types';

/**
* Multi-Agent Email Triage System
*
* This system demonstrates a sophisticated multi-agent approach to email triage,
* where specialized AI agents work together to categorize, prioritize, and route
* incoming emails to the appropriate departments.
*/

async function main() {
console.log('🚀 Starting Multi-Agent Email Triage System');
console.log('==============================================\n');

// Validate environment
if (!process.env.OPENAI_API_KEY) {
console.error('❌ Error: OPENAI_API_KEY environment variable is required');
console.log('Please set your OpenAI API key in a .env file:');
console.log('OPENAI_API_KEY=your_api_key_here');
process.exit(1);
}

// Initialize supervisor agent
const supervisor = new SupervisorAgent();
const results: TriageResult[] = [];

// Logic for processing incoming emails ...
}

// Handle errors gracefully
main().catch(error => {
console.error('❌ Application error:', error);
process.exit(1);
});

Benefits Over Single-Agent Systems

Improved Accuracy

Specialist agents consistently outperform generalist agents in their domains. A dedicated Sales Agent better identifies qualified leads than a general-purpose agent trying to handle all email types.

Better Scalability

Adding new capabilities doesn’t require retraining existing agents. You simply add new specialists to the system.

Fault Tolerance

If one agent fails or performs poorly, others can continue working. The system degrades gracefully rather than failing completely.

Explainable Decisions

Each agent provides reasoning for its decisions, making the overall system more transparent and debuggable.

Performance Considerations

Parallel Processing

Running agent evaluations in parallel significantly reduces response time:

// Parallel evaluation reduces latency
const [supportDecision, salesDecision, hrDecision] = await Promise.all([
this.customerSupportAgent.evaluate(email),
this.salesAgent.evaluate(email),
this.hrAgent.evaluate(email)
]);

Cost Optimization

The spam filter runs first to quickly eliminate low-value emails before expensive LLM evaluations:

// Quick spam check before expensive evaluations
const spamDecision = await this.spamFilterAgent.evaluate(email);

if (spamDecision.shouldHandle && spamDecision.confidence > 70) {
return this.filterAsSpam(email);
}

Caching and Memoization

Similar emails can reuse previous evaluations to reduce API calls and improve response times. This could be done with a simple Redis caching setup or similar light weight storage system.

Extending the System

Adding New Agents

To add a Legal Agent, you could use a system prompt like this:

You are a Legal specialist. Evaluate if this email requires legal attention.
Look for:
- Contract discussions and negotiations
- Legal notices and compliance issues
- Intellectual property matters
- Privacy and data protection concerns
- Litigation or dispute-related content
- Regulatory compliance questions
Assess urgency and legal risk level.

Do not forget to add that to the supervisor:

constructor() {
// … existing agents
this.legalAgent = new LegalAgent();
}
async triageEmail(email: Email): Promise<TriageResult> {
// Add to parallel evaluation
const [supportDecision, salesDecision, hrDecision, legalDecision] = await Promise.all([
this.customerSupportAgent.evaluate(email),
this.salesAgent.evaluate(email),
this.hrAgent.evaluate(email),
this.legalAgent.evaluate(email)
]);
}

Custom Routing Logic

Implement domain-specific routing rules. This can help for custom tasks for special cases.

private async makeRoutingDecision(email: Email, decisions: any[]): Promise<TriageResult> {
// Custom business logic
if (this.isVIPCustomer(email.from)) {
return this.routeToVIPSupport(email);
}
if (this.isHighValueProspect(email)) {
return this.routeToSeniorSales(email);
}
// Standard routing logic…
return this.standardRouting(decisions);
}

private isVIPCustomer(emailAddress: string): boolean {
const vipDomains = ['bigclient.com', 'enterprise.com'];
return vipDomains.some(domain => emailAddress.includes(domain));
}

private isHighValueProspect(email: Email): boolean {
const indicators = ['enterprise', 'bulk', '1000+', 'enterprise pricing'];
return indicators.some(indicator =>
email.subject.toLowerCase().includes(indicator) ||
email.body.toLowerCase().includes(indicator)
);
}

Integration with External Systems

Connect to your existing tools:

async triageEmail(email: Email): Promise<TriageResult> {
const result = await this.supervisor.triageEmail(email);
// Create ticket in helpdesk system
if (result.assignedAgent === 'CustomerSupport') {
await this.createSupportTicket(email, result);
}
// Add lead to CRM
if (result.assignedAgent === 'Sales') {
await this.createCRMLead(email, result);
}
// Notify HR system
if (result.assignedAgent === 'HR') {
await this.notifyHRSystem(email, result);
}
return result;
}

private async createSupportTicket(email: Email, result: TriageResult): Promise<void> {
// Integration with helpdesk API
const ticket = {
subject: email.subject,
description: email.body,
priority: result.priority,
customerEmail: email.from,
category: result.category
};
// await helpdeskAPI.createTicket(ticket);
console.log('Support ticket created:', ticket);
}

private async createCRMLead(email: Email, result: TriageResult): Promise<void> {
// Integration with CRM API
const lead = {
email: email.from,
source: 'email',
priority: result.priority,
notes: result.summary,
suggestedActions: result.suggestedActions
};
// await crmAPI.createLead(lead);
console.log('CRM lead created:', lead);
}

Other Use Cases for Multi-Agent Systems

Multi-agent architectures excel in many domains beyond email triage:

Content Moderation

  • Toxicity Agent: Detects harmful language and harassment
  • Spam Agent: Identifies promotional and irrelevant content
  • Policy Agent: Checks against community guidelines
  • Context Agent: Analyzes cultural and situational appropriateness

Financial Analysis

  • Risk Agent: Assesses investment and credit risks
  • Fraud Agent: Detects suspicious transactions and patterns
  • Compliance Agent: Ensures regulatory adherence
  • Market Agent: Analyzes trends and opportunities

Healthcare Triage

  • Urgency Agent: Determines medical priority levels
  • Specialty Agent: Routes to appropriate medical departments
  • Symptom Agent: Analyzes patient-reported symptoms
  • Protocol Agent: Suggests care pathways and treatments

Legal Document Review

  • Contract Agent: Analyzes agreements and terms
  • Risk Agent: Identifies legal liabilities and concerns
  • Compliance Agent: Checks regulatory requirements
  • Classification Agent: Categorizes by practice area and urgency

Cost Optimization Strategies

Intelligent Caching

Cache similar email patterns to avoid redundant API calls for emails with identical or near-identical content. This approach can reduce processing costs by up to 40% for organizations that receive many templated or automated emails.

Tiered Model Usage

Use cheaper models for initial filtering and classification, then escalate only complex cases to premium models like GPT-4. This strategy maintains high accuracy for edge cases while keeping routine processing costs minimal.

Batch Processing

Process multiple emails together when possible to take advantage of bulk pricing and reduce per-request overhead. Batching also enables more efficient context sharing between related emails in the same conversation thread.

Future Enhancements

Machine Learning Integration

Train custom models on your email data to improve accuracy and reduce reliance on expensive general-purpose LLMs. Fine-tuned models can achieve better performance on domain-specific terminology and organizational patterns.

Workflow Automation

Trigger automated actions based on triage results, such as forwarding urgent emails, creating calendar events, or updating CRM systems. This creates a fully autonomous email management system that requires minimal human intervention.

Continuous Learning

Implement feedback loops that capture user corrections and preferences to continuously improve the system’s accuracy. Over time, this creates a personalized email assistant that adapts to individual work styles and priorities.

Conclusion

Multi-agent systems provide a powerful framework for building sophisticated AI applications. By breaking complex problems into specialized components, we achieve better accuracy, maintainability, and scalability than single-agent approaches.

The email triage system demonstrates key multi-agent concepts:

  • Specialist agents with focused expertise
  • Supervisor coordination for decision-making
  • Parallel processing for efficiency
  • Conflict resolution when multiple agents compete

Key takeaways from building this system:

  1. Specialization beats generalization — focused agents consistently outperform general-purpose ones
  2. Parallel evaluation dramatically improves response times
  3. Robust error handling ensures system reliability
  4. Continuous learning keeps the system improving over time
  5. Cost optimization through caching and tiered models makes production deployment viable

This architecture scales well to other domains and can be extended with additional agents, custom routing logic, and external integrations. Whether you’re building systems for content moderation, financial analysis, healthcare triage, or legal document review, the multi-agent approach provides a solid foundation for complex AI applications.

The repository of this guide can be found here on GitHub.

From Email Triage to Incident Analysis: The Power of Specialized AI

While building custom multi-agent systems like this email triage example provides valuable learning and flexibility, many organizations struggle with another critical challenge: incident detection and analysis.

Uptime Agent specializes in solving the incident analysis problem with AI. Instead of chasing incidents manually, Uptime Agent automatically catches and analyzes them for you, providing intelligent insights that help teams respond faster and more effectively.

The multi-agent architecture principles we’ve explored in this email triage system — having specialized agents work together to solve complex problems — apply directly to incident management. Just as our email triage system routes communications to the right specialists, effective incident response requires intelligent routing, analysis, and coordination.

Whether you’re building multi-agent systems for email triage, content moderation, or other automation tasks, the core concept remains the same: specialized AI agents working together deliver better results than any single general-purpose solution.

Ready to see how AI can transform your incident response? Discover how Uptime Agent can help you stop chasing incidents and catch them automatically.

*This example demonstrates the fundamental concepts of multi-agent systems using LangChain.js and LangGraph. The complete code is available in the GitHub repository accompanying this blog post.*

--

--

Michael Bauer-Wapp
Michael Bauer-Wapp

Written by Michael Bauer-Wapp

CTO & Co-Founder of AVIMBU 🚀 Building Cutting-Edge Tech, Scalable Products & Software Solutions | Full-Stack Developer 🇦🇹

No responses yet