Skip to main content
Veto’s human-in-the-loop (HITL) feature allows you to require manual approval for sensitive operations. When a rule with action: require_approval matches, the tool call is paused until a human approves or denies it.

How it works

When the agent attempts a tool call that requires approval:
  1. Veto intercepts the call and matches a require_approval rule
  2. Veto sends an approval request to your configured callback URL
  3. Your approval system responds with approved or denied
  4. If approved, the tool executes normally; if denied, Veto throws ToolCallDeniedError
┌───────────┐         ┌─────────┐         ┌──────────────┐
│ AI Agent  │────────▶│  Veto   │────────▶│ Approval API │
└───────────┘         └─────────┘         └──────────────┘
                           │                       │
                           │◀──────────────────────┘
                           │    {decision: "approved"}

                    ┌─────────────┐
                    │ Execute Tool │
                    └─────────────┘

Configuration

Local approval (webhook)

For local development or self-hosted approval systems, configure a callback URL:
veto/veto.config.yaml
version: "1.0"
mode: "strict"

approval:
  callbackUrl: "http://localhost:8787/approvals"
  timeout: 30000  # 30 seconds
  timeoutBehavior: "block"  # or "allow"
  includeCustomContext: false  # opt-in for forwarding context.custom
  responseSchema:
    decisionField: "decision"  # JSON field for decision
    reasonField: "reason"      # JSON field for reason

Cloud approval

For team workflows with centralized approval dashboard:
veto/veto.config.yaml
version: "1.0"
mode: "strict"
validation:
  mode: "cloud"

cloud:
  apiKey: "veto_..."

approval:
  pollInterval: 1000  # Poll every 1 second
  timeout: 60000      # Timeout after 60 seconds

Creating approval rules

Use action: require_approval to mark operations that need human review:
veto/rules/payments.yaml
rules:
  - id: require-large-transfer-approval
    name: Require approval for large bank transfers
    description: Escalate transfers above $10,000 for human review
    enabled: true
    severity: high
    action: require_approval
    tools: [transfer_funds, wire_transfer]
    conditions:
      - field: arguments.amount
        operator: greater_than
        value: 10000

Examples

- id: require-approval-large-transfers
  name: Require approval for large transfers
  action: require_approval
  tools: [transfer_funds, wire_transfer, send_money]
  conditions:
    - field: arguments.amount
      operator: greater_than
      value: 10000

- id: require-approval-external-transfers
  name: Require approval for external bank transfers
  action: require_approval
  tools: [wire_transfer]
  conditions:
    - field: arguments.to_account
      operator: starts_with
      value: "EXT-"
- id: require-approval-production
  name: Require approval for production deploys
  action: require_approval
  tools: [deploy, publish, release]
  condition_groups:
    - - field: arguments.environment
        operator: equals
        value: production
    - - field: arguments.env
        operator: equals
        value: prod
- id: require-approval-pii-access
  name: Require approval for PII queries
  action: require_approval
  tools: [query_database]
  conditions:
    - field: arguments.table
      operator: in
      value: ["customers", "users", "employees"]
    - field: arguments.columns
      operator: contains
      value: "ssn"
- id: require-approval-mass-email
  name: Require approval for mass emails
  action: require_approval
  tools: [send_email, send_notification]
  conditions:
    - field: arguments.recipients
      operator: length_greater_than
      value: 10

Implementing an approval webhook

Request format

Veto sends a POST request to your callback URL:
{
  "call_id": "call_abc123",
  "tool_name": "transfer_funds",
  "arguments": {
    "amount": 15000,
    "recipient": "ACME Corp",
    "account": "12345"
  },
  "rule_id": "require-large-transfer-approval",
  "rule_name": "Require approval for large bank transfers",
  "severity": "high",
  "timestamp": "2024-03-04T10:30:00Z",
  "session_id": "session_xyz",
  "agent_id": "agent_001"
}

Response format

Your webhook should return:
{
  "decision": "approved",
  "reason": "Transfer approved by finance team"
}
Decision values:
  • "approved" — Allow the tool call to proceed
  • "denied" — Block the tool call

Example implementation

import express from 'express';

const app = express();
app.use(express.json());

app.post('/approvals', async (req, res) => {
  const { tool_name, arguments: args, rule_id } = req.body;
  
  console.log(`Approval requested for ${tool_name}:`, args);
  
  // Example: auto-approve transfers <= $25,000
  if (tool_name === 'transfer_funds' && args.amount <= 25000) {
    return res.json({
      decision: 'approved',
      reason: 'Auto-approved: amount within auto-approval limit',
    });
  }
  
  // Example: deny transfers to unknown recipients
  const knownRecipients = ['ACME Corp', 'Vendor Inc'];
  if (!knownRecipients.includes(args.recipient)) {
    return res.json({
      decision: 'denied',
      reason: 'Unknown recipient',
    });
  }
  
  // Default: approve
  return res.json({
    decision: 'approved',
    reason: 'Manual review completed',
  });
});

app.listen(8787, () => {
  console.log('Approval webhook listening on port 8787');
});

Timeout behavior

If your approval webhook doesn’t respond within the configured timeout:
approval:
  timeout: 30000  # 30 seconds
  timeoutBehavior: "block"  # or "allow"
Timeout behaviors:
  • "block" (default) — Deny the tool call if approval times out
  • "allow" — Allow the tool call if approval times out
