The Multi-Channel Dream
Imagine you’ve built the perfect AI agent. It’s smart, helpful, and your users love it.
But there’s a problem: your users are everywhere.
- Some prefer Microsoft Teams π¬
- Others use M365 Copilot π€
- Many want it on your website π
- A few still use email π§
- Some need SMS notifications π±
Building separate agents for each channel? That’s a nightmare. Five codebases. Five sets of bugs. Five times the maintenance.
There has to be a better way.
There is! And that’s what we’re covering today.
By the end of this post, you’ll understand:
- ποΈΒ Multi-Channel ArchitectureΒ – How one agent serves all channels
- πΒ Channel AdaptersΒ – The magic translation layer
- βοΈΒ Azure Bot ServiceΒ – Your channel registration hub
- πΒ Deploy to TeamsΒ – Step-by-step walkthrough
- πΒ Adding More ChannelsΒ – Web, Copilot, email, SMS
- π»Β Hands-On ProjectΒ – Build and deploy a production-ready agent
Ready to make your agent available everywhere? Let’s go! π
The Write Once, Run Everywhere Philosophy
The core promise of M365 Agents SDK is simple but powerful:
β¨ Write your agent code ONCE.
Deploy it to MULTIPLE channels.
Zero code changes.
Here’s what that looks like in practice:
// This EXACT code works in Teams, Copilot, Web, Email, SMSagent.onMessage('/help', async (context, state) => { await context.sendActivity( 'π€ I can help you with:\n' + 'β’ Weather forecasts\n' + 'β’ Calendar management\n' + 'β’ Quick calculations' );});
Same handler. Same logic. Different channels.

How Does This Actually Work?
The magic happens through channel adaptersβtranslation layers that convert channel-specific formats into the universal Activity Protocol (remember from Part 2?).
The Flow:
User in Teams β Teams Adapter β Activity Protocol β Your AgentUser on Web β Web Adapter β Activity Protocol β Your Agent User via Email β Email Adapter β Activity Protocol β Your AgentAll three users see the same intelligent responses!
The Multi-Channel Architecture
Let’s understand the complete deployment architecture:

