The Developer Problem: Unconstrained AI Execution Risk
As AI agents transition from read-only search assistants to operational systems capable of modifying enterprise state, full autonomy introduces significant risk. Giving an agent unchecked permission to execute financial transfers, send external emails to customers, alter database records, or reconfigure cloud infrastructure creates operational and compliance vulnerabilities.
Autonomous AI execution risks include:
Irreversible Side Effects: An AI model hallucinating parameters in a database mutation or refund operation cannot easily be undone once executed.
Compliance and Governance Violations: Regulated industries (such as healthcare, banking, and defense) mandate explicit human authorization trails for automated decisions impacting customer data or finances.
Context Misalignment: An agent may accurately follow an instruction while missing critical enterprise context, leading to valid execution of an undesirable action.
Security Exposure: Prompt injection attacks can manipulate tool arguments, coercing an agent into issuing unauthorized API calls.
A Human-in-the-Loop (HITL) approval workflow addresses these risks by establishing runtime approval gates. When an agent determines that a sensitive action is required, execution pauses, surfacing proposed tool parameters to a human reviewer. The agent continues execution only after receiving explicit approval or incorporates rejection feedback into its planning cycle.
In .NET, Microsoft.Extensions.AI and Microsoft Agent Framework provide native abstractions—such as ApprovalRequiredAIFunction—to implement structured, auditable HITL execution patterns.
HITL Architecture: Auto-Execution vs. Gatekeeper Interception
Human-in-the-loop design shifts sensitive tool invocation from an automatic loop into an asynchronous gatekeeper loop.
┌──────────────────────────────┐
│ User Issues Prompt │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ Agent Plans Action │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ Tool Requires Human Review? │
└──────┬────────────────┬──────┘
│ │
No │ │ Yes
┌──────────────────┘ └──────────────────┐
▼ ▼
┌─────────────────────────┐ ┌─────────────────────────┐
│ Execute Tool Directly │ │ Pause Execution & Emit │
│ Return Result to Agent │ │ ApprovalRequestContent │
└─────────────────────────┘ └────────────┬────────────┘
│
▼
┌─────────────────────────┐
│ Human Reviewer Decision │
└─────┬──────────────┬────┘
│ │
Approved │ │ Rejected
┌────────────────┘ └────────────────┐
▼ ▼
┌─────────────────────────┐ ┌─────────────────────────┐
│ Send Approval Response │ │ Send Rejection Reason │
│ Agent Executes Tool │ │ Agent Reconsiders Plan │
└─────────────────────────┘ └─────────────────────────┘
The table below compares standard auto-executed tool patterns with gatekeeper approval workflows:
| Workflow Pattern | Standard Auto-Execution | Human-in-the-Loop Approval |
| Execution Trigger | Immediate upon model request. | Paused until explicit human decision. |
| Reversibility Focus | Low; assumes tool calls are low-impact. | High; designed for non-reversible side effects. |
| Audit Compliance | Incomplete; logs record output but not permission. | Immutable; records prompt, argument payload, and reviewer ID. |
| Handling Rejection | Unhandled; failures occur post-execution. | Contextual; agent receives rejection feedback to adjust plan. |
| User Interaction | Synchronous streaming response. | Asynchronous request-response flow. |
Implementing Human-in-the-Loop Approval Workflows in .NET
The following step-by-step implementation demonstrates how to build an approval-gated tool workflow using Microsoft.Extensions.AI in C#.
Step 1: Install Package Dependencies
Add the necessary AI packages to your .NET project:
Bash
dotnet add package Microsoft.Extensions.AI
dotnet add package Microsoft.Agents.AI
dotnet add package Azure.AI.OpenAI
Step 2: Define and Wrap Sensitive Function Tools
Define your domain function tools and wrap sensitive operations using ApprovalRequiredAIFunction.
C#
using System.ComponentModel;
using Microsoft.Extensions.AI;
public static class EnterpriseTools
{
[Description("Retrieves account details for a specific user ID.")]
public static string GetAccountInfo(string userId)
{
return $"Account [{userId}]: Balance $15,400, Status: Active.";
}
[Description("Processes a monetary refund to a customer account.")]
public static string IssueRefund(
[Description("Target customer ID")] string customerId,
[Description("Refund amount in USD")] decimal amount,
[Description("Reason for the refund")] string reason)
{
// Sensitive business logic executing financial mutation
return $"SUCCESS: Issued ${amount} refund to Customer '{customerId}'. Transaction ID: TX-90412.";
}
public static AITool[] GetConfiguredTools()
{
var readOnlyTool = AIFunctionFactory.Create(GetAccountInfo);
var sensitiveRefundTool = AIFunctionFactory.Create(IssueRefund);
return new AITool[]
{
readOnlyTool,
// Force human approval gate on financial refund tool
new ApprovalRequiredAIFunction(sensitiveRefundTool)
};
}
}
Step 3: Intercept and Handle Function Approval Requests
Process messages returned by the agent and check for FunctionApprovalRequestContent instances.
C#
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
public class ApprovalWorkflowRunner
{
private readonly AIAgent _agent;
public ApprovalWorkflowRunner(AIAgent agent)
{
_agent = agent;
}
public async Task ProcessUserRequestAsync(AgentThread thread, string userPrompt)
{
Console.WriteLine($"[User]: {userPrompt}");
var userMessage = new ChatMessage(ChatRole.User, userPrompt);
var response = await _agent.RunAsync(userMessage, thread);
// Inspect response contents for pending approval requests
var approvalRequests = response.Messages
.SelectMany(m => m.Contents)
.OfType<FunctionApprovalRequestContent>()
.ToList();
if (approvalRequests.Any())
{
foreach (var request in approvalRequests)
{
await HandleApprovalRequestAsync(thread, request);
}
}
else
{
Console.WriteLine($"[Agent]: {response.Text}");
}
}
private async Task HandleApprovalRequestAsync(AgentThread thread, FunctionApprovalRequestContent request)
{
Console.WriteLine("\n=== HUMAN APPROVAL GATE TRIGGERED ===");
Console.WriteLine($"Target Function: {request.FunctionCall.Name}");
Console.WriteLine($"Call ID : {request.FunctionCall.CallId}");
foreach (var arg in request.FunctionCall.Arguments)
{
Console.WriteLine($" Arg -> {arg.Key}: {arg.Value}");
}
// Prompt user for input (in production this bridges to UI/Webhooks)
Console.Write("Approve execution? (y/n): ");
var input = Console.ReadLine()?.Trim().ToLower();
bool isApproved = input == "y";
// Create the approval/rejection content response
var approvalResponse = request.CreateResponse(isApproved);
var responseMessage = new ChatMessage(ChatRole.User, new[] { approvalResponse });
Console.WriteLine(isApproved ? "[System]: Approval granted. Continuing execution..." : "[System]: Rejection submitted. Notifying agent...");
// Resume agent execution thread with the approval decision
var resumedResponse = await _agent.RunAsync(responseMessage, thread);
Console.WriteLine($"[Agent]: {resumedResponse.Text}");
}
}
Architectural Advantages and Disadvantages
Advantages
Deterministic Risk Mitigation: Prevents destructive API actions from executing without explicit verification.
Auditable Governance Trail: Approval responses tie function execution to specific reviewer identities and timestamps for enterprise compliance.
Graceful Rejection Recovery: When a human rejects a tool call, the agent receives the rejection as conversational context, allowing it to propose alternative actions.
Disadvantages
Latency and Asynchronous Friction: Waiting for human intervention introduces operational delays, requiring durable messaging patterns.
Reviewer Fatigue: Over-gating low-risk tools causes approvers to blindly accept requests, weakening security efficacy.
Enterprise Best Practices
Categorize Tools by Risk Tier: Reserve human approval requirements for actions with low reversibility or high financial and data impact.
Expose Detailed Decision Context: Include complete argument summaries, source justifications, and user context in the approval UI so approvers do not make uninformed decisions.
Ensure Idempotent Execution: Build downstream function tools to handle retries safely, preventing duplicate side effects if approval messages are replayed.
Enforce Timeouts and Escalation Paths: Configure SLA limits on pending approval requests, automatically canceling or escalating unacted requests after set durations.
Common Mistakes to Avoid
Relying Solely on System Prompts for Safety: Instructing a model "Ask before calling IssueRefund" is unreliable. Always enforce approval rules programmatically using
ApprovalRequiredAIFunction.Treating Rejection as an Exception: Halting application code when a user rejects a tool call prevents the agent from adapting. Pass rejection context back to the agent so it can revise its strategy.
Gating Read-Only Queries: Requiring approvals for safe read operations adds friction without reducing system risk.
Troubleshooting Guide
Issue 1: Agent Executes Sensitive Tool Without Approval
Root Cause: The function tool was registered using standard
AIFunctionFactory.Createwithout wrapping it in anApprovalRequiredAIFunction.Resolution: Ensure all sensitive tools pass through
new ApprovalRequiredAIFunction(sensitiveTool)prior to registering with the agent.
Issue 2: Agent Loops Infinitely Requesting Approval
Root Cause: Approval responses are not correctly attached to the conversation thread, causing the model to re-request authorization.
Resolution: Pass the
FunctionApprovalResponseContentinstance returned byrequest.CreateResponse(isApproved)back usingagent.RunAsyncon the same active thread.
Issue 3: Rejection Breaks the Conversation Loop
Root Cause: Rejection returns an unhandled exception rather than returning a formatted message to the model.
Resolution: Treat rejection as a valid response path (
request.CreateResponse(false)), allowing the LLM to process the denial and respond to the user.
Frequently Asked Questions (FAQs)
1. What is the difference between human-in-the-loop and human-on-the-loop?
Human-in-the-loop pauses execution until a human explicitly authorizes an action. Human-on-the-loop allows automated execution while providing humans with real-time monitoring and override capabilities.
2. Can approval workflows be handled asynchronously across external systems?
Yes. FunctionApprovalRequestContent can be serialized and sent over messaging buses (such as Azure Service Bus or RabbitMQ) to external review portals or Teams webhooks, resuming execution when the approval webhook returns.
3. How does the model behave when an approver rejects a tool call?
When rejected using request.CreateResponse(false), the model receives confirmation that the tool call was denied. It evaluates this feedback and either suggests an alternative plan or informs the user that the action was cancelled.
Conclusion
Human-in-the-loop approval workflows bridge autonomous AI capabilities and enterprise compliance requirements. By leveraging .NET abstractions like ApprovalRequiredAIFunction, developers can construct clear governance boundaries around sensitive tool execution.

Join the conversation! Your thoughts help the community grow.