Skip to main content
Veto maintains a complete history of every tool call, validation decision, and rule match. Export this data for compliance audits, debugging, or analytics.

How History Tracking Works

Every tool call processed by Veto is automatically recorded:
const veto = await Veto.init();
const tools = veto.wrap([transferFunds, getBalance]);

// Agent calls tools...
await agent.invoke({ messages: [...] });

// All decisions are tracked
const stats = veto.getHistoryStats();
console.log(stats);
// {
//   totalCalls: 5,
//   allowedCalls: 4,
//   deniedCalls: 1,
//   modifiedCalls: 0,
//   callsByTool: { transfer_funds: 3, get_balance: 2 }
// }
History is stored in-memory during the Veto instance lifetime. For persistent storage, export to JSON/CSV and save to your database or logging system.

History Record Structure

Each history entry includes:
timestamp
ISO 8601 string
When the tool call was made.
tool_name
string
Name of the tool that was called.
arguments
object
Complete arguments passed to the tool (deep-cloned for immutability).
validationResult
object
  • decision: "allow" | "deny" | "modify"
  • reason: Human-readable explanation (if available)
  • ruleId: ID of the matched rule
  • ruleName: Name of the matched rule
  • severity: Rule severity level
  • metadata: Additional rule metadata
durationMs
number
Optional: execution time in milliseconds.

Exporting Decisions

JSON Export

import { Veto } from 'veto-sdk';

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

// Run your agent...
await agent.invoke({ messages: [...] });

// Export as JSON
const jsonAudit = veto.exportDecisions('json');
console.log(jsonAudit);
Output format:
[
  {
    "timestamp": "2026-03-04T14:23:45.123Z",
    "tool_name": "transfer_funds",
    "arguments": {
      "amount": 15000,
      "from_account": "ACC-001",
      "to_account": "ACC-999"
    },
    "policy_version": "1.0",
    "rule_id": "block-large-transfers",
    "decision": "deny",
    "reason": "Transfer amount exceeds $10,000 threshold"
  },
  {
    "timestamp": "2026-03-04T14:24:12.456Z",
    "tool_name": "get_balance",
    "arguments": {
      "account_id": "ACC-001"
    },
    "policy_version": "1.0",
    "rule_id": null,
    "decision": "allow",
    "reason": null
  }
]

CSV Export

const csvAudit = veto.exportDecisions('csv');
console.log(csvAudit);
Output format:
timestamp,tool_name,arguments,policy_version,rule_id,decision,reason
2026-03-04T14:23:45.123Z,transfer_funds,"{""amount"":15000,""from_account"":""ACC-001"",""to_account"":""ACC-999""}",1.0,block-large-transfers,deny,Transfer amount exceeds $10,000 threshold
2026-03-04T14:24:12.456Z,get_balance,"{""account_id"":""ACC-001""}",1.0,,allow,
CSV escaping follows RFC 4180. Complex JSON arguments are serialized and quoted.

Saving to File

import { writeFileSync } from 'node:fs';

const veto = await Veto.init();
// ... run agent ...

// Save JSON
writeFileSync('audit.json', veto.exportDecisions('json'));

// Save CSV
writeFileSync('audit.csv', veto.exportDecisions('csv'));

Querying History

Veto provides methods to filter and query history:

Get All Entries

const allEntries = veto.getHistory();

Get Statistics

const stats = veto.getHistoryStats();
// {
//   totalCalls: 10,
//   allowedCalls: 7,
//   deniedCalls: 3,
//   modifiedCalls: 0,
//   callsByTool: { transfer_funds: 5, get_balance: 3, deploy: 2 }
// }

Clear History

veto.clearHistory();
Clearing history removes all in-memory records. Export before clearing if you need the data.

Integration with Logging Systems

Stream to Datadog

import { Veto } from 'veto-sdk';
import axios from 'axios';

const veto = await Veto.init();

// Wrap tools with logging
const tools = veto.wrap([transferFunds]);

// Run agent
await agent.invoke({ messages: [...] });

// Export and send to Datadog
const decisions = JSON.parse(veto.exportDecisions('json'));
for (const decision of decisions) {
  await axios.post(
    `https://http-intake.logs.datadoghq.com/api/v2/logs`,
    {
      ddsource: 'veto',
      ddtags: `tool:${decision.tool_name},decision:${decision.decision}`,
      message: decision.reason ?? 'No reason provided',
      ...decision,
    },
    {
      headers: {
        'DD-API-KEY': process.env.DATADOG_API_KEY,
      },
    }
  );
}

Stream to Elasticsearch

import { Veto } from 'veto-sdk';
import { Client } from '@elastic/elasticsearch';

const veto = await Veto.init();
const elastic = new Client({ node: 'http://localhost:9200' });

// After agent execution
const decisions = JSON.parse(veto.exportDecisions('json'));

const bulkBody = decisions.flatMap((doc) => [
  { index: { _index: 'veto-audit' } },
  doc,
]);

await elastic.bulk({ body: bulkBody });

Stream to PostgreSQL

import { Veto } from 'veto-sdk';
import { Pool } from 'pg';

const veto = await Veto.init();
const pool = new Pool({ connectionString: process.env.DATABASE_URL });

