Semantic Kernel function invocation filters as tool-call guardrails

In Semantic Kernel, a function invocation filter wraps every function call. Don't call next and the function never runs. We used that to hold large refunds for a person, and ran it.

The short answer

Register a function invocation filter. It runs every time a Semantic Kernel function is invoked, sees the function and its arguments, and calls next to continue. If it doesn’t call next, the function doesn’t run; it can set the result itself instead.1 That’s a tool-call guardrail: refuse, or return “this needs a person,” before any side effect.

For agents that call functions automatically, the auto function invocation filter adds the chat history, the list of functions about to run and iteration counters, and can terminate the whole auto-calling loop.1

Every MCP tool call goes through your rules first. Here a delete that doesn’t say which rows is stopped before it reaches the database. These are the rules deedglass init starts with; the call is made up.

The three filter types

Filter Runs Useful for
Function invocation Every time a KernelFunction is invoked Checking arguments, overriding the result before or after, handling errors, retrying1
Prompt render Before a prompt is rendered (prompt functions only) Viewing or changing the prompt; skipping the model call by setting the result1
Auto function invocation Only inside automatic function calling Seeing the whole planned batch; terminating the loop1

Each filter gets a context and a next callback. The docs are explicit: calling next is essential to proceed, and without it the operation won’t run.1 In Python you register filters with kernel.add_filter(FilterTypes..., fn) or the @kernel.filter(...) decorator.1

A filter that holds large refunds

We installed semantic-kernel 1.45.0 in a scratch environment and gave a kernel one plugin function, billing.refund, which prints a line when it actually runs. Then a filter:

Code Program code, for developers. The text around it says what it does.

@kernel.filter(FilterTypes.FUNCTION_INVOCATION)
async def approval_gate(context: FunctionInvocationContext, next):
    if context.function.name == "refund" and float(context.arguments["amount"]) > 100:
        context.result = FunctionResult(function=context.function.metadata,
                                        value="Refund over 100 needs a person. Not run.")
        return  # no next(): the function does not run
    await next(context)

The filter reads the arguments the model chose, decides, and either continues or answers in the function’s place. The message becomes the function’s result, so a model in an auto-calling loop reads it as a tool result and can tell the user.

What we saw

We invoked the function directly with two amounts:

Code What the screen or file shows, copied as is.

  [tool ran] refund ord_42 40.0
amount=40 result='refunded 40.0'
amount=250 result='Refund over 100 needs a person. Not run.'

The 40 refund ran: the [tool ran] line printed. The 250 refund didn’t: no [tool ran] line, and the result is the filter’s message. Nothing about the function changed; the rule lives entirely in the filter.

To turn “needs a person” into an actual approval, record the pending call (function, arguments, user) somewhere a reviewer sees it, and run the function from your own code once approved. A filter that blocks the model’s call is the gate; the approval flow is yours. The docs’ own example of filters in an approval flow is checking the permissions of the person submitting an approval before the flow begins.1

Using the auto function invocation filter

When an agent calls functions automatically, the model may plan several calls in one turn. The auto function invocation filter sees that whole batch, with the chat history and iteration counters, and can set terminate to stop the loop.1 Two guardrail uses follow:

  • A budget per conversation. Count calls or iterations and terminate past a limit, so a confused agent can’t loop through a hundred refunds.
  • A batch check. If the planned functions include a delete next to a lookup, stop and hand the turn back to the user before any of them runs.

Keep per-call argument checks in the function invocation filter, where every call passes regardless of how it was triggered, and use the auto filter for limits on the loop itself.

Test filters like code

Because filters are plain async functions, they’re easy to test. Invoke the function through the kernel, as we did above, with one case that should run and one that shouldn’t, and assert both the result and whether the function’s side effect happened. Our run’s [tool ran] line is that second check in its simplest form. Without it, a filter that sets a result after calling next looks like it blocked the call while the refund has already gone out.

Order matters

In Python, filters run in the order they’re added to the kernel, whether through add_filter or the decorator, nested around the function: the docs’ example prints “before filter 1, before filter 2, function, after filter 2, after filter 1.”1 In .NET, filters added through dependency injection run in no fixed order; add them to the kernel’s filter properties directly when order matters.1

For guardrails, that gives three rules:

  1. Blocking filters first. If a logging or caching filter runs before your guardrail and sets a result, your guardrail may never see the call.
  2. Logging outermost, if you want blocked calls logged. The outer filter’s “after” code runs whether or not inner filters called next.
  3. In .NET, don’t rely on dependency-injection order for anything security-relevant.1

Where filters don’t reach

  • Calls that bypass the kernel. In .NET, when IChatCompletionService is used directly, filters run only if a Kernel is passed to the call, because filters are attached to the kernel instance.1 Code paths that call your plugin methods directly skip them too.
  • Streaming results. In streaming mode, an overriding result must also be a stream; the context has an is_streaming flag to check.1 A guardrail that replaces results needs to handle both modes.
  • Other programs. Filters live in your process. Another agent or script calling the same API isn’t under them. A check in front of the tool itself, as a tool-call policy at a gateway, covers every caller.

A checklist

  • A function invocation filter checks every function that writes, sends or spends.
  • It refuses by not calling next and sets a clear result for the model.
  • Guardrail filters are added first; logging wraps them.
  • In .NET, guardrails are added to kernel properties, not only through dependency injection.
  • Direct IChatCompletionService calls pass the kernel.
  • Streaming and non-streaming results both handled.
  • Held calls are recorded for a human-in-the-loop approval, and run by your code once approved.

For comparable hooks in other frameworks, see Google ADK before_tool_callback and CrewAI tool call hooks. To see what the agents configured on your machine can change, paste the config into the check page or run npx deedglass scan (how the grade works); to try rules in your browser, open the playground.

Written with AI assistance and checked against our claims register.

The takeaway

In our Semantic Kernel 1.45 run, a filter that skipped next() held a 250 refund and returned a message instead, while a 40 refund ran. Order filters with care.

Read nextGoogle ADK before_tool_callback: tool-call guardrails, and when a plugin is better Try it: install in 7 s

Related on this site

More posts

Sources

  1. 1Microsoft Learn: Semantic Kernel filters · learn.microsoft.com · checked 26 October 2026

Published 26 October 2026, sources last checked 26 October 2026. If something here is out of date, write to hello@deedglass.com.

Try it on your own setup

npx deedglass scan

Grades only your own setup. Nothing is uploaded.

Pricing