The Five Layers
Layer 1: Users (Where They Are)
- Microsoft Teams channels, chats, meetings
- M365 Copilot in Word, Excel, Outlook
- Your company website
- Email clients (Outlook, Gmail)
- SMS on mobile devices
Layer 2: Channels (Platform-Specific)
- Each channel has its own format and features
- Teams uses channel-specific JSON
- Web uses WebSocket or HTTP
- Email uses SMTP/IMAP protocols
Layer 3: Azure Bot Service (The Hub)
- Central registration point for all channels
- Routes messages from channels to your agent
- Handles authentication and security
- Provides channel adapters automatically
Layer 4: Your Express Server (HTTP Endpoint)
- Receives HTTP POST requests atΒ
/api/messages - Activity Protocol format (standardized JSON)
- Your agent’s “front door”
Layer 5: Agent Application (Your Code)
- M365 Agents SDK handles routing
- Your handlers process messages
- State management, AI calls, business logic
- Channel-agnostic code
Message Flow Example
Let’s trace what happens when a user in Teams sends “What’s the weather?”:
Step 1: User types in Teams β Teams client sends to Teams service
Step 2: Teams service β Azure Bot Service (registered channel)
Step 3: Azure Bot Service β Converts to Activity Protocol
Step 4: HTTP POST to your server β https://youragent.com/api/messages
Step 5: Express receives β M365 SDK parses Activity
Step 6: Your handler runs β Calls weather API β Creates response
Step 7: Response flows back β Azure Bot Service β Teams β User
Total time: Usually 200-500ms for the entire round trip!
Azure Bot Service: Your Channel Hub
Azure Bot Service is Microsoft’s managed service that handles the hard parts of multi-channel deployment:
What It Provides
- β Β Channel RegistrationΒ – One-click enable for Teams, Web, Email, etc.
- β Β AuthenticationΒ – OAuth, JWT token validation
- β Β Message RoutingΒ – Reliable delivery to your agent
- β Β ScalingΒ – Handle millions of users automatically
- β Β MonitoringΒ – Built-in analytics and logs
- β Β SecurityΒ – DDoS protection, encryption
Pricing
π° Cost: Azure Bot Service has a FREE tier with unlimited messages for standard channels!
Premium channels (like Teams) are included. You only pay for your hosting (Azure App Service, Container, etc.).
Hands-On: Deploy Your First Multi-Channel Agent
Let’s build and deploy a production-ready agent to Microsoft Teams, then add more channels.
Prerequisites
- β Azure account (free tier works!)
- β Node.js 18+ installed
- β Azure CLI installed
- β VS Code (optional but recommended)
Step 1: Create Your Agent
Let’s build a simple but complete multi-channel agent:
// index.mjsimport { AgentApplication, MemoryStorage } from '@microsoft/agents-hosting';import { createAgentExpressServer } from '@microsoft/agents-hosting-express';import { ActivityTypes } from '@microsoft/agents-activity';// Create agentconst agent = new AgentApplication({ storage: new MemoryStorage()});// Welcome message (works in ALL channels)agent.onActivity(ActivityTypes.ConversationUpdate, async (context, state) => { const membersAdded = context.activity.membersAdded || []; for (const member of membersAdded) { if (member.id !== context.activity.recipient.id) { const channelId = context.activity.channelId; let channelName = channelId; // Detect which channel the user is on if (channelId === 'msteams') channelName = 'Microsoft Teams'; else if (channelId === 'webchat') channelName = 'Web Chat'; else if (channelId === 'email') channelName = 'Email'; else if (channelId === 'sms') channelName = 'SMS'; await context.sendActivity( `π Welcome to Multi-Channel Agent!\n\n` + `You're using: **${channelName}**\n\n` + `Try these commands:\n` + `β’ /help - Show all commands\n` + `β’ /channel - Detect your channel\n` + `β’ /echo [text] - Echo your message` ); } }});// Help command (universal)agent.onMessage('/help', async (context, state) => { await context.sendActivity( `π€ **Available Commands:**\n\n` + `β’ **/help** - Show this help\n` + `β’ **/channel** - Show which channel you're using\n` + `β’ **/echo [text]** - Echo your message\n` + `β’ **/features** - Show channel-specific features\n\n` + `This agent works in Teams, Copilot, Web, Email, and SMS!` );});// Channel detectionagent.onMessage('/channel', async (context, state) => { const channelId = context.activity.channelId; const userId = context.activity.from.id; const conversationId = context.activity.conversation.id; let response = `π± **Channel Information:**\n\n`; response += `β’ **Channel:** ${channelId}\n`; response += `β’ **User ID:** ${userId}\n`; response += `β’ **Conversation ID:** ${conversationId}\n\n`; // Channel-specific messages if (channelId === 'msteams') { response += `You're in Microsoft Teams! π¬\n`; response += `This agent supports rich cards, files, and mentions here.`; } else if (channelId === 'webchat') { response += `You're on the web! π\n`; response += `Enjoy fast responses and rich interactions.`; } else if (channelId === 'email') { response += `You're using email! π§\n`; response += `Responses will appear in your inbox.`; } else { response += `Channel detected: ${channelId}`; } await context.sendActivity(response);});// Echo commandagent.onMessage(/^\/echo\s+(.+)/i, async (context, state) => { const text = context.activity.text.match(/^\/echo\s+(.+)/i)[1]; await context.sendActivity(`π You said: "${text}"`);});// Feature detection (channel-specific)agent.onMessage('/features', async (context, state) => { const channelId = context.activity.channelId; let features = `β¨ **Features Available in ${channelId}:**\n\n`; // Different features per channel const channelFeatures = { msteams: [ 'β
Text messages', 'β
Adaptive Cards', 'β
File attachments', 'β
Mentions (@user)', 'β
Rich formatting', 'β
Proactive notifications' ], webchat: [ 'β
Text messages', 'β
Adaptive Cards', 'β
Rich formatting', 'β
Fast responses', 'β οΈ Limited file support' ], email: [ 'β
Text messages', 'β
HTML formatting', 'β οΈ No real-time responses', 'β No Adaptive Cards' ], sms: [ 'β
Text messages (160 chars)', 'β οΈ Plain text only', 'β No rich formatting', 'β No cards or media' ] }; const featureList = channelFeatures[channelId] || ['β
Basic text messages']; featureList.forEach(f => features += `${f}\n`); await context.sendActivity(features);});// Catch-allagent.onActivity(ActivityTypes.Message, async (context) => { await context.sendActivity( `I received your message: "${context.activity.text}"\n\n` + `Type **/help** for available commands.` );});// Create Express serverconst server = createAgentExpressServer(agent, { port: process.env.PORT || 3978, appId: process.env.MICROSOFT_APP_ID, appPassword: process.env.MICROSOFT_APP_PASSWORD});console.log(`π Agent listening on port ${server.address().port}`);console.log(`π‘ Ready for multi-channel connections!`);
Step 2: Configure Environment
Create a .env file:
# .envPORT=3978MICROSOFT_APP_ID=your-app-id-hereMICROSOFT_APP_PASSWORD=your-app-password-here
Don’t worryβwe’ll get these values from Azure in the next step!
Step 3: Create Azure Bot Service
Option 1: Azure Portal (Visual)
- Go toΒ portal.azure.com
- ClickΒ “Create a resource”Β β SearchΒ “Azure Bot”
- Fill in details:
- Bot handle:Β my-multi-channel-agent (unique name)
- Subscription:Β Your subscription
- Resource group:Β Create new “agents-rg”
- Pricing tier:Β F0 (Free!)
- Microsoft App ID:Β Create new
- ClickΒ “Review + Create”
- Copy theΒ App IDΒ and generateΒ App Password
- Save these in yourΒ
.envΒ file
Option 2: Azure CLI (Faster)
# Login to Azureaz login# Create resource groupaz group create --name agents-rg --location eastus# Create Azure Botaz bot create \ --name my-multi-channel-agent \ --resource-group agents-rg \ --kind registration \ --sku F0 \ --appid $(az ad app create --display-name "Multi-Channel Agent" --query appId -o tsv)# Get App IDaz bot show --name my-multi-channel-agent --resource-group agents-rg --query "properties.msaAppId"# Create App Password (save this immediately!)az ad app credential reset --id YOUR_APP_ID
Step 4: Deploy Your Agent
You have several hosting options:
Option A: Azure App Service
# Install Azure CLI extensionaz extension add --name webapp# Create App Service Planaz appservice plan create \ --name agent-plan \ --resource-group agents-rg \ --sku B1 \ --is-linux# Create Web Appaz webapp create \ --name my-agent-app \ --resource-group agents-rg \ --plan agent-plan \ --runtime "NODE:18-lts"# Configure environment variablesaz webapp config appsettings set \ --name my-agent-app \ --resource-group agents-rg \ --settings \ MICROSOFT_APP_ID="your-app-id" \ MICROSOFT_APP_PASSWORD="your-app-password"# Deploy code (using zip deploy)zip -r agent.zip .az webapp deployment source config-zip \ --name my-agent-app \ --resource-group agents-rg \ --src agent.zip
Option B: Azure Container Instances
# Build and push Docker imagedocker build -t myagent:latest .docker tag myagent:latest myregistry.azurecr.io/myagent:latestdocker push myregistry.azurecr.io/myagent:latest# Create container instanceaz container create \ --name my-agent-container \ --resource-group agents-rg \ --image myregistry.azurecr.io/myagent:latest \ --dns-name-label my-unique-agent \ --ports 3978 \ --environment-variables \ MICROSOFT_APP_ID="your-app-id" \ MICROSOFT_APP_PASSWORD="your-app-password"
Option C: Ngrok for Local Testing
# Install ngrok: https://ngrok.combrew install ngrok # macOS# Start your agent locallynode index.mjs# In another terminal, create tunnelngrok http 3978# Copy the HTTPS URL (e.g., https://abc123.ngrok.io)
Step 5: Configure Messaging Endpoint
Back in Azure Portal:
- Go to your Azure Bot resource
- ClickΒ “Configuration”
- SetΒ Messaging endpointΒ to:
https://your-agent-url.com/api/messages
(Use your App Service URL, Container URL, or ngrok URL) - ClickΒ “Apply”
Step 6: Test in Web Chat
- In Azure Portal β Your Bot β ClickΒ “Test in Web Chat”
- TypeΒ “/help”
- You should see your agent respond!
π Congratulations! Your agent is now deployed and running in Web Chat!
Adding Microsoft Teams Channel
Now let’s make your agent available in Teams:
Step 1: Enable Teams Channel
- Azure Portal β Your Bot βΒ “Channels”
- ClickΒ “Microsoft Teams”Β icon
- ClickΒ “Save”
- That’s it! Teams channel is now enabled.
Step 2: Create Teams App Manifest
Create a manifest.json:
{ "$schema": "https://developer.microsoft.com/json-schemas/teams/v1.16/MicrosoftTeams.schema.json", "manifestVersion": "1.16", "version": "1.0.0", "id": "YOUR_APP_ID", "packageName": "com.mycompany.multiagent", "developer": { "name": "Your Company", "websiteUrl": "https://yourcompany.com", "privacyUrl": "https://yourcompany.com/privacy", "termsOfUseUrl": "https://yourcompany.com/terms" }, "name": { "short": "Multi-Channel Agent", "full": "My Multi-Channel AI Agent" }, "description": { "short": "AI agent that works everywhere", "full": "An intelligent agent built with M365 Agents SDK that works in Teams, web, and more." }, "icons": { "outline": "outline.png", "color": "color.png" }, "accentColor": "#0078D4", "bots": [ { "botId": "YOUR_APP_ID", "scopes": ["personal", "team", "groupchat"], "supportsFiles": false, "isNotificationOnly": false, "commandLists": [ { "scopes": ["personal", "team", "groupchat"], "commands": [ { "title": "help", "description": "Show available commands" }, { "title": "channel", "description": "Show channel information" }, { "title": "echo", "description": "Echo your message" } ] } ] } ], "validDomains": []}
Step 3: Create App Package
- Create two icon files:
color.pngΒ – 192x192px (full color)outline.pngΒ – 32x32px (white outline, transparent bg)
- Zip all three files:
zip -r teams-app.zip manifest.json color.png outline.png
Step 4: Install in Teams
- Open Microsoft Teams
- ClickΒ “Apps”Β in left sidebar
- ClickΒ “Manage your apps”Β βΒ “Upload an app”
- ChooseΒ “Upload a custom app”
- Select yourΒ
teams-app.zip - ClickΒ “Add”
Your agent is now in Teams! Try chatting with it.
Adding More Channels
Web Chat (Already Enabled!)
Web Chat is enabled by default. To embed on your website:
- Azure Portal β Bot βΒ “Channels”Β βΒ “Web Chat”
- Copy theΒ embed code
- Paste into your HTML:
<!-- Add to your website -->
<div id="webchat"></div>
https://cdn.botframework.com/botframework-webchat/latest/webchat.js
<script>
window.WebChat.renderWebChat({
directLine: window.WebChat.createDirectLine({
secret: 'YOUR_WEB_CHAT_SECRET'
}),
userID: 'user-' + Math.random().toString(36).substring(7)
}, document.getElementById('webchat'));
</script>
- Azure Portal β Bot βΒ “Channels”Β βΒ “Email”
- Configure:
- Email address:Β Choose or bring your own
- Password:Β Set for authentication
- ClickΒ “Save”
Users can now email your agent at agent@yourdomain.com!
SMS (via Twilio)
- CreateΒ Twilio account
- Get phone number and API credentials
- Azure Portal β Bot βΒ “Channels”Β βΒ “Twilio SMS”
- Enter Twilio credentials
- Configure webhook URL
M365 Copilot
To make your agent available in Copilot:
- Ensure your agent is in Teams
- Add Copilot extension manifest
- Submit to Microsoft Teams Store (for org or public)
π Note: M365 Copilot integration requires Microsoft 365 subscription and admin approval for deployment.
Channel-Specific Features
While your core logic stays the same, you can enhance the experience for specific channels:

