n4nAI

Semantic Kernel functions tutorial: prompt vs native

A practical head-to-head comparison of Semantic Kernel prompt functions versus native functions with code examples, a decision matrix, and clear guidance on when to use each approach.

n4n Team6 min read1,287 words

Audio narration

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

If you’re building with Semantic Kernel, the choice between prompt functions and native functions shapes everything from latency to testability to how your team collaborates. This comparison breaks down the trade-offs across the dimensions that actually matter in production: capabilities, cost, latency, ergonomics, ecosystem, and hard limits.

What each function type actually does

A prompt function is a templated string sent to an LLM. You define the prompt, optionally with variables, and Semantic Kernel handles the rendering and invocation. The model does the work — reasoning, formatting, extraction, whatever the prompt describes.

A native function is compiled code (C#, Python, Java) that runs in-process. It can call APIs, transform data, enforce schemas, or implement deterministic logic. The LLM never sees it unless you explicitly chain it.

Both implement IKernelFunction and participate in the same planning and invocation pipeline. The difference is where execution happens and what you can guarantee.

Capabilities: what each can express

Prompt functions excel at fuzzy, open-ended tasks: summarization, classification, creative generation, few-shot reasoning, and anything that benefits from the model’s latent knowledge. They handle ambiguity gracefully — if the prompt says “extract the key points,” the model interprets that in context.

Native functions excel at deterministic, verifiable operations: JSON schema validation, database queries, cryptographic signatures, date math, file I/O, and any logic that must produce the same output for the same input every time. They also handle streaming and cancellation natively because they’re just code.

The boundary blurs when you need structured output from a prompt function. You can enforce schema via JsonSchema or the new Handlebars prompt templates with typed parameters, but you’re still relying on the model to comply. Native functions give you compile-time guarantees.

// Prompt function: flexible but probabilistic
var summarize = kernel.CreateFunctionFromPrompt(
    "{{$input}}\n\nSummarize in 3 bullet points.",
    functionName: "Summarize",
);

// Native function: deterministic, testable, typed
public class TextUtils {
    [KernelFunction, Description("Counts words in text")]
    public int CountWords(string text) => text.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;
}

Price and cost model

Prompt functions incur token costs on every invocation — input tokens for the prompt plus context, output tokens for the completion. At scale, this dominates your bill. A 2k-token prompt with a 500-token response at GPT-4o pricing runs roughly $0.006 per call. Multiply by millions of requests and you’re optimizing prompts like database queries.

Native functions have zero marginal token cost. They consume CPU cycles and memory in your process. The cost is infrastructure: container runtime, cold starts if serverless, and developer time to write and maintain them. For high-volume, low-complexity operations (validation, transformation, lookup), native functions are orders of magnitude cheaper.

There’s a hidden cost with prompt functions: retry logic. When the model returns malformed JSON or hallucinates, you retry — often 2-3x. That multiplies token spend. Native functions either succeed or throw; you handle the exception once.

Latency and throughput

Prompt functions add network round-trips to the model provider. Even with streaming, first-token latency ranges from 200ms (small models, nearby regions) to 3-5s (large models, cross-region). Throughput is bounded by provider rate limits and your quota.

Native functions run in-process. Latency is microseconds to milliseconds depending on what the code does. Throughput scales horizontally with your compute — no external quota to hit.

Chaining changes the math. A planner that calls three prompt functions sequentially pays 3x latency. A native function that does the same work in one pass avoids the cascade. But a native function calling an external API (database, HTTP) introduces its own latency profile.

# Prompt function chain: 3 LLM round-trips
async def classify_and_route(text: str):
    category = await kernel.invoke("Classify", input=text)
    priority = await kernel.invoke("Priority", input=f"{text}\nCategory: {category}")
    return await kernel.invoke("Route", input=f"{category}|{priority}")

# Native equivalent: 1 LLM call + deterministic logic
async def classify_and_route_native(text: str):
    category = await kernel.invoke("Classify", input=text)  # only fuzzy step
    priority = compute_priority(category, text)              # native, instant
    return route_map[category][priority]                     # dict lookup

Ergonomics: authoring, debugging, testing

Prompt functions live in text — .skprompt files, YAML configs, or inline strings. Version control shows diffs, but semantic changes are hard to review. “Changed ‘summarize’ to ‘condense’” looks trivial; the behavioral impact is opaque.

Debugging prompt functions means inspecting rendered prompts and raw completions. Semantic Kernel’s IChatCompletionService logging helps, but you’re debugging a black box. Unit tests are integration tests: they require a model endpoint (or a mock that simulates one).

Native functions are code. Your IDE refactors them, your type checker validates them, your test framework runs them in milliseconds without mocks. You can set breakpoints, inspect variables, and write property-based tests. The feedback loop is tight.

The trade-off: prompt functions are faster to prototype. You describe intent in natural language and iterate. Native functions require upfront design — signatures, error handling, edge cases. For exploratory work, prompt functions win. For production hardening, native functions win.

# Prompt function as YAML — easy to tweak, hard to test
name: ExtractEntities
template: |
  Extract entities (person, org, location) from: {{$input}}
  Return JSON: {"entities": [{"type": "", "value": ""}]}
template_format: handlebars
input_variables:
  - name: input
    is_required: true

Ecosystem and tooling

Semantic Kernel’s planner, filters, and function calling abstraction treat both types identically. Planners can discover and invoke either. Filters (logging, auth, rate limiting) wrap both. The KernelFunctionMetadata surface area is the same.

Where they diverge: prompt functions integrate with prompt management tools (Langfuse, PromptLayer, custom registries). You can A/B prompt versions, track drift, and roll back without code deploy. Native functions integrate with your standard CI/CD, feature flags, and observability stack (OpenTelemetry, Datadog, etc.).

Serialization differs. Prompt functions serialize naturally to YAML/JSON for storage and sharing across services. Native functions require code deployment — you can’t hot-reload a C# assembly into a running kernel without plugin reloading logic.

Hard limits

Prompt functions hit model context windows. A prompt with 10k tokens of few-shot examples leaves little room for input. They hit provider rate limits (RPM, TPM). They hit content filters. They hallucinate. They’re non-deterministic by design.

Native functions hit memory, CPU, and OS limits. They hit your database connection pool. They hit third-party API quotas. They don’t hallucinate, but they have bugs. They’re deterministic — same input, same output — which means bugs are reproducible.

Prompt functions can’t access your private data unless you stuff it into the prompt (costly, limited by context). Native functions query your database, call your internal APIs, read your file system directly.

Comparison table

Dimension Prompt functions Native functions
Execution LLM inference (remote) In-process code
Determinism Probabilistic Deterministic
Marginal cost Per-token pricing Near-zero (compute only)
Latency 200ms–5s + network µs–ms (local)
Throughput limit Provider quota Your infrastructure
Structured output Schema-guided, not guaranteed Compile-time guaranteed
Private data access Via context stuffing Direct (DB, APIs, FS)
Authoring Natural language / templates Code (C#, Python, Java)
Debugging Prompt/completion inspection Standard debugger
Testing Integration-style, needs model Unit tests, fast, no mocks
Versioning Prompt registry, hot-reload CI/CD, plugin reload
Planner compatibility Full Full
Failure modes Hallucination, filter, quota Exceptions, bugs, timeouts

Which to choose: verdict by use case

Use prompt functions when:

  • The task requires reasoning, synthesis, or knowledge the model possesses (summarization, classification, translation, creative writing)
  • You’re prototyping and need to iterate on behavior faster than you can write code
  • The logic is genuinely fuzzy — “tone detection,” “intent classification,” “extract action items” — where edge cases are infinite
  • You need non-technical stakeholders to review or modify behavior (prompt templates are readable)
  • Volume is low enough that token cost doesn’t dominate

Use native functions when:

  • The operation is deterministic: validation, transformation, calculation, lookup, serialization
  • You need guaranteed schema compliance (JSON, protobuf, Avro) without retries
  • Private data access is required — database queries, internal service calls, file operations
  • Latency budget is tight (sub-100ms) or throughput is high (thousands of RPS)
  • You need testability: unit tests, property-based tests, fuzzing, CI gates
  • The logic is complex enough that expressing it in a prompt is harder than writing code (recursive parsing, state machines, crypto)

Hybrid pattern (most production systems): Use a prompt function for the fuzzy decision and native functions for everything else.

// Planner sees three functions, but only one hits the LLM
kernel.ImportPluginFromType<TextProcessing>();  // native: CountWords, SplitSentences, RedactPII
kernel.ImportPluginFromPromptDirectory("prompts"); // prompt: ClassifyIntent, Summarize

// Planner invocation: "Summarize the user's request and redact PII"
// 1. ClassifyIntent (prompt) -> "support_ticket"
// 2. RedactPII (native) -> clean text
// 3. Summarize (prompt) -> summary
// 4. CountWords (native) -> metadata

This keeps token spend minimal, latency predictable, and test coverage high. The prompt functions do what models do well; native functions do what code does well.

Rule of thumb: if you can write a pure function with a clear signature and exhaustive tests in 15 minutes, make it native. If the spec is “make this sound professional” or “figure out what the user wants,” make it a prompt function. When in doubt, start native — you can always wrap it in a prompt later, but extracting logic from a prompt into code is refactoring, not iteration.

Tagssemantic-kernelfunctionsprompt-functionscomparison

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 →