AI agents are becoming capable of doing more than answering questions or generating code. They can call APIs, update databases, create cloud resources, modify application settings, and execute operational tasks.
These capabilities can save time for development and operations teams. An AI agent might identify a failed deployment, prepare a configuration change, or restart an unhealthy service.
However, allowing an AI agent to make production changes without human approval creates serious risks.
An incorrect configuration update could interrupt an application. A database operation could remove important records. An agent might misunderstand an instruction and perform an action that affects the wrong environment.
The solution is not necessarily to prevent AI agents from performing operational tasks. Instead, separate the preparation of a change from its execution. Let the agent investigate the problem and prepare an action, but require an authorized person to approve sensitive operations before they reach production.
This article explains how to implement a human approval workflow for AI agents using ASP.NET Core, C#, and a production deployment service.
Why AI Agents Need Human Approval
Traditional automation usually follows predefined rules. A deployment pipeline executes known steps, and an administrator configures which environments it can modify.
AI agents work differently. They interpret instructions, select tools, and decide which actions may help complete a task.
This flexibility is useful, but it introduces uncertainty.
Consider an AI agent monitoring an ASP.NET Core application. It detects that the application is returning a large number of server errors.
The agent might investigate logs and discover that a database connection pool is exhausted. It then decides that restarting the application service could temporarily restore availability.
If the agent has direct production access, it may restart the service immediately.
That action could help, but it could also interrupt active requests or hide the underlying problem.
A safer workflow is:
The agent investigates the problem.
It prepares a proposed action.
The system records the action and its expected impact.
An authorized person reviews the proposal.
The person approves or rejects it.
The system verifies the approval.
The production operation executes.
The result is recorded for auditing.
This approach keeps the agent useful while ensuring that important operational decisions remain under human control.
Design the Approval Workflow
Before writing code, define which actions require approval.
Not every agent operation needs the same level of control.
For example, reading application logs is generally a low-risk operation. Restarting a production service is more sensitive. Deleting a database or changing network access rules can have a much larger impact.
A simple classification might look like this:
Action | Risk level | Approval requirement |
|---|
Read application logs | Low | Usually not required |
Check service health | Low | Usually not required |
Generate a deployment plan | Low | Not required |
Restart a production service | High | Required |
Change production configuration | High | Required |
Modify database records | High | Required |
Delete production resources | Critical | Strict approval and additional controls |
The classification should reflect the organization's environment. A restart might be routine in one system and highly disruptive in another.
The important principle is that the AI agent should not decide its own approval requirements. The application must enforce them through predefined policies.
Understand the Architecture
A human approval system can be implemented using five main components.
Component | Responsibility |
|---|
AI agent | Investigates problems and proposes actions |
Approval service | Validates actions and creates approval requests |
Approval interface | Allows authorized users to review requests |
Execution service | Performs approved production operations |
Audit service | Records decisions and execution results |
The agent communicates with the approval service instead of directly calling production APIs.
For example, an agent may request:
{
"action": "RestartService",
"environment": "Production",
"resource": "order-api",
"reason": "Service health checks are failing",
"expectedImpact": "Temporary interruption of active requests"
}
The approval service validates the request and stores it as pending.
The agent receives an approval request ID rather than permission to execute the operation.
Only the execution service can perform the production action, and it must verify that the approval is valid before proceeding.
This separation is important. Hiding a production API from the AI agent is more reliable than simply instructing the agent not to use it.
Create the Approval Data Model
Let's build a simplified approval system using ASP.NET Core and Entity Framework Core.
The system needs to store the proposed action, its target resource, the approval status, and the person who made the decision.
Create an enum for the approval status:
public enum ApprovalStatus
{
Pending = 0,
Approved = 1,
Rejected = 2,
Executing = 3,
Completed = 4,
Failed = 5,
Expired = 6
}
Next, create the approval entity:
public class AgentApproval
{
public Guid Id { get; set; }
public string ActionType { get; set; } = string.Empty;
public string Environment { get; set; } = string.Empty;
public string ResourceName { get; set; } = string.Empty;
public string Reason { get; set; } = string.Empty;
public string ExpectedImpact { get; set; } = string.Empty;
public ApprovalStatus Status { get; set; }
public string RequestedByAgent { get; set; } = string.Empty;
public string? ApprovedBy { get; set; }
public DateTime CreatedAtUtc { get; set; }
public DateTime? ExpiresAtUtc { get; set; }
public DateTime? DecisionAtUtc { get; set; }
public DateTime? ExecutedAtUtc { get; set; }
public string? ExecutionResult { get; set; }
public string? RejectionReason { get; set; }
}
The RequestedByAgent property identifies the agent or workflow that created the request.
The ApprovedBy property records the human decision-maker. It should be populated from the authenticated user's identity, not from a value supplied by the AI agent.
The expiration field prevents old approval requests from remaining valid indefinitely.
Register the entity in the database context:
public class AppDbContext : DbContext
{
public AppDbContext(
DbContextOptions<AppDbContext> options)
: base(options)
{
}
public DbSet<AgentApproval> AgentApprovals =>
Set<AgentApproval>();
}
In a production system, also add database constraints, indexes, and an audit record for each state transition.
Allow the Agent to Request Approval
The agent should be able to submit an action proposal, but it should not be able to approve or execute that proposal.
Create a request model:
public record CreateApprovalRequest(
string ActionType,
string Environment,
string ResourceName,
string Reason,
string ExpectedImpact);
Next, implement an approval service:
public class ApprovalService
{
private readonly AppDbContext _context;
public ApprovalService(AppDbContext context)
{
_context = context;
}
public async Task<Guid> CreateAsync(
CreateApprovalRequest request,
string agentId,
CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(request.ActionType) ||
string.IsNullOrWhiteSpace(request.ResourceName))
{
throw new ArgumentException(
"Action type and resource are required.");
}
var approval = new AgentApproval
{
Id = Guid.NewGuid(),
ActionType = request.ActionType,
Environment = request.Environment,
ResourceName = request.ResourceName,
Reason = request.Reason,
ExpectedImpact = request.ExpectedImpact,
RequestedByAgent = agentId,
Status = ApprovalStatus.Pending,
CreatedAtUtc = DateTime.UtcNow,
ExpiresAtUtc = DateTime.UtcNow.AddHours(2)
};
_context.AgentApprovals.Add(approval);
await _context.SaveChangesAsync(
cancellationToken);
return approval.Id;
}
}
The example creates a request that expires after two hours.
The agent identity should come from the authenticated service or workload identity. Do not trust an arbitrary agent ID provided in the request body.
Also, validate the action type and target against an allowlist. A production approval system should not accept arbitrary commands or unrestricted resource identifiers.
For example, it might allow RestartService for a specific group of production services but reject commands that attempt to execute arbitrary shell scripts.
Display Pending Requests to Authorized Users
The approval interface should show enough information for a reviewer to understand the proposed change.
At a minimum, display:
Action type.
Target environment.
Resource name.
Reason for the request.
Expected impact.
Request creation time.
Expiration time.
Agent identity.
A reviewer should not have to inspect application logs to understand what the agent wants to do.
Create an endpoint that returns pending approvals:
[ApiController]
[Route("api/approvals")]
[Authorize(Policy = "ProductionApprover")]
public class ApprovalsController : ControllerBase
{
private readonly AppDbContext _context;
public ApprovalsController(AppDbContext context)
{
_context = context;
}
[HttpGet("pending")]
public async Task<IActionResult> GetPending(
CancellationToken cancellationToken)
{
var approvals = await _context.AgentApprovals
.Where(x => x.Status == ApprovalStatus.Pending)
.OrderBy(x => x.CreatedAtUtc)
.Select(x => new
{
x.Id,
x.ActionType,
x.Environment,
x.ResourceName,
x.Reason,
x.ExpectedImpact,
x.CreatedAtUtc,
x.ExpiresAtUtc
})
.ToListAsync(cancellationToken);
return Ok(approvals);
}
}
The ProductionApprover policy should be configured using your application's identity provider and role or permission system.
Do not use a simple client-side check to decide whether someone can approve a request. The server must enforce the authorization policy on every approval operation.
Approve or Reject a Proposed Action
A reviewer needs two clear options: approve the request or reject it.
Approval should require an authenticated user with the appropriate permission.
A simplified approval endpoint looks like this:
[HttpPost("{id:guid}/approve")]
public async Task<IActionResult> Approve(
Guid id,
CancellationToken cancellationToken)
{
var approval = await _context.AgentApprovals
.FirstOrDefaultAsync(
x => x.Id == id,
cancellationToken);
if (approval == null)
{
return NotFound();
}
if (approval.Status != ApprovalStatus.Pending)
{
return Conflict(
"This request is no longer pending.");
}
if (approval.ExpiresAtUtc <= DateTime.UtcNow)
{
approval.Status = ApprovalStatus.Expired;
await _context.SaveChangesAsync(
cancellationToken);
return Conflict("The approval request has expired.");
}
approval.Status = ApprovalStatus.Approved;
approval.ApprovedBy =
User.Identity?.Name;
approval.DecisionAtUtc = DateTime.UtcNow;
await _context.SaveChangesAsync(
cancellationToken);
return Ok(new
{
approval.Id,
approval.Status
});
}
A rejection endpoint should follow the same authorization and state-validation rules.
It should also require a rejection reason so the team can understand why the proposed action was not accepted.
The example above demonstrates the basic workflow, but it is not sufficient for high-concurrency production use. Two reviewers could attempt to approve the same request at nearly the same time.
Use an atomic conditional database update, optimistic concurrency token, or equivalent transaction-based approach to ensure that only one decision can succeed.
Execute Only Approved Actions
This is the most important part of the architecture.
The production execution service must independently verify the approval before performing any operation.
Do not rely on the AI agent to remember whether a request was approved.
For example, create a service that handles approved restart operations:
public class ProductionActionService
{
private readonly AppDbContext _context;
private readonly ILogger<ProductionActionService> _logger;
public ProductionActionService(
AppDbContext context,
ILogger<ProductionActionService> logger)
{
_context = context;
_logger = logger;
}
public async Task ExecuteAsync(
Guid approvalId,
CancellationToken cancellationToken)
{
var approval = await _context.AgentApprovals
.FirstOrDefaultAsync(
x => x.Id == approvalId,
cancellationToken);
if (approval == null)
{
throw new InvalidOperationException(
"Approval request was not found.");
}
if (approval.Status != ApprovalStatus.Approved)
{
throw new InvalidOperationException(
"The action has not been approved.");
}
if (approval.ExpiresAtUtc <= DateTime.UtcNow)
{
approval.Status = ApprovalStatus.Expired;
await _context.SaveChangesAsync(
cancellationToken);
throw new InvalidOperationException(
"Approval has expired.");
}
if (approval.Environment != "Production")
{
throw new InvalidOperationException(
"Unexpected target environment.");
}
if (approval.ActionType != "RestartService")
{
throw new NotSupportedException(
"This action is not supported.");
}
if (approval.ResourceName != "order-api")
{
throw new InvalidOperationException(
"The requested resource is not allowed.");
}
approval.Status = ApprovalStatus.Executing;
await _context.SaveChangesAsync(
cancellationToken);
// Call the approved deployment or operations API here.
await RestartOrderServiceAsync(
cancellationToken);
approval.Status = ApprovalStatus.Completed;
approval.ExecutedAtUtc = DateTime.UtcNow;
approval.ExecutionResult = "Service restart completed.";
await _context.SaveChangesAsync(
cancellationToken);
_logger.LogInformation(
"Approved action {ApprovalId} completed.",
approvalId);
}
private Task RestartOrderServiceAsync(
CancellationToken cancellationToken)
{
// Integrate with the approved production
// operations service.
return Task.CompletedTask;
}
}
The restart method is an integration placeholder. Connect it to a deployment platform, cloud management API, or internal operations service with narrowly scoped permissions.
The execution service should not accept a resource name or action type directly from the AI agent at execution time. It should use the values stored in the approved request.
For example, if the reviewer approved restarting order-api, the agent must not be able to change the target to payment-api after approval.
The approval should apply to the exact action and parameters that were reviewed.
Handle Retries and Duplicate Execution
Production operations do not always complete successfully on the first attempt.
A network failure may occur after the production API has accepted a restart request but before the execution service receives the response.
If the application retries without checking the previous result, it may execute the same operation more than once.
Use an idempotency key based on the approval ID:
Idempotency-Key: 6c8a5b8e-2a21-4c11-a8f2-7d3f5c9a1234
The production operations service should recognize repeated requests with the same key and avoid performing the operation again.
For long-running operations, use a background worker and a durable job record instead of keeping an HTTP request open until execution finishes.
The execution service should also use an atomic state transition from Approved to Executing. Only one worker should be able to claim an approved request.
If the external operation succeeds but the application crashes before recording Completed, the system should reconcile the operation using the idempotency key or the external service's operation status.
Do not simply reset every Executing request to Approved. That could repeat an operation that already succeeded.
Add an Audit Trail
Approval decisions should be traceable.
For every request, record:
Who or which agent created the request.
What action was proposed.
Which environment and resource were targeted.
Who approved or rejected it.
When the decision was made.
Which execution service performed the operation.
Whether execution succeeded or failed.
The external operation identifier, if available.
Keep approval history separate from ordinary application logs.
For sensitive production operations, use append-only audit records or another mechanism that prevents ordinary application users from modifying historical decisions.
Also avoid recording secrets, access tokens, or unnecessary customer information in approval descriptions.
Common Mistakes to Avoid
Letting the AI agent approve its own request
The agent that proposes an action must not be allowed to approve it. Use separate identities and permissions for request creation, human approval, and production execution.
Checking approval only in the user interface
Hiding an Execute button does not provide security. The execution API must independently validate approval status and authorization.
Approving a general instruction instead of an exact action
Approval should cover the specific resource, environment, parameters, and expected operation. Any material change should require a new approval.
Allowing approvals to remain valid indefinitely
Use expiration times and reject outdated requests. A change that was safe two days ago may no longer be appropriate.
Ignoring duplicate execution
Network failures and worker restarts can cause retries. Use atomic state transitions, idempotency keys, and execution reconciliation.
Giving the agent production credentials
The agent should normally have permission to request an operation, not perform it directly. Keep production credentials inside the controlled execution service.
Summary
Human approval adds an important control layer between AI agents and production systems. It allows an agent to investigate problems and prepare operational changes without giving it unrestricted authority to execute those changes.
A reliable implementation separates request creation, approval, and execution. It validates the exact action, checks the reviewer's permissions, expires outdated requests, prevents duplicate execution, and records every decision.
For production applications, do not depend on prompts or agent instructions to enforce safety. Put approval rules in the backend and make the execution service verify them before every operation.
This approach allows teams to use AI agents for operational work while keeping sensitive production changes under human control.
Join the conversation! Your thoughts help the community grow.