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:

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 PatternStandard Auto-ExecutionHuman-in-the-Loop Approval
Execution TriggerImmediate upon model request.Paused until explicit human decision.
Reversibility FocusLow; assumes tool calls are low-impact.High; designed for non-reversible side effects.
Audit ComplianceIncomplete; logs record output but not permission.Immutable; records prompt, argument payload, and reviewer ID.
Handling RejectionUnhandled; failures occur post-execution.Contextual; agent receives rejection feedback to adjust plan.
User InteractionSynchronous 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

Disadvantages

Enterprise Best Practices

  1. Categorize Tools by Risk Tier: Reserve human approval requirements for actions with low reversibility or high financial and data impact.

  2. Expose Detailed Decision Context: Include complete argument summaries, source justifications, and user context in the approval UI so approvers do not make uninformed decisions.

  3. Ensure Idempotent Execution: Build downstream function tools to handle retries safely, preventing duplicate side effects if approval messages are replayed.

  4. 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

Troubleshooting Guide

Issue 1: Agent Executes Sensitive Tool Without Approval

Issue 2: Agent Loops Infinitely Requesting Approval

Issue 3: Rejection Breaks the Conversation Loop

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.