Welcome Back to the M365 Agents Journey!
In Getting Started With M365 Agents SDK, we explored what M365 Agents SDK is and why you should care about it. You learned that it lets you build agents that work everywhere—Teams, Copilot, web, email, and more.
But here’s the thing: knowing what a tool does is only half the story.
Today, we’re pulling back the curtain to show you how it actually works. We’re diving deep into the magic that happens behind the scenes every time a user sends a message to your agent.
By the end of this post, you’ll understand:
- 🔄 The Activity Protocol – The universal language of conversation
- ⚡ Turns – How conversation flows work
- 🎯 TurnContext – Your Swiss Army knife for handling messages
- 📨 Activity Types – Different kinds of interactions
- 💻 Hands-on code – Build a working agent that demonstrates everything
Ready to see the magic? Let’s go! ✨
The Universal Language Problem (And How Activity Protocol Solves It)
Imagine you’re building an agent that needs to work in three places: Microsoft Teams, your company website, and email.
Here’s what the nightmare scenario looks like without a standard protocol:
// Teams sends messages like this:
{
"from": { "id": "user-teams-123", "name": "John" },
"text": "Hello bot!",
"channelData": { "team": "..." }
}
// Your website sends messages like this:
{
"userId": "web-user-456",
"userName": "John",
"message": "Hello bot!",
"sessionId": "..."
}
// Email sends messages like this:
{
"sender": "john@company.com",
"subject": "Bot inquiry",
"body": "Hello bot!",
"thread": "..."
}
Three different formats. You’d need three different parsers, three different handlers, and three times the headaches every time you want to add a feature.
What is the Activity Protocol?
The Activity Protocol is a standardized JSON format that represents any interaction in a conversation—whether it’s a message, a user joining, a button click, or a custom event.
Think of it as the “universal translator” from Star Trek, but for chatbots.
🌍 Real-World Analogy
Activity Protocol is like international shipping labels.
Whether you’re sending a package from Tokyo, London, or New York, the label format is standardized: sender address, recipient address, contents, tracking number.
Same idea here: whether the message comes from Teams, web, or email, it’s translated into a standard Activity format your agent understands.
How It Works: The Complete Flow

Here’s what happens when a user sends “What’s the weather?” in Teams:
Step 1: User Action
User types “What’s the weather?” in Teams and hits send.
Step 2: Channel Creates Activity
Teams converts that into an Activity:
{
"type": "message",
"id": "msg-abc-123",
"timestamp": "2026-01-06T10:30:00Z",
"channelId": "msteams",
"from": {
"id": "29:user-aad-id-here",
"name": "John Doe",
"aadObjectId": "user-aad-id"
},
"conversation": {
"id": "19:meeting-id-here",
"conversationType": "channel"
},
"text": "What's the weather?",
"locale": "en-US"
}
Step 3: Activity Sent to Your Agent
This Activity is posted to your agent’s endpoint: POST /api/messages
Step 4: Agent Processes
Your Express server receives it, M365 Agents SDK parses it, and your handler code runs.
Step 5: Agent Responds
You create a response Activity:
{
"type": "message",
"text": "It's sunny and 72°F!"
}
Step 6: Response Flows Back
The SDK sends this back through the same channel, and Teams renders it to the user.
The Beautiful Part: The exact same flow works for web chat, email, SMS, or any other channel. Your code stays the same!
Understanding Activity Types
Not all interactions are simple text messages. Users join conversations, click buttons, and trigger events. The Activity Protocol handles all of these with different activity types.
The Four Main Activity Types

