Building a declarative agent for Microsoft 365 Copilot with the M365 Agents Toolkit

Back in 2017, I built the SPAdminBot. It was a bot that could create site collections, look up site info, reindex webs, all through a conversational interface in Teams. I was pretty proud of it at the time. It required the Bot Framework SDK, a LUIS model for natural language understanding, custom dialogs to manage conversation flow, an Azure App Service to host it, a CI/CD pipeline to deploy it, and a few hundred lines of C# to glue everything together. That was the cost of entry for a conversational bot in 2017.
Today you can build something remarkably similar with a JSON file. No code. No hosting. No LUIS. I’m not even exaggerating.
What is a declarative agent
A declarative agent is essentially a manifest, a JSON document that tells Microsoft 365 Copilot what your agent does, what data it can access, and how it should behave. You don’t write bot logic. You don’t handle message activities. You don’t deploy anything to Azure. The Copilot runtime does all the heavy lifting. You just describe the agent’s purpose, write clear instructions for how it should respond, and connect it to data sources.
Think of it this way: instead of building a car engine from scratch, you’re filling out a spec sheet and letting the factory build it for you. The engine is Copilot. Your job is to tell it what kind of car you want.
Let’s build one
Let’s get started. We’re going to build a “SharePoint Site Advisor,” an agent that helps site owners understand their SharePoint sites. Storage usage, permissions, recent activity, that kind of thing. Open VS Code, make sure you have the M365 Agents Toolkit extension installed (that’s the thing formerly known as Teams Toolkit… yes, Microsoft renamed it again), and create a new declarative agent project.
The heart of the whole thing is the agent manifest. Here’s what ours looks like:
{
"$schema": "https://developer.microsoft.com/json-schemas/copilot/declarative-agent/v1.3/schema.json",
"version": "v1.3",
"name": "SharePoint Site Advisor",
"description": "Helps site owners understand their SharePoint sites: storage, permissions, recent activity, and best practices.",
"instructions": "You are a SharePoint site management assistant. When users ask about their sites, use the available tools to look up real data. Always present storage numbers in MB or GB (whichever is more readable). When reporting permissions, flag any external users or overly broad access. Be practical and direct. Suggest actions, not just information. If you don't have access to a site, say so clearly rather than guessing.",
"capabilities": [
{
"name": "GraphConnectors",
"connections": [
{
"connection_id": "sharepoint-sites"
}
]
}
],
"actions": [
{
"id": "siteStorageAction",
"file": "siteStoragePlugin.json"
}
]
}
That’s your entire agent definition. The instructions field is where the magic happens. It’s basically a system prompt for Copilot, scoped to your agent. The capabilities section connects it to a Graph Connector so it can access SharePoint data. And the actions array points to plugins that give it specific skills. No ActivityHandler. No ITurnContext<IMessageActivity>. No DI container registration. Just JSON.
Now compare that to the hundreds of lines of C# I wrote for the SPAdminBot. I’m not going to lie, it stings a little.
Connecting to real data
Of course, the agent manifest alone doesn’t do much. You need to give it actual tools to work with. That’s where plugins come in. Here’s the plugin config for our site storage checker:
{
"$schema": "https://developer.microsoft.com/json-schemas/copilot/plugin/v2.2/schema.json",
"schema_version": "v2.2",
"name_for_human": "Site Storage Checker",
"description_for_human": "Checks storage usage and quota for SharePoint sites",
"namespace": "siteStorage",
"functions": [
{
"name": "getSiteStorage",
"description": "Returns current storage usage, quota, and trend for a SharePoint site",
"capabilities": {
"response_semantics": {
"data_path": "$.results",
"properties": {
"title": "$.siteName",
"subtitle": "$.storageUsedFormatted"
}
}
}
}
],
"runtimes": [
{
"type": "OpenApi",
"auth": { "type": "OAuthPluginVault" },
"spec": { "url": "openapi-site-storage.yaml" },
"run_for_functions": ["getSiteStorage"]
}
]
}
The plugin points to an OpenAPI spec that describes your API. This way Copilot knows how to call it, what parameters to send, and how to present the results. The response_semantics section tells it how to map the API response to something human-readable. You still need an API somewhere that actually returns the data, but you don’t need to build the conversational layer around it. Copilot handles all of that.
When you still need a real bot
I’d love to tell you that declarative agents replace everything, but I’d be lying. There are scenarios where you still need a proper bot built with the Teams SDK.
First off, if you need complex multi-turn workflows where you’re tracking state across many conversation steps, a declarative agent’s instructions can only get you so far. Proactive messaging (where the bot reaches out to users without being asked) isn’t something a declarative agent can do at all. If you need custom UI with Adaptive Cards that have buttons, forms, and interactive elements, you’re still in bot territory. And of course, if your users don’t have Copilot licenses (which is still a reality for many organizations), a declarative agent simply won’t be available to them.
For those cases, a full bot with the Teams SDK is still the right call. But for a lot of the “ask a question, get an answer” scenarios that make up the majority of what we used to build bots for? A declarative agent is probably enough.
The developer role shift
What strikes me most about all of this is how the job has changed. In 2017, building the SPAdminBot was a coding exercise. I spent my time writing C#, configuring LUIS intents, debugging dialog waterfalls, setting up Azure resources. The “what should the bot do” part took maybe 10% of my time. The other 90% was plumbing.
With declarative agents, it’s flipped. The hard part is now writing good instructions. What should the agent do when it doesn’t have access to a site? How should it format storage numbers? Should it proactively flag permission issues or wait to be asked? These are product design questions, not engineering questions. And honestly, I think that’s a good thing because we were always spending too much time on the plumbing and not enough on the experience.
The SPAdminBot took me weeks to build. This SharePoint Site Advisor took an afternoon. I’m choosing not to think too hard about what that says about my 2017 productivity.