The Approval Pattern: Using Webhooks for Human-in-the-Loop Agents
Autonomous agents are great, but some tasks need a human 'OK.' Learn how to use FetchHook as a secure, persistent approval queue for your AI agents.
Approval Workflow Primitive
1. Agent: "Should I buy this server? (Y/N)"
2. User: Clicks 'Approve' in custom internal tool
3. Tool: Sends Webhook -> FetchHook
4. Agent: Pulls 'Approved' event -> Executes purchase
5. Result: Human oversight without infrastructure#Why do agents need an approval mailbox?
For high-stakes tasks (money, production code, emails to customers), you don't want an agent running 100% solo. The Human-in-the-Loop (HITL) pattern solves this. By using FetchHook, the agent can pause its execution, poll its 'Approval Stash,' and only proceed when it 'sees' the human approval event in its mailbox. This creates a safe, auditable workflow where agents can be autonomous 95% of the time but defer to humans for critical decisions.
#How does the approval loop work technically?
When the agent reaches an approval gate, it sends a notification to the human (via email, Slack, SMS, or custom dashboard). That notification includes a link to an approval interface. When the human clicks 'Approve' or 'Reject', the interface sends a webhook to FetchHook with the decision payload. The agent, which has been polling FetchHook every 30-60 seconds, sees the approval event and proceeds (or aborts).
Agent Approval Logic (Python)
import requests
import time
def wait_for_approval(action_id, timeout_minutes=60):
"""
Agent pauses and waits for human approval via FetchHook.
Returns True if approved, False if rejected or timeout.
"""
deadline = time.time() + (timeout_minutes * 60)
while time.time() < deadline:
# Poll approval mailbox
response = requests.get(
"https://api.fetchhook.app/api/v1/stash_approvals",
headers={"Authorization": "Bearer fh_live_xxx"}
)
events = response.json().get('events', [])
for event in events:
# Check if this is our approval
if event['payload'].get('action_id') == action_id:
decision = event['payload'].get('decision')
if decision == 'approved':
print(f"✓ Action {action_id} approved by human")
return True
elif decision == 'rejected':
print(f"✗ Action {action_id} rejected by human")
return False
# Wait 30 seconds before checking again
time.sleep(30)
print(f"⏱ Approval timeout for action {action_id}")
return False
# Agent workflow
if agent_wants_to_charge_customer():
send_approval_notification_to_human()
if wait_for_approval(action_id="charge_customer_123"):
execute_payment()
else:
cancel_transaction()#How do I build the approval interface?
The approval interface can be anything that sends webhooks: (1) A simple HTML page with Approve/Reject buttons that POST to FetchHook, (2) A Slack bot with interactive buttons, (3) An internal admin dashboard, (4) A mobile app with push notifications. The key is that clicking a button triggers a webhook with the approval decision to your FetchHook ingress URL.
Simple Approval Interface (HTML + JS)
<!-- approval.html -->
<h2>Agent requests approval to deploy to production</h2>
<button onclick="sendApproval('approved')">Approve</button>
<button onclick="sendApproval('rejected')">Reject</button>
<script>
async function sendApproval(decision) {
await fetch('https://api.fetchhook.app/in/stash_approvals', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action_id: 'deploy_prod_456',
decision: decision,
approved_by: 'user@company.com',
timestamp: new Date().toISOString()
})
});
alert(`Decision: ${decision}`);
window.close();
}
</script>#Building the secure approval loop
Because FetchHook is pull-based, the agent never has to open a server to wait for your answer. You can send an approval from a mobile app, a Slack button, or a custom dashboard—even if the agent is running on localhost behind a firewall. The approval workflow is asynchronous and works across any network boundary. This is especially powerful for agents running in restrictive enterprise environments or on developer laptops.
#What happens if the human never responds?
Always implement timeout logic. If the human hasn't approved within N minutes, the agent should fail safely (cancel the action) and log the timeout. You can also implement escalation: after 30 minutes, send the approval request to a backup approver. After 60 minutes, auto-reject and alert the team. Never let an agent wait indefinitely—that creates a stuck state that's hard to debug.
Approval with Escalation (Node.js)
async function waitForApprovalWithEscalation(actionId) {
const ESCALATION_TIME = 30 * 60 * 1000; // 30 minutes
const TIMEOUT = 60 * 60 * 1000; // 60 minutes
const startTime = Date.now();
let escalated = false;
while (Date.now() - startTime < TIMEOUT) {
// Check for approval
const events = await fetchApprovals();
const approval = events.find(e => e.payload.action_id === actionId);
if (approval) {
return approval.payload.decision === 'approved';
}
// Escalate if primary approver hasn't responded
if (!escalated && Date.now() - startTime > ESCALATION_TIME) {
await notifyBackupApprover(actionId);
escalated = true;
}
await sleep(30000); // Check every 30 seconds
}
// Timeout: auto-reject and alert
await alertTeam(`Approval timeout for ${actionId}`);
return false;
}#What are real-world use cases for this pattern?
Common HITL scenarios: (1) Financial agents that need approval before charging customers or making purchases, (2) Code deployment agents that require sign-off before pushing to production, (3) Email/communication agents that need review before sending to customers, (4) Data deletion agents that require confirmation before removing records, (5) Infrastructure agents that need approval before scaling expensive resources. Any autonomous action with irreversible consequences should have a human gate.
Agent Protocol Instruction