The Table Everyone Skims Past
Most people read this comparison as a framework preference. It is not. It is a change in control model.
Part 1 introduced the high-level contrast. Before moving on, it is worth putting real code on both sides of it, because the table alone hides where the actual engineering effort shifts.
| Dimension | Bot Framework SDK | Microsoft 365 Agents SDK |
|---|---|---|
| Interaction model | Conversations and dialogs you author explicitly | Intent-driven; the agent plans its own steps |
| Control flow | Manual — you write the branching logic | AI planning — the model decides the path |
| Best fit | Deterministic workflows | Open-ended, multi-step tasks |
| Ecosystem | Azure Bot Service | Microsoft 365 Copilot ecosystem |
Read literally, this looks like a style choice. It is not. Swapping the implementation changes where your bugs live, what your test suite needs to cover, and whether you need a human-in-the-loop gate at all.
(See the accompanying diagram: a fixed decision tree enumerated at compile time, versus a planning loop that decides its sequence at runtime.)
The Bot Framework Version (from Part 2)
Recall the original "status" bot from Part 2: a fixed if/else inside OnMessageActivityAsync that recognizes the literal word "status" and returns a canned reply. Every possible input is enumerated by you at compile time. There is no ambiguity in what the bot will do — which is exactly the point.
protected override async Task OnMessageActivityAsync(
ITurnContext<IMessageActivity> turnContext,
CancellationToken cancellationToken)
{
var text = turnContext.Activity.Text?.Trim();
if (string.Equals(text, "status", StringComparison.OrdinalIgnoreCase))
{
await turnContext.SendActivityAsync(
MessageFactory.Text("All systems operational."), cancellationToken);
return;
}
await turnContext.SendActivityAsync(
MessageFactory.Text($"Received: {text}"), cancellationToken);
}
Now extend the scope: "check deployment status, and if it has failed, roll back the last release." That is no longer one branch. It becomes a sequence of tool calls where step two depends on what step one returns. You could keep nesting if/else statements, but then you would be hand-writing a planner. That is the signal to switch models.
The Same Task, Rebuilt as an Agent
The Microsoft 365 Agents SDK flips the control model. Instead of you writing the branch logic, you register tools and provide a goal, and the model decides the sequence.
var agent = new AgentBuilder()
.WithModel("azure-openai-gpt")
.WithInstructions(
"You help engineers check deployment status and roll back failed releases. " +
"Always confirm with the user before rolling back.")
.WithTool(new AgentTool
{
Name = "get_deployment_status",
Description = "Returns the current status of the latest deployment.",
Handler = async (args) => await _deployService.GetStatusAsync()
})
.WithTool(new AgentTool
{
Name = "rollback_release",
Description = "Rolls back to the previous stable release. Requires explicit confirmation.",
Handler = async (args) => await _deployService.RollbackAsync()
})
.Build();
protected override async Task OnMessageActivityAsync(
ITurnContext<IMessageActivity> turnContext,
CancellationToken cancellationToken)
{
var result = await agent.RunAsync(turnContext.Activity.Text, cancellationToken);
await turnContext.SendActivityAsync(MessageFactory.Text(result.FinalResponse), cancellationToken);
}
Notice what moved and what did not. OnMessageActivityAsync is still the entry point — the Teams-facing plumbing from Part 2 does not change. What changes is everything after the activity arrives: instead of a hand-written branch, the model reads the tool registry, decides that get_deployment_status should run first, reads the result, and only then decides whether rollback_release applies. And, per the instructions, it pauses and asks for confirmation before calling it.
Where the Engineering Effort Actually Shifts
This is the part the comparison table does not show. With Bot Framework, most of your effort goes into conversation logic. With an agent, that effort shifts almost entirely into three places:
-
Tool descriptions. The model's only signal for when to call
rollback_releaseis theDescriptionstring. Vague descriptions produce wrong tool choices — and that is not an exaggeration. - Guardrails. "Always confirm before rolling back" is an instruction, not a guarantee. Production agents need this enforced structurally — through a confirmation step that the code requires regardless of what the model outputs, not just in the system prompt.
-
Retry and failure handling. A Bot Framework
if/elseeither matches or falls through predictably. An agent can call a tool with malformed arguments, choose the wrong tool, or loop. You need schema validation on tool inputs and a max-retry cap, the same defensive pattern used for any tool-calling system.
None of this is optional polish. Skip it, and the instruction to "confirm before rollback" becomes little more than a suggestion the model may ignore under the wrong prompt.
When to Use Which
The honest decision rule is straightforward: if you can enumerate every input and the exact response to each, write the if/else branch. It is simpler to test, cheaper to run, and impossible for the model to route incorrectly because there is no model in the loop.
Reach for the Agents SDK only when the task genuinely has more branches than you want to hand-write, and when the cost of an occasional wrong tool call is something your guardrails can catch before it causes damage.
For regulated processes — approvals, financial actions, and anything irreversible — default to Bot Framework's determinism, or to an agent with a hard-coded human-in-the-loop gate that the model cannot route around. We will build that HITL checkpoint properly with Durable Functions in Part 8.
What's Next
Part 4 connects the agent side of this comparison to Azure OpenAI directly — covering RAG, memory, and function calling patterns for when the "tool" the agent needs is a knowledge base rather than an internal API.
This is Part 3 of the "Building Intelligent Microsoft Teams Applications with .NET & Azure" series, part of the Microsoft Teams Integration Series.

Top comments (0)