Adaptive Cards (Teams, Web, Copilot)
agent.onMessage('/card', async (context, state) => { if (['msteams', 'webchat'].includes(context.activity.channelId)) { // Rich card for Teams and Web await context.sendActivity({ attachments: [{ contentType: 'application/vnd.microsoft.card.adaptive', content: { type: 'AdaptiveCard', version: '1.4', body: [ { type: 'TextBlock', text: 'Hello from Adaptive Card!', size: 'large', weight: 'bolder' }, { type: 'TextBlock', text: 'This works in Teams and Web Chat.' } ], actions: [ { type: 'Action.Submit', title: 'Click Me', data: { action: 'button_clicked' } } ] } }] }); } else { // Fallback for email/SMS await context.sendActivity('Hello! (Cards not supported in this channel)'); }});
Proactive Messaging (Teams, Web)
// Send message without user initiatingimport { proactiveMessaging } from '@microsoft/agents-hosting';// Save conversation reference when user first connectsagent.onActivity(ActivityTypes.ConversationUpdate, async (context, state) => { state.user.conversationReference = TurnContext.getConversationReference(context.activity);});// Later, send proactive messageasync function sendNotification(userId, message) { const reference = await getUserConversationReference(userId); await proactiveMessaging.continueConversation(reference, async (context) => { await context.sendActivity(message); });}
File Uploads (Teams)
agent.onActivity(ActivityTypes.Message, async (context, state) => { if (context.activity.attachments && context.activity.attachments.length > 0) { const attachment = context.activity.attachments[0]; await context.sendActivity( `π Received file:\n` + `β’ Name: ${attachment.name}\n` + `β’ Type: ${attachment.contentType}\n` + `β’ URL: ${attachment.contentUrl}` ); }});
Production Best Practices
1. Environment-Based Configuration
const config = { development: { storage: new MemoryStorage(), logLevel: 'debug' }, production: { storage: new BlobsStorage(process.env.STORAGE_CONNECTION_STRING, 'agent-state'), logLevel: 'error' }};const env = process.env.NODE_ENV || 'development';const agent = new AgentApplication(config[env]);
2. Health Check Endpoint
app.get('/health', (req, res) => { res.json({ status: 'healthy', timestamp: new Date().toISOString(), uptime: process.uptime() });});
3. Error Handling & Logging
agent.onActivity(ActivityTypes.Message, async (context, state) => { try { // Your handler logic } catch (error) { console.error('Error in message handler:', error); await context.sendActivity( 'β Sorry, something went wrong. Please try again later.' ); // Log to Application Insights, etc. }});
4. Rate Limiting
import rateLimit from 'express-rate-limit';const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100 // limit each user to 100 requests per windowMs});app.use('/api/messages', limiter);
5. Monitoring & Analytics
agent.onActivity(ActivityTypes.Message, async (context, state) => { // Track usage console.log({ userId: context.activity.from.id, channelId: context.activity.channelId, message: context.activity.text, timestamp: new Date() }); // Send to Application Insights, etc.});
Troubleshooting Common Issues
Issue 1: “Unauthorized” Error
Cause: Wrong App ID or Password
Fix: Verify MICROSOFT_APP_ID and MICROSOFT_APP_PASSWORD in .env match Azure Bot
Issue 2: Agent Not Responding
Cause: Messaging endpoint not configured
Fix: Azure Portal β Bot β Configuration β Set messaging endpoint to https://yoururl.com/api/messages
Issue 3: Works in Web Chat, Not Teams
Cause: Teams channel not enabled or app not installed
Fix: Enable Teams channel in Azure, upload Teams app package
Issue 4: SSL/HTTPS Errors
Cause: Azure Bot Service requires HTTPS
Fix: Use ngrok for local testing, or deploy to Azure with HTTPS
What’s Next: AI Integration
You now know how to deploy your agent to multiple channelsβTeams, Copilot, web, email, SMSβusing a single codebase!
But there’s one more critical piece:
How do you make your agent actually intelligent?
That’s where AI comes inβand it’s what we’re covering in Part 5!
π Coming in Part 5: AI Integration – Bringing Your Own AI
You’ll learn:
β’ Integrating Azure OpenAI (GPT-4, GPT-4o)
β’ Using Semantic Kernel for orchestration
β’ LangChain integration patterns
β’ Claude, Gemini, and custom models
β’ Building a production RAG agent
β’ The AI-agnostic philosophy
Key Takeaways
- πΒ Write once, run everywhereΒ – Same code works in all channels
- πΒ Channel adaptersΒ – Convert channel formats to Activity Protocol
- βοΈΒ Azure Bot ServiceΒ – Free tier handles channel registration and routing
- π¬Β Teams deploymentΒ – Enable channel + upload app manifest
- πΒ Multi-channel supportΒ – Teams, Copilot, Web, Email, SMS with zero code changes
- π¨Β Channel-specific featuresΒ – Adaptive Cards, proactive messaging, file uploads
- βοΈΒ Production-readyΒ – Health checks, logging, rate limiting, monitoring
Practice Exercise
π― Deploy a Multi-Region Agent
Challenge:
1. Deploy your agent to Azure in 2 regions (East US, West Europe)
2. Use Azure Traffic Manager for load balancing
3. Add Cosmos DB for global state replication
4. Enable in Teams, Web, and Email
5. Add monitoring with Application Insights
This is production-grade architecture!
Join the Conversation
How did your deployment go? What channels are you enabling?
- Did you deploy to Teams? Web? Both?
- Any challenges with Azure Bot Service setup?
- What’s your production deployment strategy?
Drop a comment belowβI respond to every one! π¬
Happy Sharing…