1. Message Activities (Most Common)
These are your bread and butter—text messages between users and your agent.
// Example message activity
{
type: 'message',
text: 'Hello bot!',
from: { id: 'user-123', name: 'John' },
conversation: { id: 'conv-456' }
}
When you’ll see these:
- User sends a text message
- Agent replies with text
- Messages with attachments (images, files, cards)
Handling message activities:
agent.onActivity(ActivityTypes.Message, async (context) => {
const userText = context.activity.text;
console.log(`User said: ${userText}`);
await context.sendActivity(`You said: ${userText}`);
});
2. ConversationUpdate Activities
These fire when someone joins or leaves a conversation.
// User joins conversation
{
type: 'conversationUpdate',
membersAdded: [
{ id: 'user-789', name: 'Jane Doe' }
],
conversation: { id: 'conv-456' }
}
Perfect for:
- Welcome messages when users start chatting
- Onboarding flows
- Cleanup when users leave
agent.onActivity(ActivityTypes.ConversationUpdate, async (context) => {
const membersAdded = context.activity.membersAdded || [];
for (const member of membersAdded) {
if (member.id !== context.activity.recipient.id) {
// Don't welcome the bot itself!
await context.sendActivity(
`Welcome ${member.name}! 👋 Type /help to get started.`
);
}
}
});
3. Event Activities
Custom events for special scenarios—background notifications, scheduled triggers, or integration events.
// Custom event example
{
type: 'event',
name: 'reportReady',
value: {
reportId: 'report-123',
downloadUrl: 'https://...'
}
}
Use cases:
- Proactive notifications (your agent initiates)
- System events (approval completed, task assigned)
- Integration triggers (CRM updated, order shipped)
4. Invoke Activities (Card Actions)
When users interact with Adaptive Cards—clicking buttons, submitting forms, or selecting dropdown options.
// Button click on an Adaptive Card
{
type: 'invoke',
name: 'adaptiveCard/action',
value: {
action: 'approve',
requestId: 'req-456',
comments: 'Looks good!'
}
}
Common scenarios:
- Approval buttons (Approve/Reject)
- Form submissions
- Interactive polls or surveys
Turns: The Heartbeat of Conversation
Now that you understand Activities, let’s talk about Turns.
What is a Turn?
A Turn is one complete request-response cycle in a conversation. It’s like one exchange in a tennis match—the ball goes from one side to the other and back.
✅ Simple Definition
Turn = User sends something → Agent processes → Agent responds
That’s it! One turn. Once your agent sends a response, the turn is complete.

The Seven Steps of Every Turn
Let’s break down what happens in every single turn:
Step 1: Activity Received
An HTTP POST arrives at your /api/messages endpoint with an Activity JSON.
// Express receives this
POST /api/messages
Content-Type: application/json
{
"type": "message",
"text": "Hello bot!",
...
}
Step 2: Authentication
The SDK verifies the JWT token from Azure Bot Service to ensure the request is legitimate.
Step 3: Create TurnContext
The SDK wraps everything into a TurnContext object—your handle for this turn.
Step 4: Route to Handler
Based on the activity type and your routing rules, the appropriate handler is called.
// Your handler gets called
agent.onActivity(ActivityTypes.Message, async (context) => {
// You're here! Processing this turn.
});
Step 5: Execute Handler (Your Code!)
This is where your business logic runs—call AI, query databases, whatever you need.
async (context) => {
// Your code runs here
const userMessage = context.activity.text;
// Call your AI
const response = await openai.chat.completions.create({
messages: [{ role: 'user', content: userMessage }]
});
// Send response
await context.sendActivity(response.choices[0].message.content);
}
Step 6: Send Response
Your response Activity goes back through the channel to the user.
Step 7: Cleanup
The TurnContext is disposed, state is saved, and the turn is complete. System is ready for the next turn.
⚠️ Important: Turns are Independent
Each turn is separate! If you want information to persist across turns (like remembering the user’s name), you need state management—which we’ll cover in Part 3.
TurnContext: Your Swiss Army Knife
The TurnContext object is your interface to everything in the current turn. Let me show you all the powerful things you can do with it.

