n4nAI

Semantic Kernel plugins tutorial: writing native functions

Hands-on semantic kernel plugins native functions tutorial: build Python native function plugins, register them, invoke from kernel or LLM, with runnable code.

n4n Team3 min read701 words

Audio narration

Coming soon — every post will get a voice note here.

This semantic kernel plugins native functions tutorial walks through defining, registering, and invoking Python functions as first-class kernel plugins. Unlike semantic functions that lean on LLM prompts, native functions execute deterministic code while staying discoverable by planners and chat models.

Prerequisites

  • Python 3.10 or newer
  • semantic-kernel Python package (1.0+)
  • An OpenAI API key, or any OpenAI-compatible endpoint credential
  • Comfort with async/await and type hints
python -m venv .venv
source .venv/bin/activate
pip install semantic-kernel

Set your key in the environment:

export OPENAI_API_KEY="sk-..."

What a native function actually is

A native function is a regular Python method wrapped with @kernel_function. The decorator attaches a name, description, and schema derived from type hints. Once added to the kernel, the function lives in the kernel’s function catalog and can be called directly, composed with other functions, or offered to an LLM as a tool.

The decorator does not magically parallelize or cache anything. It simply makes the method inspectable and invocable through kernel.invoke.

Step 1: Write a native plugin class

Create text_tools.py. We define two trivial but realistic utilities: a word counter and a string reverser.

from semantic_kernel.functions import kernel_function

class TextTools:
    @kernel_function(
        name="word_count",
        description="Count the whitespace-separated words in a string",
    )
    def word_count(self, text: str) -> int:
        return len(text.split())

    @kernel_function(
        name="reverse",
        description="Return the input string reversed",
    )
    def reverse(self, text: str) -> str:
        return text[::-1]

The name parameter is optional; if omitted, the method name is used. The description is mandatory for good LLM tool selection, but technically optional for direct invocation.

Step 2: Register and call directly

The kernel is the registry and execution context. Add the plugin instance, then invoke by plugin and function name.

import asyncio
from semantic_kernel import Kernel
from text_tools import TextTools

async def main():
    kernel = Kernel()
    kernel.add_plugin(TextTools(), plugin_name="TextTools")

    wc_result = await kernel.invoke(
        plugin_name="TextTools",
        function_name="word_count",
        text="hello world from semantic kernel",
    )
    print("word_count:", wc_result.value)

    rev_result = await kernel.invoke(
        plugin_name="TextTools",
        function_name="reverse",
        text="abc",
    )
    print("reverse:", rev_result.value)

if __name__ == "__main__":
    asyncio.run(main())

Expected output:

word_count: 5
reverse: cba

invoke returns a FunctionResult object; .value holds the raw return type. If you skip .value, you’ll see the wrapper and metadata, which is useful when debugging parameter binding.

Step 3: Compose native functions in-process

Native functions are just callables in the kernel. You can chain them without an LLM by passing one result into another. This is handy for preprocessing pipelines.

async def pipeline(kernel: Kernel, phrase: str):
    reversed_text = (await kernel.invoke(
        plugin_name="TextTools", function_name="reverse", text=phrase
    )).value
    count = (await kernel.invoke(
        plugin_name="TextTools", function_name="word_count", text=reversed_text
    )).value
    return count

# inside main:
print("pipeline count:", await pipeline(kernel, "one two three"))

Expected output:

pipeline count: 3

The reversal does not change word count, but the example shows how the kernel normalizes inputs and outputs across calls.

Step 4: Expose functions to an LLM chat

The real payoff is letting a model decide when to call your code. Semantic Kernel maps @kernel_function metadata to OpenAI-style tool schemas. You point the chat service at any compatible backend, add the plugin, and pass the function view to the model.

If you want to avoid hard-coding a single provider, you can point the OpenAI connector at an OpenAI-compatible gateway such as n4n.ai, which fronts 240+ models and handles fallback when a provider is degraded.

import os
from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion

chat_service = OpenAIChatCompletion(
    model_id="gpt-3.5-turbo",
    api_key=os.getenv("OPENAI_API_KEY"),
    base_url="https://api.n4n.ai/v1",  # optional: OpenAI-compatible endpoint
)
kernel.add_chat_service("default", chat_service)

Now retrieve the function descriptors and send them alongside a user message:

from semantic_kernel.contents import ChatHistory

functions = kernel.get_functions_view().plugins["TextTools"].functions

history = ChatHistory()
history.add_user_message("Reverse 'kernel' and tell me how many words are in 'hello world'.")

response = await chat_service.get_chat_message_contents(
    chat_history=history,
    functions=functions,
)
print(response[0].content)

If the model emits a tool call, Semantic Kernel returns it inside the message; you then invoke the named function via kernel.invoke and feed the result back. In practice, higher-level agents automate this loop, but the native function surface is identical.

Step 5: Typing, defaults, and failure modes

The kernel uses Python type hints to build the JSON schema. Use explicit types; Any or missing hints degrade the schema and confuse LLM callers.

from semantic_kernel.functions import kernel_function
from typing import Annotated

class MathTools:
    @kernel_function(name="divide", description="Divide a by b")
    def divide(
        self,
        a: Annotated[float, "numerator"],
        b: Annotated[float, "denominator"],
    ) -> float:
        if b == 0:
            raise ValueError("division by zero")
        return a / b

Exceptions propagate as FunctionExecutionException. Catch them around kernel.invoke if you need to return a safe fallback to an LLM context.

Step 6: Verify the function catalog

Before shipping, inspect what the kernel actually exposes:

view = kernel.get_functions_view()
for plugin_name, plugin in view.plugins.items():
    print(plugin_name, "->", list(plugin.functions.keys()))

This prints:

TextTools -> ['word_count', 'reverse']
MathTools -> ['divide']

If a function is missing, check that the plugin was added with the correct instance and that the decorator imported from semantic_kernel.functions (not an old sk_function alias).

Operational notes

Native functions run in your process. They are not sandboxed. Anything you expose to an LLM—via a chat service or planner—can be invoked with arguments synthesized by the model. Validate and clamp inputs inside the function body; do not rely on the model to respect ranges.

For observability, wrap native functions with your own logging or use the kernel’s FunctionInvoked hook. Per-token metering and provider routing are concerns of the inference layer, not the kernel; if you front models with a gateway, those concerns stay out of your plugin code.

Closing checklist

  • Decorate methods with @kernel_function and write real descriptions.
  • Add the plugin instance via kernel.add_plugin, not the class.
  • Call via kernel.invoke(plugin_name=..., function_name=..., **kwargs).
  • Pass kernel.get_functions_view() to chat services to enable model-driven calls.
  • Type hints are the contract—treat them as production schema.

That is the full loop for a semantic kernel plugins native functions tutorial: write the class, register it, invoke directly, then hand the same functions to an LLM without rewriting a line of business logic.

Tagssemantic-kernelpluginsnative-functionstutorial

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All semantic kernel plugins & native functions posts →