Skip to main content
Veto supports human-in-the-loop approval for sensitive operations. When a rule has action: require_approval, execution pauses and sends the tool call context to your approval webhook. A human reviewer approves or denies, and Veto resumes execution accordingly.

How It Works

1

Agent calls a tool

The AI agent invokes a tool (e.g., transfer_funds with amount: 15000).
2

Veto intercepts the call

Veto matches the call against rules. A rule with action: require_approval triggers.
3

Approval request sent

Veto sends a POST request to your callbackUrl with:
  • Tool name
  • Arguments
  • Rule metadata
  • Session/agent/user IDs
4

Human reviews and responds

Your approval system (Slack bot, dashboard, API endpoint) prompts a reviewer. Reviewer responds with {"decision": "approved"} or {"decision": "denied"}.
5

Veto resumes execution

  • Approved: Tool executes normally
  • Denied: ToolCallDeniedError thrown
  • Timeout: Behavior determined by timeoutBehavior config

Configuration

Configure approval settings in veto/veto.config.yaml:
veto/veto.config.yaml
version: "1.0"
mode: "strict"

approval:
  callbackUrl: "http://localhost:8787/approvals"
  timeout: 30000                    # milliseconds (30 seconds)
  timeoutBehavior: "block"          # "block" (deny) or "allow" (permit)
  includeCustomContext: false       # forward context.custom in payload
  responseSchema:
    decisionField: "decision"       # field name for approval decision
    reasonField: "reason"           # optional reason field
callbackUrl
string
required
HTTP(S) endpoint where approval requests are sent.
timeout
number
default:30000
Maximum wait time (ms) for approval response.
timeoutBehavior
string
default:"block"
What to do if the callback times out:
  • "block": Deny the tool call (safe default)
  • "allow": Permit the tool call (risky, use with caution)
includeCustomContext
boolean
default:false
Include context.custom from veto.guard() in approval payload.
responseSchema
object
Custom field names for approval response:
  • decisionField: Name of the decision field (default "decision")
  • reasonField: Name of the optional reason field (default "reason")

Writing Rules with Approval

Set action: require_approval in your rule:
veto/rules/financial.yaml
version: "1.0"
name: financial-policies
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]
    conditions:
      - field: arguments.amount
        operator: greater_than
        value: 10000

Approval Request Payload

Veto sends a POST request with this structure:
{
  "call_id": "call_20260304_abc123",
  "tool_name": "transfer_funds",
  "arguments": {
    "amount": 15000,
    "from_account": "ACC-001",
    "to_account": "ACC-999"
  },
  "rule_id": "require-large-transfer-approval",
  "rule_name": "Require approval for large bank transfers",
  "severity": "high",
  "timestamp": "2026-03-04T14:23:45.123Z",
  "session_id": "session-xyz",
  "agent_id": "financial-agent",
  "user_id": "user-123",
  "role": "finance-manager"
}
If includeCustomContext: true, an additional custom field is included with data passed to veto.guard().

Implementing an Approval Endpoint

server.ts
import express from 'express';

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

app.post('/approvals', async (req, res) => {
  const { tool_name, arguments: args, rule_id, call_id } = req.body;

  console.log(`Approval requested for ${tool_name}:`, args);

  // Example logic: auto-approve <= 25k, deny above
  const amount = Number(args?.amount ?? 0);
  if (amount <= 25000) {
    return res.json({
      decision: 'approved',
      reason: 'Treasury auto-approval rule',
    });
  }

  return res.json({
    decision: 'denied',
    reason: 'Amount exceeds treasury limit',
  });
});

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

Response Format

Your approval endpoint must respond with:
{
  "decision": "approved",  // or "denied"
  "reason": "Optional human-readable reason"
}
Custom field names: If you configure responseSchema with custom fields:
responseSchema:
  decisionField: "status"
  reasonField: "message"
Your response should match:
{
  "status": "approved",
  "message": "Approved by finance team"
}

Timeout Handling

If your approval callback doesn’t respond within timeout milliseconds:
Safe default: Deny the tool call.
approval:
  timeout: 30000
  timeoutBehavior: "block"
// Tool call denied after 30s timeout
throw new ToolCallDeniedError('Approval timeout: no response from callback');
Use this for production systems where safety is paramount.

Real-World Example: Finance Approval

1

Configure rules

veto/rules/financial.yaml
version: "1.0"
name: financial-policies
rules:
  - id: require-large-transfer-approval
    name: Large Transfer Approval
    enabled: true
    severity: high
    action: require_approval
    tools: [transfer_funds]
    conditions:
      - field: arguments.amount
        operator: greater_than
        value: 10000

  - id: require-external-transfer-approval
    name: External Transfer Approval
    enabled: true
    severity: critical
    action: require_approval
    tools: [transfer_funds]
    conditions:
      - field: arguments.to_account
        operator: starts_with
        value: "EXT-"
2

Set up approval webhook

approval-server.ts
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;

  // Multi-tier approval logic
  const amount = args?.amount ?? 0;

  if (amount <= 25000) {
    // Tier 1: auto-approve
    return res.json({
      decision: 'approved',
      reason: 'Auto-approved under $25k limit',
    });
  }

  if (amount <= 100000) {
    // Tier 2: manager approval (integrate with your system)
    const managerApproval = await requestManagerApproval(req.body);
    return res.json(managerApproval);
  }

  // Tier 3: executive approval required
  const execApproval = await requestExecutiveApproval(req.body);
  return res.json(execApproval);
});

app.listen(8787);
3

Wrap tools and run agent

agent.ts
import { Veto } from 'veto-sdk';
import { createAgent, tool } from 'langchain';
import { z } from 'zod';

const transferFunds = tool(
  async ({ amount, from_account, to_account }) => {
    // Actual transfer logic
    return `Transferred $${amount} from ${from_account} to ${to_account}`;
  },
  {
    name: 'transfer_funds',
    schema: z.object({
      amount: z.number(),
      from_account: z.string(),
      to_account: z.string(),
    }),
  }
);

const veto = await Veto.init();
const tools = veto.wrap([transferFunds]);

const agent = createAgent({
  model: 'gpt-4o',
  tools,
});

// This will trigger approval workflow
await agent.invoke({
  messages: [{ role: 'user', content: 'Transfer $50,000 to EXT-BANK-999' }],
});

Testing Approval Flows

Local Testing

  1. Start approval server:
    node approval-server.js
    
  2. Test with veto guard check:
    veto guard check \
      --tool transfer_funds \
      --args '{"amount": 15000, "to_account": "EXT-999"}' \
      --json
    
  3. Inspect approval request: Your server logs the incoming payload. Verify all fields are present.

Integration Testing

Create a mock approval server for CI:
mock-approval-server.ts
import express from 'express';

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

// Always approve in test mode
app.post('/approvals', (req, res) => {
  console.log('Test approval:', req.body.tool_name);
  res.json({ decision: 'approved', reason: 'Test mode' });
});

const server = app.listen(8787);
process.on('SIGTERM', () => server.close());

Audit Trail

All approval decisions are recorded in Veto’s history. Export them with:
const jsonAudit = veto.exportDecisions('json');
const csvAudit = veto.exportDecisions('csv');
Each record includes:
  • timestamp
  • tool_name
  • arguments
  • rule_id
  • decision (allow/deny)
  • reason
See Audit Trail for full details.

Next Steps

Audit Trail

Export and analyze approval decisions

Testing Policies

Write tests for approval workflows

Writing Rules

Learn all rule syntax and operators

CI/CD Integration

Enforce approval rules in CI

Build docs developers (and LLMs) love