daimon.email
Guides

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:

  1. Agent detects sensitive operation - Your agent logic identifies an action that requires approval
  2. Create approval request - Call POST /v1/notify-operator with action details
  3. Send magic link to operator - API returns a unique approval URL
  4. Operator reviews and decides - Operator clicks link, sees context, approves/rejects
  5. Agent polls for decision - Poll GET /v1/approvals/{id} until status changes
  6. 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 action

Creating 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 TypeUse CaseContext Fields
send_emailEmail sending approvalto, subject, preview, attachments
delete_dataData deletionresource_type, resource_id, warning
purchaseFinancial transactionsamount, currency, vendor, description
config_changeSystem configurationsetting_name, old_value, new_value
customGeneric approvaltitle, 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:

  1. Start with full approval - Require approval for all actions initially
  2. Identify safe patterns - Track which approvals are always approved (e.g., routine reports)
  3. Whitelist safe actions - Skip approval for pre-approved patterns
  4. Notify instead of block - Send FYI notifications after taking action
  5. Full autonomy - Reserve HITL only for truly exceptional cases

See Agent Autonomy Levels for the full spectrum.