Build a Microsoft Teams Bot With Bot Framework for Real ChatOps
A practical walkthrough for building a Teams bot with the Bot Framework SDK — handling commands, posting adaptive cards, and adding an AI assist layer safely.
- #microsoft-teams
- #bot-framework
- #chatops
- #automation
- #nodejs
- #azure
For years my team ran ChatOps through a pile of incoming webhooks. It worked one direction — services shouted into Teams — but nobody could talk back. You couldn’t type deploy status web-api and get an answer. For two-way ChatOps you need a real bot, and on Teams that means the Bot Framework.
Here’s how I build one without it turning into a six-month platform project.
What a Teams bot actually is
A Teams bot is a web service that Microsoft’s Bot Framework forwards messages to. When a user @-mentions your bot or sends it a message, the Teams channel relays that to the Azure Bot Service, which posts a JSON activity to your bot’s HTTPS endpoint. Your code reads the activity, does something, and replies.
The moving parts:
- An Azure Bot resource — registers your bot and holds its app ID and password.
- A messaging endpoint — your HTTPS URL where activities arrive (
/api/messages). - The Bot Framework SDK — handles auth, activity parsing, and reply plumbing.
- A Teams app manifest — the package you upload so the bot appears in Teams.
Minimal bot in Node
The SDK does the heavy lifting. A working command bot is small:
const { TeamsActivityHandler, MessageFactory } = require("botbuilder");
class OpsBot extends TeamsActivityHandler {
constructor() {
super();
this.onMessage(async (context, next) => {
const text = context.activity.text.trim().toLowerCase();
if (text.includes("deploy status")) {
const service = text.split("deploy status")[1].trim();
const status = await getDeployStatus(service);
await context.sendActivity(MessageFactory.text(`${service}: ${status}`));
} else {
await context.sendActivity("Try: deploy status <service>");
}
await next();
});
}
}
Wire it to an Express endpoint with the CloudAdapter, and the SDK validates the inbound JWT from Bot Framework for you. That auth handling is the main reason to use the SDK rather than hand-rolling the webhook.
Replying with adaptive cards
Text replies are fine for one-liners, but for anything structured, reply with a card. The SDK wraps card JSON in an attachment:
const card = CardFactory.adaptiveCard(deployStatusCard(service, status));
await context.sendActivity({ attachments: [card] });
This is where a bot beats a webhook: the card can carry Action.Execute buttons that call back into your bot. A “Rollback” button on a deploy card posts an action to your endpoint, and you handle it in onAdaptiveCardInvoke. Now you have interactive ChatOps, not just notifications.
Adding an AI assist layer — carefully
The natural temptation is to let the bot understand free-form requests: “is checkout healthy?” instead of a rigid command. AI makes that possible, but I put hard rails on it.
My rule, the same one I use for incident triage: AI interprets, humans authorize anything that changes state. The bot can use a model to map a fuzzy message to a known, whitelisted command — but read-only commands run automatically while state-changing ones require a confirmation card.
const intent = await classifyIntent(context.activity.text); // returns {command, args, mutates}
if (intent.mutates) {
await context.sendActivity(confirmationCard(intent)); // user must click Confirm
} else {
await runReadOnly(intent);
}
Never let the model emit a raw shell command that you then execute. It maps natural language to a fixed set of safe operations. That distinction is the whole safety model.
Packaging the manifest
Teams needs an app package: a manifest.json plus two icons, zipped. The bot section is what matters:
{
"bots": [
{
"botId": "YOUR-APP-ID",
"scopes": ["team", "personal"],
"commandLists": [
{
"scopes": ["team"],
"commands": [
{ "title": "deploy status", "description": "Get deploy status for a service" }
]
}
]
}
]
}
The commandLists populate the bot’s command menu so users discover what it can do. Sideload the zip via Teams admin or “Upload a custom app” while developing.
Local development without redeploying
Redeploying to Azure for every code change is miserable. Use the Bot Framework Emulator plus a tunnel (dev tunnels or ngrok) to point the Azure Bot’s messaging endpoint at your laptop. You edit, the bot reloads, and you test in real Teams against local code. This single setup change is the difference between a fun build and a painful one.
Operational gotchas I’ve hit
- Proactive messages need a stored conversation reference. To have the bot post unprompted (e.g., an alert), capture the
conversationReferenceon first contact and persist it. You can’t message a channel you’ve never seen. - Throttling is real. Bot Framework rate-limits replies. Batch and back off, especially during incident storms.
- Token rotation. The bot’s password expires. Put a calendar reminder on it or you’ll get a confusing 401 in six months.
Where to start
Don’t build the everything-bot. Build one command that answers one question your team asks ten times a day — deploy status, who’s on call, last incident — and ship it. A bot that reliably does one useful thing earns the right to grow. Layer in cards, then a constrained AI interpreter, then proactive alerts. The prompt library has intent-classification prompts that make the AI layer safer to add.
Bot-driven automation can change real systems. Gate every state-changing action behind explicit human confirmation and a whitelist of allowed operations.
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.