What’s Inside TurnContext?
Every time your handler runs, you get a context parameter. Here’s what’s available:
agent.onActivity(ActivityTypes.Message, async (context) => {
// 1. Access the incoming activity
const userMessage = context.activity.text;
const userId = context.activity.from.id;
const userName = context.activity.from.name;
const channelId = context.activity.channelId;
const conversationId = context.activity.conversation.id;
// 2. Send responses (multiple ways)
await context.sendActivity('Simple text');
await context.sendActivity({
type: 'message',
text: 'Formatted message'
});
// 3. Send typing indicator
await context.sendActivity({ type: 'typing' });
// 4. Send multiple activities
await context.sendActivities([
{ type: 'typing' },
{ type: 'message', text: 'First message' },
{ type: 'message', text: 'Second message' }
]);
// 5. Update a previously sent message
const response = await context.sendActivity('Processing...');
// ...do work...
await context.updateActivity({
id: response.id,
type: 'message',
text: 'Done!'
});
// 6. Delete a message
await context.deleteActivity(response.id);
// 7. Check which channel you're in
if (channelId === 'msteams') {
// Teams-specific logic
}
// 8. Access state (we'll cover this in Part 3)
const state = await context.storage.read(['conversationState']);
});
Real-World Example: Intelligent Greeting
Let’s use TurnContext to build something practical—a smart greeting that knows who the user is and what channel they’re using:
agent.onActivity(ActivityTypes.Message, async (context) => {
const message = context.activity.text?.toLowerCase() || '';
if (message.includes('hello') || message.includes('hi')) {
const userName = context.activity.from.name;
const channel = context.activity.channelId;
let greeting = `Hello ${userName}! 👋`;
// Customize based on channel
if (channel === 'msteams') {
greeting += ` Welcome to our Teams workspace!`;
} else if (channel === 'webchat') {
greeting += ` Thanks for visiting our website!`;
} else if (channel === 'email') {
greeting += ` I've received your email.`;
}
greeting += ` How can I help you today?`;
await context.sendActivity(greeting);
}
});
Hands-On: Build a Working Agent
Theory is great, but let’s build something real. Here’s a complete agent that demonstrates everything we’ve learned:
The Interactive Command Agent
import { AgentApplication, MemoryStorage } from '@microsoft/agents-hosting';
import { ActivityTypes } from '@microsoft/agents-activity';
// Create agent with memory storage
const agent = new AgentApplication({
storage: new MemoryStorage()
});
// 1. Handle new users joining (ConversationUpdate)
agent.onActivity(ActivityTypes.ConversationUpdate, async (context) => {
const membersAdded = context.activity.membersAdded || [];
for (const member of membersAdded) {
if (member.id !== context.activity.recipient.id) {
await context.sendActivity(
`🎉 Welcome ${member.name}!\n\n` +
`I'm your demo agent. Try these commands:\n` +
`• /help - Show all commands\n` +
`• /info - Show your info\n` +
`• /channel - Show which channel you're using\n` +
`• Or just chat with me!`
);
}
}
});
// 2. Handle /help command
agent.onMessage('/help', async (context) => {
await context.sendActivity(
`📚 **Available Commands:**\n\n` +
`• **/help** - Show this help message\n` +
`• **/info** - Display your user information\n` +
`• **/channel** - Show which channel you're using\n` +
`• **/activity** - Show details about the last activity\n` +
`• **/echo [message]** - Echo back your message\n\n` +
`Send me any message and I'll respond!`
);
});
// 3. Handle /info command
agent.onMessage('/info', async (context) => {
const user = context.activity.from;
const conversation = context.activity.conversation;
await context.sendActivity(
`👤 **Your Information:**\n\n` +
`• **Name:** ${user.name}\n` +
`• **ID:** ${user.id}\n` +
`• **Conversation:** ${conversation.id}\n` +
`• **Locale:** ${context.activity.locale || 'Not specified'}`
);
});
// 4. Handle /channel command
agent.onMessage('/channel', async (context) => {
const channelId = context.activity.channelId;
let channelName;
let channelIcon;
switch (channelId) {
case 'msteams':
channelName = 'Microsoft Teams';
channelIcon = '💬';
break;
case 'webchat':
channelName = 'Web Chat';
channelIcon = '🌐';
break;
case 'email':
channelName = 'Email';
channelIcon = '📧';
break;
case 'sms':
channelName = 'SMS';
channelIcon = '📱';
break;
default:
channelName = channelId;
channelIcon = '❓';
}
await context.sendActivity(
`${channelIcon} **Channel Information:**\n\n` +
`You're currently using **${channelName}**!\n` +
`Channel ID: \`${channelId}\``
);
});
// 5. Handle /activity command
agent.onMessage('/activity', async (context) => {
const activity = context.activity;
await context.sendActivity(
`📊 **Last Activity Details:**\n\n` +
`\`\`\`json\n${JSON.stringify({
type: activity.type,
id: activity.id,
timestamp: activity.timestamp,
channelId: activity.channelId,
text: activity.text
}, null, 2)}\n\`\`\``
);
});
// 6. Handle /echo command
agent.onMessage(/^\/echo\s+(.+)/i, async (context) => {
const match = context.activity.text.match(/^\/echo\s+(.+)/i);
const messageToEcho = match[1];
await context.sendActivity(`🔊 Echo: ${messageToEcho}`);
});
// 7. Catch-all for any other message
agent.onActivity(ActivityTypes.Message, async (context) => {
const message = context.activity.text;
await context.sendActivity(
`You said: "${message}"\n\n` +
`Type **/help** to see what I can do!`
);
});
export default agent;
What This Agent Demonstrates
This agent showcases everything we’ve learned:
- ✅ Multiple activity types – ConversationUpdate and Message
- ✅ Command routing – Different handlers for different commands
- ✅ TurnContext usage – Accessing user info, channel info, activity details
- ✅ Pattern matching – Regex for the /echo command
- ✅ Channel detection – Different responses based on channel
- ✅ Catch-all handler – Fallback for unmatched messages
Try It Yourself!
🎯 Challenge: Extend This Agent
Try adding these features:
1. A /time command that shows the current time
2. A /joke command that tells random jokes
3. A /uppercase command that converts text to uppercase
All the patterns you need are in the code above!
Common Patterns You’ll Use Every Day
Pattern 1: Greeting New Users
agent.onActivity(ActivityTypes.ConversationUpdate, async (context) => {
const membersAdded = context.activity.membersAdded || [];
for (const member of membersAdded) {
if (member.id !== context.activity.recipient.id) {
await context.sendActivity(`Welcome ${member.name}! 👋`);
}
}
});
Pattern 2: Command with Parameters
// Match: /search azure agents
agent.onMessage(/^\/search\s+(.+)/i, async (context) => {
const searchQuery = context.activity.text.match(/^\/search\s+(.+)/i)[1];
const results = await searchDatabase(searchQuery);
await context.sendActivity(`Found ${results.length} results for: ${searchQuery}`);
});
Pattern 3: Typing Indicator
agent.onMessage('/analyze', async (context) => {
// Show typing while processing
await context.sendActivity({ type: 'typing' });
// Do expensive operation
const analysis = await performAnalysis();
// Send result
await context.sendActivity(analysis);
});
Pattern 4: Channel-Specific Responses
agent.onActivity(ActivityTypes.Message, async (context) => {
if (context.activity.channelId === 'msteams') {
// Send rich Adaptive Card in Teams
await context.sendActivity({
attachments: [createAdaptiveCard()]
});
} else {
// Send plain text elsewhere
await context.sendActivity('Simple text response');
}
});
Debugging Tips: When Things Go Wrong
1. Log Everything
agent.onActivity(ActivityTypes.Message, async (context) => {
console.log('=== Turn Started ===');
console.log('Activity Type:', context.activity.type);
console.log('User Message:', context.activity.text);
console.log('User ID:', context.activity.from.id);
console.log('Channel:', context.activity.channelId);
// Your logic here
console.log('=== Turn Complete ===');
});
2. Pretty-Print Activities
agent.onActivity(ActivityTypes.Message, async (context) => {
// Send the activity structure back to yourself for debugging
await context.sendActivity(
`\`\`\`json\n${JSON.stringify(context.activity, null, 2)}\n\`\`\``
);
});
3. Error Handling
agent.onActivity(ActivityTypes.Message, async (context) => {
try {
// Your code here
const result = await riskyOperation();
await context.sendActivity(result);
} catch (error) {
console.error('Error in turn:', error);
await context.sendActivity(
`❌ Sorry, something went wrong: ${error.message}`
);
}
});
What’s Next: State Management
You now understand how messages flow through your agent using Activities and Turns. But there’s one critical piece missing:
How does your agent remember things?
Right now, every turn is independent. If a user says “My name is John” and then asks “What’s my name?” in the next message, your agent has no memory of the first turn.
That’s where State Management comes in—and it’s what we’ll cover in Part 3!
🎓 Coming in Part 3: State Management & The Agent Container
You’ll learn:
• How to make your agent remember information across turns
• Conversation State vs User State vs Turn State
• Storage options (Memory, Blob, Cosmos DB)
• The Agent Container architecture
• Building a stateful shopping cart agent
Key Takeaways
Let’s recap what you learned today:
- 🔄 Activity Protocol is the universal format for all messages—solves the multi-channel problem
- 📨 Activities represent every interaction: Message, ConversationUpdate, Event, Invoke
- ⚡ Turns are complete request-response cycles—the heartbeat of conversation
- 🎯 TurnContext gives you access to everything: activity, user info, send methods, state
- 💻 7 Steps happen in every turn: receive → authenticate → context → route → execute → respond → cleanup
- 🔧 Patterns like greetings, commands, typing indicators are easy to implement
- ⚠️ Turns are independent —need state management for memory (coming in Part 3!)
Practice Exercise
Before moving on to Part 3, try building this agent:
🎯 Build a “Magic 8-Ball” Agent
Requirements:
1. Welcome new users with instructions
2. Respond to questions with random predictions
3. Show different messages based on channel
4. Add a /help command
Sample responses:
“Yes, definitely!” | “Ask again later” | “Don’t count on it”
Use the patterns from this post. You’ve got everything you need!
Join the Conversation
How did you do with the hands-on code? Did you extend the agent? Drop a comment below:
- What commands did you add?
- What’s your biggest “aha!” moment from this post?
- Any questions about Activities, Turns, or TurnContext?
I read and respond to every comment! 💬
Next in Series: Part 3: State Management & The Agent Container – Making Your Agent Remember (Coming Next Week)
Previous in Series: Part 1: Getting Started with Microsoft 365 Agents SDK
Happy Sharing…