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:
Veto intercepts the call and matches a require_approval rule
Veto sends an approval request to your configured callback URL
Your approval system responds with approved or denied
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:
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:
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:
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
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"
}
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
Node.js (Express)
Python (Flask)
Cloudflare Worker
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' );
});
from flask import Flask, request, jsonify
app = Flask( __name__ )
@app.route ( '/approvals' , methods = [ 'POST' ])
def handle_approval ():
data = request.json
tool_name = data[ 'tool_name' ]
args = data[ 'arguments' ]
print ( f "Approval requested for { tool_name } : { args } " )
# Example: auto-approve transfers <= $25,000
if tool_name == 'transfer_funds' and args[ 'amount' ] <= 25000 :
return jsonify({
'decision' : 'approved' ,
'reason' : 'Auto-approved: amount within auto-approval limit'
})
# Example: deny transfers to unknown recipients
known_recipients = [ 'ACME Corp' , 'Vendor Inc' ]
if args[ 'recipient' ] not in known_recipients:
return jsonify({
'decision' : 'denied' ,
'reason' : 'Unknown recipient'
})
# Default: approve
return jsonify({
'decision' : 'approved' ,
'reason' : 'Manual review completed'
})
if __name__ == '__main__' :
app.run( port = 8787 )
export default {
async fetch ( request : Request ) : Promise < Response > {
if ( request . method !== 'POST' ) {
return new Response ( 'Method not allowed' , { status: 405 });
}
const data = await request . json ();
const { tool_name , arguments : args } = data ;
// Example: auto-approve transfers <= $25,000
if ( tool_name === 'transfer_funds' && args . amount <= 25000 ) {
return Response . json ({
decision: 'approved' ,
reason: 'Auto-approved: amount within limit' ,
});
}
// Default: deny
return Response . json ({
decision: 'denied' ,
reason: 'Approval required from finance team' ,
});
} ,
} ;
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
Configure cloud mode:
validation :
mode : "cloud"
cloud :
apiKey : "veto_..."
Access the dashboard at veto.so/dashboard
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
Start with high-value operations
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"
Implement auto-approval logic
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:
Configure approval
version : "1.0"
mode : "strict"
approval :
callbackUrl : "http://localhost:8787/approvals"
timeout : 30000
timeoutBehavior : "block"
rules :
directory : "./rules"
Create approval rule
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
Implement webhook
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 );
Use in agent
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