// Create table (once)
await pool.query(`
  CREATE TABLE IF NOT EXISTS veto_audit (
    id SERIAL PRIMARY KEY,
    timestamp TIMESTAMPTZ NOT NULL,
    tool_name TEXT NOT NULL,
    arguments JSONB NOT NULL,
    policy_version TEXT,
    rule_id TEXT,
    decision TEXT NOT NULL,
    reason TEXT
  )
`);

// Export and insert
const decisions = JSON.parse(veto.exportDecisions('json'));
for (const decision of decisions) {
  await pool.query(
    `INSERT INTO veto_audit (timestamp, tool_name, arguments, policy_version, rule_id, decision, reason)
     VALUES ($1, $2, $3, $4, $5, $6, $7)`,
    [
      decision.timestamp,
      decision.tool_name,
      decision.arguments,
      decision.policy_version,
      decision.rule_id,
      decision.decision,
      decision.reason,
    ]
  );
}

Real-Time Event Hooks

For real-time monitoring, use Veto’s event system:
import { Veto } from 'veto-sdk';

const veto = await Veto.init();

veto.on('decision', (event) => {
  console.log('Decision made:', {
    tool: event.toolName,
    decision: event.decision,
    ruleId: event.ruleId,
    timestamp: event.timestamp,
  });

  // Send to your monitoring system
  if (event.decision === 'deny') {
    sendSlackAlert(`🚨 Blocked: ${event.toolName}`);
  }
});

const tools = veto.wrap([...]);
Event hooks fire before the decision is recorded in history. Use them for real-time alerts.

Compliance Reporting

Generate Daily Reports

import { Veto } from 'veto-sdk';
import { writeFileSync } from 'node:fs';

const veto = await Veto.init();

// Run agent throughout the day...

// End-of-day export
const report = JSON.parse(veto.exportDecisions('json'));
const timestamp = new Date().toISOString().split('T')[0];

writeFileSync(`reports/veto-audit-${timestamp}.json`, JSON.stringify(report, null, 2));

console.log(`Audit report saved: veto-audit-${timestamp}.json`);

Filter by Decision Type

const decisions = JSON.parse(veto.exportDecisions('json'));

const blocked = decisions.filter((d) => d.decision === 'deny');
const approved = decisions.filter((d) => d.decision === 'allow');

console.log(`Total calls: ${decisions.length}`);
console.log(`Blocked: ${blocked.length}`);
console.log(`Approved: ${approved.length}`);

Group by Tool

const decisions = JSON.parse(veto.exportDecisions('json'));
const byTool = new Map();

for (const decision of decisions) {
  if (!byTool.has(decision.tool_name)) {
    byTool.set(decision.tool_name, { total: 0, allowed: 0, denied: 0 });
  }
  const stats = byTool.get(decision.tool_name);
  stats.total++;
  if (decision.decision === 'allow') stats.allowed++;
  if (decision.decision === 'deny') stats.denied++;
}

console.table([...byTool.entries()].map(([tool, stats]) => ({ tool, ...stats })));

History Configuration

Configure history limits in veto.config.yaml:
veto/veto.config.yaml
version: "1.0"

history:
  maxSize: 1000  # Maximum number of entries to keep in memory
Default: 1000 entries. Oldest entries are evicted when the limit is reached.
For high-throughput systems, export history periodically and clear it to avoid memory growth.

Example: Complete Audit Pipeline

1

Initialize Veto with history tracking

import { Veto } from 'veto-sdk';

const veto = await Veto.init();
2

Wrap tools and run agent

const tools = veto.wrap([transferFunds, getBalance]);

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

await agent.invoke({
  messages: [{ role: 'user', content: 'Transfer $50,000 to ACC-999' }],
});
3

Export decisions after execution

const jsonAudit = veto.exportDecisions('json');
const csvAudit = veto.exportDecisions('csv');
4

Save to disk and database

import { writeFileSync } from 'node:fs';
import { pool } from './db';

// Save to file
writeFileSync('audit.json', jsonAudit);
writeFileSync('audit.csv', csvAudit);

// Save to database
const decisions = JSON.parse(jsonAudit);
for (const decision of decisions) {
  await pool.query(
    'INSERT INTO veto_audit (timestamp, tool_name, arguments, rule_id, decision, reason) VALUES ($1, $2, $3, $4, $5, $6)',
    [
      decision.timestamp,
      decision.tool_name,
      decision.arguments,
      decision.rule_id,
      decision.decision,
      decision.reason,
    ]
  );
}
5

Clear history for next session

veto.clearHistory();

Debugging with History

Use history exports to debug policy issues:
# Export decisions after a test run
node agent.js

# Inspect decisions
cat audit.json | jq '.[] | select(.decision == "deny")'
Example output:
{
  "timestamp": "2026-03-04T14:23:45.123Z",
  "tool_name": "transfer_funds",
  "arguments": {
    "amount": 15000,
    "from_account": "ACC-001",
    "to_account": "ACC-999"
  },
  "rule_id": "block-large-transfers",
  "decision": "deny",
  "reason": "Transfer amount exceeds $10,000 threshold"
}
Use this data to:
  • Verify rules matched correctly
  • Identify unexpected denials
  • Tune rule conditions
  • Test rule changes with veto diff --log audit.json

Next Steps

Testing Policies

Use audit logs to validate rule behavior

CI/CD Integration

Export audit logs in CI for compliance checks

Writing Rules

Learn to write rules that generate clean audit logs

Approval Workflows

Track approval decisions in audit logs

Build docs developers (and LLMs) love