Building API Plugins with TypeSpec for Microsoft 365 Copilot
Building API Plugins with TypeSpec for Microsoft 365 Copilot
If you've been building Copilot extensions, you've probably already dealt with OpenAPI specifications. They work, but writing them by hand is tedious, error-prone, and about as enjoyable as filling out tax forms. TypeSpec changes that equation.
TypeSpec is a language Microsoft created specifically for defining APIs. Think of it as a cleaner, more expressive way to describe your REST endpoints that then compiles down to OpenAPI specs. When paired with the Microsoft 365 Agents Toolkit, it gives you a much nicer workflow for building API plugins that connect your services to Copilot.
I've been using TypeSpec on a few recent projects, and the difference in developer experience compared to hand-writing OpenAPI JSON is significant. Here's what the workflow looks like and where it shines.
Why TypeSpec Over Raw OpenAPI?
I want to be clear about what problem TypeSpec solves. API plugins for Copilot need an OpenAPI specification to describe the endpoints the agent can call. You could write that spec by hand in JSON or YAML. Many teams do. But OpenAPI specs are verbose. A simple CRUD API can easily hit 200+ lines of specification, and keeping the spec in sync with your actual API is a constant chore.
TypeSpec lets you define the same API in maybe 50 lines of strongly-typed code. It handles the boilerplate. You get type safety, better readability, and the OpenAPI spec gets generated automatically. For teams maintaining multiple Copilot agents with different API integrations, this saves real time.
The other benefit is the Copilot-specific decorators. TypeSpec has extensions for Microsoft 365 that let you define agent metadata, conversation starters, and Adaptive Card templates right alongside your API definition. Everything lives in one place instead of being scattered across multiple config files.
Walking Through a Real Example
Let me walk through building an API plugin that manages blog posts using the JSON Placeholder API. This is a simplified example, but the patterns apply directly to production APIs.
Defining the Model and GET Operation
In your main.tsp file, you define a namespace for your API, point it at your server, and describe the operations:
@service
@server("https://jsonplaceholder.typicode.com")
@actions(#{
nameForHuman: "Posts APIs",
descriptionForHuman: "Manage blog post items with the JSON Placeholder API.",
descriptionForModel: "Read, create, update and delete blog post items."
})
namespace PostsAPI {
@route("/posts")
@get op listPosts(): PostItem[];
model PostItem {
userId: integer;
@visibility(Lifecycle.Read)
id: integer;
title: string;
body: string;
}
}
That's it. The @visibility(Lifecycle.Read) decorator on id tells the system that ID is read-only - it won't appear in create or update requests. Small detail, but it's the kind of thing that takes several lines of OpenAPI to express.
Adding Query Parameters
Want to filter posts by user? Add an optional parameter:
@route("/posts")
@get op listPosts(@query userId?: integer): PostItem[];
One line. In raw OpenAPI, you'd be adding parameter definitions, schema references, and marking things as optional across multiple sections.
Write Operations
Adding POST, PATCH, and DELETE follows the same pattern:
@route("/posts")
@post op createPost(@body post: PostItem): PostItem;
@route("/posts/{id}")
@patch op updatePost(@path id: integer, @body post: PostItem): PostItem;
@route("/posts/{id}")
@delete op deletePost(@path id: integer): void;
Clean and readable. Anyone familiar with TypeScript can understand what this API does at a glance.
Adaptive Cards for Better Responses
This is where things get interesting for the user experience. By default, Copilot renders API responses as plain text. Adding an Adaptive Card template changes how citations appear in the response.
You create a JSON template (post-card.json) with the card layout, then reference it with a decorator:
@route("/posts")
@card(#{ dataPath: "quot;, file: "post-card.json", properties: #{ title: "$.title" } })
@get op listPosts(@query userId?: integer): PostItem[];
The result is that when a user asks "list all blog posts", each post gets a nicely formatted card with title, body, and a "Read More" action button. It's a much better experience than a wall of text.
The Complete Agent Definition
What I really like about TypeSpec is that the entire agent definition - instructions, conversation starters, API endpoints - lives in a single file:
@agent("My Posts Agent", "Declarative agent for blog posts management.")
@instructions("""
You should help users with blog posts management.
You can read, create, update and delete blog post items.
""")
@conversationStarter(#{
title: "List Blog Posts",
text: "List all blog posts and render them as a table."
})
Compare that to juggling separate manifest files, instruction files, and OpenAPI specs. It's a genuinely better developer experience.
What I've Learned Using TypeSpec on Real Projects
Start with GET operations. Don't try to build a full CRUD plugin on day one. Get a read-only agent working, deploy it, let users test it. Then add write operations once you're confident the read path works correctly. Write operations with Copilot add confirmation prompts and more complex interaction flows - you want the foundation solid first.
The descriptionForModel field matters more than you'd think. Copilot uses this text to decide when and how to call your API. Vague descriptions lead to the agent calling the wrong endpoints or not calling them at all. Be specific about what each operation does, what parameters it expects, and what it returns.
Adaptive Cards are worth the effort. The default text rendering is functional but ugly. Spending 30 minutes on a card template makes a noticeable difference in how users perceive the agent. It goes from feeling like a hack to feeling like a proper feature.
Manifest validation can be picky. During provisioning, you might hit validation errors for certain parameter types. Nested objects and properties with minimum, maximum, or default values can cause failures. If the teamsApp/validateAppPackage step fails, check your generated OpenAPI spec for these patterns and simplify.
When TypeSpec Makes Sense
TypeSpec is the right choice when you're building new API plugins from scratch. The productivity gains are real, and having everything in one place reduces the cognitive overhead of managing Copilot agents.
If you already have a well-maintained OpenAPI spec for your API, the calculus is different. You might be better off using the existing spec directly rather than rewriting it in TypeSpec. The Agents Toolkit supports both approaches.
For organisations maintaining multiple Copilot agents - which is increasingly common as different departments want their own agents - TypeSpec's conciseness really pays off. Managing 10 agents through TypeSpec files is much more manageable than 10 sets of OpenAPI specs and manifest files.
Getting This Into Production
The deployment workflow uses the Microsoft 365 Agents Toolkit's provisioning step, which handles packaging and sideloading. For production deployments, you'll want to go through your organisation's normal Teams app approval process.
We've been helping Australian organisations build Copilot extensions and custom AI agents across a range of industries. TypeSpec has become our preferred approach for new API plugins because the development speed and maintainability gains are too significant to ignore.
If you're looking at connecting your APIs to Microsoft 365 Copilot and want guidance on the best approach, our Microsoft AI consulting team can help you evaluate your options and get to a working prototype quickly.
The full TypeSpec documentation for Copilot is available at Build API plugins with TypeSpec for Microsoft 365 Copilot.