Invoke Actions with Semantic Kernel - Wiring Real Work into Microsoft 365 Copilot
There's a moment in almost every Copilot project where the excitement wears off. Someone asks it a question about last quarter's numbers, it gives a tidy answer, and then they ask it to actually raise the purchase order, or update the CRM, or kick off the onboarding workflow. And nothing happens. Because a stock Copilot talks. It doesn't act.
Closing that gap is where the real value sits, and Semantic Kernel is one of the cleaner ways to do it. If you've been asked to make Copilot "do something in our systems" and you're staring at a documentation page wondering where to start, this is the post I wish someone had handed me eighteen months ago. Microsoft's own reference on invoking actions with Semantic Kernel is the canonical source, and this is the field version: what actually happens when you build it for a real Australian business.
What Semantic Kernel is doing here
Semantic Kernel is Microsoft's open-source SDK for building AI applications in .NET, Python and Java. Strip away the marketing and it does one job very well: it lets a language model call your code. You write a plain C# method that talks to your inventory system, you describe it to the kernel, and the model can decide to call it, with the right arguments, at the right time. Microsoft calls these "plugins" and the individual methods "functions".
That description step is the whole game. The model never sees your code. It sees a name, a description, and a list of parameters with their own descriptions. Get those words right and the model calls the function reliably. Get them wrong and it either ignores the function or calls it with rubbish arguments. I'll come back to this, because it's where most of the effort actually goes, and almost nobody expects that going in.
Here's the shape of a function in C#:
public class InventoryPlugin
{
[KernelFunction("check_stock")]
[Description("Gets the current stock level for a product by its SKU code.")]
public async Task<int> CheckStockAsync(
[Description("The product SKU, for example ELE-4471")] string sku)
{
return await _inventory.GetLevelAsync(sku);
}
}
That's it. No prompt engineering, no chain-of-thought scaffolding. You register the plugin with the kernel, and when a user asks "do we have any of ELE-4471 left", the model works out it should call check_stock with that SKU and hands you back a natural-language answer built from the result.
The Microsoft Graph angle
The reason this matters specifically for Microsoft 365 Copilot is Microsoft Graph. Graph is the single API surface for almost everything in the Microsoft 365 world: mail, calendar, Teams, SharePoint, Planner, users, groups, the lot. When you wire Semantic Kernel functions to Graph calls, you can build actions that operate on the actual tenant your users already live in.
So instead of a Copilot that says "you should email the project lead", you get one that drafts and sends the email, books the follow-up meeting, and posts a note to the Teams channel. The pattern is the same as the inventory example above, except the function body calls the Graph SDK:
[KernelFunction("send_summary")]
[Description("Sends an email summary to a named person in the organisation.")]
public async Task SendSummaryAsync(
[Description("The recipient's email address")] string recipient,
[Description("The plain-text body of the summary")] string body)
{
var message = new Message
{
Subject = "Project summary",
Body = new ItemBody { ContentType = BodyType.Text, Content = body },
ToRecipients = new List<Recipient>
{
new() { EmailAddress = new EmailAddress { Address = recipient } }
}
};
await _graphClient.Me.SendMail.PostAsync(new() { Message = message });
}
The honest part: the interesting complexity is not in Semantic Kernel. It's in the Graph permissions and the identity model behind that _graphClient. Which brings me to the thing that will actually slow your project down.
Identity is the hard bit, not the AI
Every action Copilot takes on someone's behalf runs as some identity, and getting that right is at least half of the real work. There are two broad choices and they have very different security implications.
The first is delegated permissions, where the action runs as the signed-in user. If Jenny asks Copilot to send an email, it sends as Jenny, and it can only touch what Jenny can already touch. This is almost always what you want, because it means your AI layer never becomes a way to sidestep the permissions your organisation already spent years setting up. The trade-off is you have to flow the user's token through to your function, which takes some plumbing with the on-behalf-of flow.
The second is application permissions, where the action runs as the app itself with its own broad rights. This is simpler to build and genuinely dangerous if you're not careful, because now your bot can read anyone's mail regardless of who asked. There are legitimate uses, background jobs, service accounts, but "we couldn't get delegated auth working so we used app permissions" is a sentence that shows up in incident reviews. Don't be that sentence.
We spend a real chunk of every Copilot engagement on exactly this decision, and it's the kind of thing our Microsoft AI consultants get called in to untangle after a proof of concept quietly used app-level access and someone in security finally noticed. Decide it deliberately, up front, in writing.
Where this fits against Copilot Studio and declarative agents
Fair question: Microsoft 365 has a low-code path too. Copilot Studio and declarative agents let you add actions without writing C#, and for a lot of scenarios that's the right call. So when do you reach for Semantic Kernel instead?
My rough rule after building both: if your actions are straightforward API calls with clean inputs and outputs, and the people maintaining them aren't developers, go low-code. Copilot Studio is genuinely good now, and our Copilot Studio consultants ship real things on it every week. But the moment you need actual logic between the model's decision and the API call, validation, multi-step orchestration, calling three systems and reconciling the results, retry logic that's smarter than "try again", you'll hit the ceiling of the low-code tools and start fighting them. That's when a pro-code approach built on Semantic Kernel or the broader Microsoft Agent Framework earns its keep. If you're weighing that decision, our Microsoft Agent Framework consultants have opinions worth borrowing.
The two aren't enemies, either. A common shape is a declarative agent for the conversational surface, calling a Semantic Kernel service for the genuinely hard actions behind it. Best of both, if you can stomach maintaining two things.
The thing nobody warns you about: descriptions are your prompt
I said I'd come back to this. Here it is, because it's the difference between a demo and something you'd trust in production.
The model chooses which function to call based entirely on the text in your [Description] attributes and function names. That text is, effectively, your prompt. And it's easy to write it like you'd write a code comment, terse, assuming context, which is exactly wrong. The model has no context. It has the words.
A description like "Gets stock" will get called at the wrong times and skipped at the right ones. "Gets the current stock level for a single product by its exact SKU code. Use this when a user asks whether a specific product is available or how many units are in stock." works far better, because it tells the model both what the function does and when to reach for it. Parameter descriptions matter just as much. If two of your functions sound similar to the model, it will guess, and it will guess wrong at the worst moment in front of the worst person.
Practically, this means you test your functions the way you'd test prompts: with a spread of real phrasings, watching which function fires. We keep a small evaluation script that runs fifty or so representative user utterances against the function set and reports which one got called. It's caught more bugs than any unit test, because the failure mode isn't a crash, it's a confidently wrong action. Those are the ones that erode trust, and trust is the entire product.
What to actually do next
If you're starting fresh, build one action end to end before you build ten. Pick something read-only and low-stakes, "what's on my calendar tomorrow" is a good first one, get the auth flowing and the descriptions tuned, and only then add write actions. Ship the boring safe thing first. The teams that get burned are the ones whose first shipped action could send email to the whole company.
Wrap every write action with a confirmation step. Copilot supports confirmation prompts precisely so the user can see "I'm about to send this email to this person" before it happens. For anything that changes state, use them. The small friction is worth it the first time it stops a mistake.
And log everything. Which function fired, with which arguments, as which identity, and what came back. When someone asks "why did Copilot update that record", and they will, you want an answer that isn't a shrug.
Semantic Kernel is a solid, well-supported way to make Copilot genuinely useful rather than just conversational, and the pattern is cleaner than it looks from the docs. The AI part is the easy part. The identity, the descriptions, and the discipline around write actions are where the project lives or dies. If you'd like a hand getting the architecture right before you're ten integrations deep, get in touch. It's a much shorter conversation before the build than after it.