What Plugins Are
A plugin is a group of related C# methods that an LLM can call. You write normal C# code, add two attributes, and the LLM gains the ability to invoke your code when it determines the tool is relevant.
In Semantic Kernel’s architecture, plugins sit between your business logic and the AI model:
User prompt → LLM reasoning → Plugin invocation → Your C# code → Result to LLM → Response
The LLM never calls your code directly. It generates a structured request (“call get_order with orderId=123”), SK intercepts this, invokes your method, and feeds the result back. This is the same mechanism used in function calling — plugins are the organizational pattern on top of it.
Building Your First Plugin
A plugin is just a class. The attributes tell SK what to expose:
using System.ComponentModel;
using Microsoft.SemanticKernel;
public class OrderPlugin
{
[KernelFunction("get_order")]
[Description("Retrieve order details by order ID, including status, items, and shipping info")]
public async Task<string> GetOrderAsync(
[Description("The numeric order ID")] int orderId)
{
// In production: query your database
return $"Order {orderId}: 2 items, Status: Shipped, ETA: March 15";
}
[KernelFunction("search_orders")]
[Description("Search orders by customer email address. Returns up to 10 most recent orders.")]
public async Task<string> SearchOrdersAsync(
[Description("Customer email address")] string email)
{
return $"Found 3 orders for {email}: #1001 (Shipped), #998 (Delivered), #956 (Delivered)";
}
[KernelFunction("cancel_order")]
[Description("Cancel an order that hasn't shipped yet. Returns success or failure with reason.")]
public async Task<string> CancelOrderAsync(
[Description("The order ID to cancel")] int orderId)
{
return $"Order {orderId} cancelled successfully. Refund will process in 3-5 business days.";
}
}
The Two Required Attributes
[KernelFunction("name")] — Registers the method. The name parameter becomes the tool identifier the LLM sees. Use snake_case — it matches the tool naming convention LLMs are trained on.
[Description("...")] — Critical for both the method and each parameter. The LLM reads these descriptions to decide:
- Whether this tool is relevant to the user’s request
- What values to pass for each parameter
Bad descriptions → bad tool selection. This is the most common plugin bug.
Registration and Invocation
Register your plugin and enable automatic function calling:
var kernel = Kernel.CreateBuilder()
.AddAzureOpenAIChatCompletion("chat-deployment", endpoint, apiKey)
.Build();
// Register the plugin
kernel.Plugins.AddFromType<OrderPlugin>();
// Enable automatic tool invocation
var settings = new AzureOpenAIPromptExecutionSettings
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
};
var result = await kernel.InvokePromptAsync(
"What's the status of order 1001?", new(settings));
SK sends the function schemas to the LLM, the LLM decides to call get_order(orderId: 1001), SK invokes your method, returns the result, and the LLM formulates a natural language response.
Plugins with Dependency Injection
Real plugins need services — database contexts, HTTP clients, caches. SK supports constructor injection:
public class InventoryPlugin
{
private readonly IInventoryService _inventory;
private readonly ILogger<InventoryPlugin> _logger;
public InventoryPlugin(IInventoryService inventory, ILogger<InventoryPlugin> logger)
{
_inventory = inventory;
_logger = logger;
}
[KernelFunction("check_stock")]
[Description("Check current inventory stock level for a product by SKU")]
public async Task<string> CheckStockAsync(
[Description("Product SKU code, e.g. 'SKU-12345'")] string sku)
{
_logger.LogInformation("Checking stock for {Sku}", sku);
var stock = await _inventory.GetStockLevelAsync(sku);
return $"SKU {sku}: {stock.Quantity} units in stock, warehouse: {stock.Location}";
}
[KernelFunction("reserve_stock")]
[Description("Reserve inventory for a customer order. Returns reservation ID or failure reason.")]
public async Task<string> ReserveStockAsync(
[Description("Product SKU")] string sku,
[Description("Number of units to reserve")] int quantity)
{
var reservation = await _inventory.ReserveAsync(sku, quantity);
return reservation.Success
? $"Reserved {quantity}x {sku}. Reservation ID: {reservation.Id}"
: $"Cannot reserve: {reservation.FailureReason}";
}
}
Register with DI using AddFromObject with a resolved instance, or use AddFromType if the kernel builder has access to the service provider:
// Option 1: AddFromType with kernel DI
var builder = Kernel.CreateBuilder();
builder.Services.AddSingleton<IInventoryService, InventoryService>();
builder.Services.AddLogging();
builder.Plugins.AddFromType<InventoryPlugin>();
// Option 2: AddFromObject with pre-built instance
var inventory = serviceProvider.GetRequiredService<IInventoryService>();
var logger = serviceProvider.GetRequiredService<ILogger<InventoryPlugin>>();
kernel.Plugins.AddFromObject(new InventoryPlugin(inventory, logger));
Writing Good Descriptions
Descriptions are the most important part of a plugin. The LLM uses them as its only signal for tool selection. Compare:
// Bad — the LLM can't tell when to use this
[KernelFunction("get_data")]
[Description("Gets data")]
public string GetData(string id) { ... }
// Good — the LLM knows exactly what this does and when to use it
[KernelFunction("get_customer_profile")]
[Description("Get a customer's profile including name, email, subscription tier, and account creation date. Use when the user asks about customer info, account details, or membership status.")]
public string GetCustomerProfile(
[Description("Customer ID (numeric) or email address")] string customerIdentifier) { ... }
Description Guidelines
- State what the function does — not how it does it
- List what it returns — the LLM needs to know if the output will be useful
- Describe when to use it — reduces false positive invocations
- Document parameter formats — “numeric order ID” vs “UUID string”
Complex Return Types
Plugins can return complex objects. SK serializes them to JSON for the LLM:
public class ProductInfo
{
public string Name { get; set; } = "";
public decimal Price { get; set; }
public int StockLevel { get; set; }
public string Category { get; set; } = "";
}
public class ProductPlugin
{
[KernelFunction("get_product")]
[Description("Get detailed product information including price, stock, and category")]
public async Task<ProductInfo> GetProductAsync(
[Description("Product SKU code")] string sku)
{
return new ProductInfo
{
Name = "Wireless Keyboard",
Price = 49.99m,
StockLevel = 142,
Category = "Electronics"
};
}
}
The LLM receives the JSON representation and incorporates the structured data into its response. This is cleaner than formatting strings manually.
Multiple Plugins Working Together
Register multiple plugins and let the LLM chain them:
kernel.Plugins.AddFromType<OrderPlugin>();
kernel.Plugins.AddFromType<InventoryPlugin>();
kernel.Plugins.AddFromType<CustomerPlugin>();
var result = await kernel.InvokePromptAsync(
"Customer [email protected] wants to know if order 1001 has shipped, " +
"and whether product SKU-789 is still in stock.",
new(settings));
The LLM will call search_orders to find Alice’s order, check_stock for the SKU, and combine both results into a single response. SK handles the multi-tool orchestration automatically.
Prompt Functions as Plugins
Not all plugins need to be C# code. You can create plugins from prompt templates:
var summarizeFunction = kernel.CreateFunctionFromPrompt(
"Summarize the following text in exactly 3 bullet points:\n\n{{$input}}",
functionName: "summarize",
description: "Summarize text into 3 key bullet points");
kernel.Plugins.AddFromFunctions("text_tools", [summarizeFunction]);
The LLM can call this prompt function the same way it calls native plugins. This is useful for AI-to-AI delegation — one LLM call decides to summarize, and a separate LLM call does the summarization with a focused prompt.
Plugin Design Patterns
Single Responsibility
Each plugin should represent one domain capability:
OrderPlugin → order queries and mutations
InventoryPlugin → stock checks and reservations
CustomerPlugin → customer profile and preferences
NotificationPlugin → email and push notification triggers
Don’t create a “UtilityPlugin” with 30 unrelated functions.
Read/Write Separation
Separate query plugins from mutation plugins when possible:
public class OrderQueryPlugin // Safe — no side effects
{
[KernelFunction("get_order")] ...
[KernelFunction("search_orders")] ...
[KernelFunction("get_order_history")] ...
}
public class OrderMutationPlugin // Dangerous — modifies state
{
[KernelFunction("cancel_order")] ...
[KernelFunction("update_shipping")] ...
}
This lets you register query plugins freely but gate mutation plugins behind confirmation logic or human-in-the-loop filters.
Portable Across Frameworks
Plugins written for SK work directly in Microsoft Agent Framework. Design them as standalone tool libraries:
// This class works in SK, Agent Framework, and any future Microsoft AI SDK
// because it depends only on [KernelFunction] attributes
public class WeatherPlugin
{
private readonly HttpClient _http;
public WeatherPlugin(HttpClient http) => _http = http;
[KernelFunction("get_weather")]
[Description("Get current weather for a city")]
public async Task<string> GetWeatherAsync(string city) { ... }
}
Next Steps
- What is Semantic Kernel? — Architecture overview and where plugins fit
- Getting Started with Semantic Kernel — Hands-on from install to first plugin
- Function Calling in Semantic Kernel — The mechanism behind plugin invocation
- Build Your First AI Agent — Agents that use plugins as tools