Skip to main content

Build Semantic Kernel Plugins in C#: [KernelFunction] + DI + Auto Calling

Verified Apr 2026 Intermediate Original .NET 10 Microsoft.SemanticKernel 1.71.0
By Rajesh Mishra · Mar 11, 2026 · 14 min read
In 30 Seconds

Semantic Kernel plugins are C# classes with methods exposed to the LLM via [KernelFunction] and [Description] attributes. Plugins support DI, async operations, and complex return types. They're the primary extensibility mechanism in both SK and Agent Framework. Design plugins as focused tools with clear descriptions for reliable tool selection.

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:

  1. Whether this tool is relevant to the user’s request
  2. 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

  1. State what the function does — not how it does it
  2. List what it returns — the LLM needs to know if the output will be useful
  3. Describe when to use it — reduces false positive invocations
  4. 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

⚠ Production Considerations

  • Vague descriptions cause unreliable tool selection. 'Gets data' tells the LLM nothing. 'Get the current stock price for a given ticker symbol from the NYSE' gives the LLM enough context to use the tool correctly.
  • Don't put too many functions in one plugin. The LLM receives all function schemas in its context — 20+ functions degrade selection accuracy and consume tokens. Keep plugins focused: 3-7 functions each.

Enjoying this article?

Get weekly .NET + AI insights delivered to your inbox. No spam.

Subscribe Free →

🧠 Architect’s Note

Think of plugins as your AI tool inventory. Every capability you want AI to access needs a plugin. Design them like you design APIs — focused, well-documented, and single-responsibility. These plugins outlive any specific framework version.

AI-Friendly Summary

Summary

Semantic Kernel plugins are C# classes with methods exposed to the LLM via [KernelFunction] and [Description] attributes. Plugins support DI, async operations, and complex return types. They're the primary extensibility mechanism in both SK and Agent Framework. Design plugins as focused tools with clear descriptions for reliable tool selection.

Key Takeaways

  • Decorate methods with [KernelFunction] and [Description] to expose them to the LLM
  • Descriptions are critical — the LLM uses them to decide when to call your function
  • Plugins support constructor DI for accessing services
  • Group related functions in one plugin class (3-7 functions per plugin)
  • Plugins work in both Semantic Kernel and Microsoft Agent Framework

Implementation Checklist

  • Create a C# class with public methods for each tool
  • Add [KernelFunction] attribute with a snake_case name
  • Add [Description] attributes to methods and parameters
  • Register the plugin with kernel.Plugins.AddFromType<T>()
  • Enable FunctionChoiceBehavior.Auto() for automatic invocation
  • Test with varied prompts to verify description quality

Frequently Asked Questions

What is a Semantic Kernel plugin?

A plugin is a C# class whose methods are exposed to the LLM as callable tools. Methods are annotated with [KernelFunction] and [Description] attributes. The LLM reads the descriptions and decides when to invoke each function based on user intent.

Can Semantic Kernel plugins use dependency injection?

Yes. Plugins support constructor injection. Register your services in the DI container, and SK will resolve dependencies when it creates plugin instances. This lets plugins access databases, HTTP clients, caches, and other services.

Do SK plugins work in Microsoft Agent Framework?

Yes. Agent Framework is built on SK. Any plugin you write with [KernelFunction] works identically in both frameworks. This is by design — your plugins are a long-term investment.

What is the difference between a plugin and function calling?

Function calling is the mechanism — the LLM generates structured calls, SK invokes them. A plugin is the pattern — a class that groups related functions under a logical name. Plugins organize your tools; function calling executes them.

Track your progress through this learning path.

You Might Also Enjoy

Was this article useful?

Feedback is anonymous and helps us improve content quality.

Discussion

Engineering discussion powered by GitHub Discussions.

#Semantic Kernel #Plugins #KernelFunction #.NET AI #Function Calling