NORNRMandates, approvals and evidence for autonomous agents.
Guide / Semantic Kernel
10 minutesHow to add a budget to a Microsoft Semantic Kernel agent
Add a governed wallet, spend limit and approval gate to a Semantic Kernel agent using NORNR in under 10 minutes.
1. Why Semantic Kernel agents need a budget
Microsoft Semantic Kernel is a popular orchestration layer for enterprise agents connecting to Azure OpenAI, OpenAI or other model providers. Once you deploy an SK agent in a loop — processing incoming requests, executing function plugins or running planners — each invoke_async call is a billable event. Without a governed spend layer, the only control available is the provider's account-level limit, which applies to the entire organisation and does not give you per-agent or per-workflow visibility.
NORNR adds a per-agent mandate in front of the kernel invocation. Every call declares its amount and intent before the model is touched. Low-cost requests continue immediately. Requests above your review threshold are queued for human sign-off in the control room. Out-of-policy requests are rejected with a reason before the API call is made.
2. Install what you need
pip install agentpay semantic-kernel openai
The agentpay SDK works with the hosted NORNR endpoint at https://nornr.com. You do not need to run a local server.
3. Create the governed wallet
from agentpay import Wallet
wallet = Wallet.create(
owner="sk-enterprise-agent",
daily_limit=60,
require_approval_above=20,
allowed_counterparties=["openai", "azure-openai"],
base_url="https://nornr.com",
)
Create this wallet once and persist the wallet ID. You can create it programmatically at application startup or from the NORNR control room at /app and store the ID in your environment variables.
4. Gate a direct kernel invocation
This pattern covers the simplest SK usage: calling kernel.invoke_async() directly to run a prompt or semantic function.
import asyncio
import semantic_kernel as sk
from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion
kernel = sk.Kernel()
kernel.add_service(OpenAIChatCompletion(service_id="gpt4o", ai_model_id="gpt-4o"))
async def run_governed_task(user_input: str):
# Estimate cost for this invocation (~2000 tokens at GPT-4o pricing)
estimated_cost = 0.04
decision = wallet.pay(
amount=estimated_cost,
to="openai",
purpose=f"sk-agent summarisation task: {user_input[:60]}",
)
if decision.get("status") == "approved":
summarise = kernel.add_function(
function_name="summarise",
plugin_name="WritingPlugin",
prompt="Summarise the following in two sentences: {{$input}}",
)
result = await kernel.invoke(summarise, input=user_input)
print(result)
elif decision.get("status") == "queued":
print("Approval required. Check the NORNR control room.", decision)
else:
print("Spend blocked by policy.", decision)
asyncio.run(run_governed_task("Explain the impact of the EU AI Act on enterprise deployments."))
5. Wrap a plugin function with the governance check
For plugin-based agents, the best practice is to add wallet.pay() at the top of the native function that triggers an external or paid API call. This ensures every plugin execution — not just the orchestrator's decision — goes through the mandate.
from semantic_kernel.functions import kernel_function
class ResearchPlugin:
"""Plugin that calls an external paid data API."""
def __init__(self, wallet):
self._wallet = wallet
@kernel_function(name="fetch_market_data", description="Fetch live market data for a given ticker.")
def fetch_market_data(self, ticker: str) -> str:
# Gate the paid external call before it executes
decision = self._wallet.pay(
amount=2.50,
to="market-data-vendor",
purpose=f"live market data fetch for {ticker}",
)
if decision.get("status") != "approved":
raise RuntimeError(
f"Market data fetch blocked: {decision.get('status')} — {decision.get('reasons', [])}"
)
# Only reaches here when policy has approved
return self._call_market_api(ticker)
def _call_market_api(self, ticker: str) -> str:
# Real API call goes here
return f"{{\"ticker\": \"{ticker}\", \"price\": 183.47}}"
6. Using NORNR with Azure OpenAI via Semantic Kernel
If your Semantic Kernel project targets Azure OpenAI rather than the OpenAI API directly, the pattern is identical. The only difference is the to field in the pay call — use "azure-openai" and ensure it is in your wallet's allowed_counterparties list.
from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion
# Register Azure OpenAI as the kernel service
kernel.add_service(
AzureChatCompletion(
service_id="azure-gpt4o",
deployment_name="gpt-4o",
endpoint="https://your-resource.openai.azure.com/",
api_key="...",
)
)
# Gate with the Azure counterparty label
decision = wallet.pay(
amount=0.08,
to="azure-openai",
purpose="document classification via Azure GPT-4o",
)
if decision.get("status") == "approved":
result = await kernel.invoke(classify_fn, input=document_text)
print(result)
7. What happens at each decision state
- approved — the spend intent is within the wallet's daily limit, below the approval threshold, and the counterparty is on the allowlist.
kernel.invoke()runs immediately. - queued — the amount exceeds the approval threshold. The kernel invocation is held. The decision appears in the NORNR control room for human review. Resume after approval via webhook or polling.
- rejected — policy blocked the action. The kernel never makes the API call. Log
decision.get("reasons")for diagnostics.
8. Common mistakes in Semantic Kernel integrations
- Calling
kernel.invoke()first and NORNR second. The governance check must precede the kernel call — not follow it. Reverse the order if this happens. - Using a flat global wallet for all kernel invocations. Create one wallet per agent or per workflow type so budgets are traceable to a specific execution context.
- Not handling
queuedin async planner flows. SK's planner can invoke multiple functions in sequence. Each paid step should check the decision state; a queued result should halt the planner, not silently succeed or fail. - Estimating amount as zero. A zero-amount spend intent will always return approved, bypassing the review threshold. Use a realistic token-cost estimate even if it is approximate.