Human-in-the-Loop
Approval workflows for sensitive agent operations
Overview
Human-in-the-loop (HITL) workflows allow AI agents to request operator approval before executing sensitive operations. This pattern is essential for high-stakes actions like:
- Sending emails to VIPs or C-level executives
- Making financial commitments or purchases
- Modifying critical system configurations
- Deleting data or closing accounts
daimon.email provides a built-in HITL mechanism via the /v1/notify-operator endpoint, which generates magic approval links your operator can click to approve or reject actions.
Info
Human-in-the-loop is recommended for Level 1 autonomy agents (see Agent Autonomy Levels) where every action requires explicit approval.
How It Works
The HITL flow follows these steps:
- Agent detects sensitive operation - Your agent logic identifies an action that requires approval
- Create approval request - Call
POST /v1/notify-operatorwith action details - Send magic link to operator - API returns a unique approval URL
- Operator reviews and decides - Operator clicks link, sees context, approves/rejects
- Agent polls for decision - Poll
GET /v1/approvals/{id}until status changes - Execute or abort - Proceed with action if approved, log rejection if denied
sequenceDiagram
Agent->>API: POST /v1/notify-operator
API-->>Agent: {approval_id, magic_link}
Agent->>Operator: Send magic_link via Slack/SMS/email
Operator->>Browser: Click magic_link
Browser->>API: GET /approve/{token}
Operator->>Browser: Click "Approve" or "Reject"
Browser->>API: POST /approve/{token}
Agent->>API: GET /v1/approvals/{approval_id}
API-->>Agent: {status: "approved"}
Agent->>Agent: Execute sensitive actionCreating Approval Requests
import { DaimonClient } from '@daimon/sdk';
const client = new DaimonClient({ apiKey: process.env.DAIMON_API_KEY });
// Agent wants to send email to CEO
const approval = await client.approvals.create({
action_type: 'send_email',
action_context: {
to: 'ceo@company.com',
subject: 'Quarterly Revenue Report',
preview: 'Attached is the Q1 2026 revenue analysis...',
},
operator_message: 'Agent wants to send quarterly report to CEO. Review draft before sending.',
expires_in_seconds: 3600, // 1 hour
});
// Send magic link to operator (via Slack, SMS, email, etc.)
console.log(`Approval needed: ${approval.magic_link}`);
console.log(`Approval ID: ${approval.approval_id}`);
// Poll for decision
const checkApproval = async () => {
const status = await client.approvals.get(approval.approval_id);
if (status.status === 'approved') {
console.log('Operator approved! Sending email...');
await client.inboxes.send(inboxId, { /* email data */ });
} else if (status.status === 'rejected') {
console.log(`Operator rejected: ${status.rejection_reason}`);
} else if (status.status === 'expired') {
console.log('Approval request expired. Aborting action.');
}
};from daimon import DaimonClient
import time
client = DaimonClient(api_key=os.environ['DAIMON_API_KEY'])
# Create approval request
approval = client.approvals.create(
action_type='send_email',
action_context={
'to': 'ceo@company.com',
'subject': 'Quarterly Revenue Report',
'preview': 'Attached is the Q1 2026 revenue analysis...',
},
operator_message='Agent wants to send quarterly report to CEO. Review draft before sending.',
expires_in_seconds=3600
)
print(f"Approval needed: {approval.magic_link}")
# Poll for decision
while True:
status = client.approvals.get(approval.approval_id)
if status.status == 'approved':
print('Operator approved! Sending email...')
client.inboxes.send(inbox_id, {...})
break
elif status.status in ['rejected', 'expired']:
print(f'Request {status.status}: {status.rejection_reason or "timeout"}')
break
time.sleep(5) # Poll every 5 seconds# Create approval request
curl -X POST https://api.daimon.email/v1/notify-operator \
-H "Authorization: Bearer dm_live_..." \
-H "Content-Type: application/json" \
-d '{
"action_type": "send_email",
"action_context": {
"to": "ceo@company.com",
"subject": "Quarterly Revenue Report",
"preview": "Attached is the Q1 2026 revenue analysis..."
},
"operator_message": "Agent wants to send quarterly report to CEO. Review draft before sending.",
"expires_in_seconds": 3600
}'
# Response:
# {
# "approval_id": "apr_2gH8kLm9pQw1xYz3",
# "magic_link": "https://daimon.email/approve/eyJhbGc...",
# "expires_at": "2026-03-16T17:00:00Z",
# "status": "pending"
# }
# Check approval status
curl https://api.daimon.email/v1/approvals/apr_2gH8kLm9pQw1xYz3 \
-H "Authorization: Bearer dm_live_..."
# Response when approved:
# {
# "approval_id": "apr_2gH8kLm9pQw1xYz3",
# "status": "approved",
# "approved_at": "2026-03-16T16:23:45Z",
# "operator_note": "Looks good, send it!"
# }Approval Action Types
daimon.email supports several built-in action types with custom UI rendering:
| Action Type | Use Case | Context Fields |
|---|---|---|
send_email | Email sending approval | to, subject, preview, attachments |
delete_data | Data deletion | resource_type, resource_id, warning |
purchase | Financial transactions | amount, currency, vendor, description |
config_change | System configuration | setting_name, old_value, new_value |
custom | Generic approval | title, description, metadata |
Note
Custom action types use a generic approval UI. For best operator experience, use built-in types when possible.
Webhook Notifications
Instead of polling, you can receive webhook notifications when approvals are decided:
// Configure webhook endpoint
await client.webhooks.create({
url: 'https://your-agent.com/webhooks/daimon',
events: ['approval.approved', 'approval.rejected', 'approval.expired'],
});
// Handle webhook
app.post('/webhooks/daimon', async (req, res) => {
const event = req.body;
if (event.type === 'approval.approved') {
const { approval_id, action_type, action_context } = event.data;
// Execute the approved action
if (action_type === 'send_email') {
await client.inboxes.send(inboxId, action_context);
}
}
res.json({ received: true });
});from flask import Flask, request
app = Flask(__name__)
@app.route('/webhooks/daimon', methods=['POST'])
def handle_webhook():
event = request.json
if event['type'] == 'approval.approved':
approval_id = event['data']['approval_id']
action_type = event['data']['action_type']
action_context = event['data']['action_context']
# Execute approved action
if action_type == 'send_email':
client.inboxes.send(inbox_id, action_context)
return {'received': True}Best Practices
Set Reasonable Timeouts
Default to 1-4 hours for most approvals. Use 15 minutes for time-sensitive actions.
Provide Rich Context
Include email previews, change diffs, or cost estimates so operators can make informed decisions.
Handle Expiration Gracefully
Log expired approvals and notify the requester. Don't leave actions in limbo.
Track Approval Metrics
Monitor approval/rejection rates to tune autonomy levels over time.
Transitioning to Higher Autonomy
As operators build trust in your agent, you can reduce approval friction:
- Start with full approval - Require approval for all actions initially
- Identify safe patterns - Track which approvals are always approved (e.g., routine reports)
- Whitelist safe actions - Skip approval for pre-approved patterns
- Notify instead of block - Send FYI notifications after taking action
- Full autonomy - Reserve HITL only for truly exceptional cases
See Agent Autonomy Levels for the full spectrum.