Use timeoutBehavior: "allow" with caution. It creates a fail-open system where timeouts bypass approval.

Custom context forwarding

By default, Veto does not forward context.custom to prevent leaking sensitive data. Opt in:
approval:
  includeCustomContext: true
This includes any custom context you provide:
const veto = await Veto.init({
  customContext: {
    user_role: 'admin',
    department: 'finance',
  },
});
Your webhook receives:
{
  "tool_name": "transfer_funds",
  "arguments": {...},
  "custom": {
    "user_role": "admin",
    "department": "finance"
  }
}

Cloud approval workflows

Veto Cloud provides a managed approval system with:
  • Web dashboard for approving/denying requests
  • Slack integration for approval notifications
  • Role-based access for approval permissions
  • Audit trail of all approval decisions

Setup

  1. Configure cloud mode:
    validation:
      mode: "cloud"
    cloud:
      apiKey: "veto_..."
    
  2. Access the dashboard at veto.so/dashboard
  3. View pending approvals and approve/deny with one click

Programmatic polling

You can also poll for approvals programmatically:
import { VetoCloudClient } from 'veto-sdk';

const client = new VetoCloudClient({
  apiKey: 'veto_...',
});

const approval = await client.pollApproval('approval_123', {
  interval: 1000,  // Poll every 1 second
  timeout: 60000,  // Timeout after 60 seconds
});

console.log(approval.status); // "approved" or "denied"

Approval in the agent workflow

From the agent’s perspective, approval is transparent:
import { Veto } from 'veto-sdk';
import { tool } from '@langchain/core/tools';

const veto = await Veto.init();

const tools = veto.wrap([
  tool(
    async ({ amount, recipient }) => {
      // This function only runs if approved
      return `Transferred $${amount} to ${recipient}`;
    },
    {
      name: 'transfer_funds',
      description: 'Transfer funds to a recipient',
      schema: z.object({
        amount: z.number(),
        recipient: z.string(),
      }),
    }
  ),
]);

// Agent calls tool normally
try {
  const result = await tools[0].invoke({
    amount: 15000,
    recipient: 'ACME Corp',
  });
  console.log(result); // Only prints if approved
} catch (error) {
  if (error instanceof ToolCallDeniedError) {
    console.error('Transfer denied:', error.reason);
  }
}

Best practices

Begin by requiring approval for the highest-risk operations:
  • Large financial transactions
  • Production deployments
  • Bulk data modifications
  • External API calls with side effects
Combine conditions to create smart approval rules:
- id: require-approval-after-hours
  action: require_approval
  tools: [deploy]
  conditions:
    - field: context.time
      operator: outside_hours
      value:
        start: "09:00"
        end: "17:00"
        timezone: "America/New_York"
Reduce approval fatigue by auto-approving low-risk variations:
// Auto-approve transfers to verified vendors
if (knownVendors.includes(args.recipient) && args.amount < 10000) {
  return { decision: 'approved', reason: 'Auto-approved: verified vendor' };
}
Track how long approvals take to identify bottlenecks:
const start = Date.now();
const approval = await client.pollApproval(approvalId);
const latency = Date.now() - start;
metrics.record('approval.latency', latency);
Balance responsiveness with human availability:
  • Development: 30-60 seconds
  • Production: 5-15 minutes
  • Critical operations: No timeout (fail closed)

Complete example

Here’s a full example with approval webhook and agent:
1

Configure approval

veto/veto.config.yaml
version: "1.0"
mode: "strict"
approval:
  callbackUrl: "http://localhost:8787/approvals"
  timeout: 30000
  timeoutBehavior: "block"
rules:
  directory: "./rules"
2

Create approval rule

veto/rules/payments.yaml
rules:
  - id: require-large-transfer-approval
    name: Require approval for large transfers
    action: require_approval
    tools: [transfer_funds]
    conditions:
      - field: arguments.amount
        operator: greater_than
        value: 10000
3

Implement webhook

webhook.ts
import express from 'express';

const app = express();
app.use(express.json());

app.post('/approvals', async (req, res) => {
  const { tool_name, arguments: args } = req.body;
  
  console.log(`Approval requested:`, args);
  
  // Simple approval logic
  if (args.amount <= 25000) {
    return res.json({
      decision: 'approved',
      reason: 'Amount within auto-approval limit',
    });
  }
  
  return res.json({
    decision: 'denied',
    reason: 'Amount exceeds auto-approval limit',
  });
});

app.listen(8787);
4

Use in agent

agent.ts
import { Veto, ToolCallDeniedError } from 'veto-sdk';
import { tool } from '@langchain/core/tools';

const veto = await Veto.init();

const tools = veto.wrap([
  tool(
    async ({ amount, recipient }) => {
      return `Transferred $${amount} to ${recipient}`;
    },
    { name: 'transfer_funds', /* ... */ }
  ),
]);

try {
  // This requires approval for amounts > $10,000
  const result = await tools[0].invoke({
    amount: 15000,
    recipient: 'ACME Corp',
  });
  console.log(result);
} catch (error) {
  if (error instanceof ToolCallDeniedError) {
    console.error('Denied:', error.reason);
  }
}

Next steps

How it works

Understand the validation flow

Rules

Learn the complete rule format

Approval workflows guide

Advanced approval workflow patterns

Audit trail

Export and analyze decision history

Build docs developers (and LLMs) love