> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kavachos.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Approval flows

> CIBA-style async human-in-the-loop approval for actions that should not run unattended.

## What approval flows solve

Some agent actions are too sensitive to run without a human saying yes first. File deletions, financial transfers, permission escalations, these benefit from a checkpoint before execution.

KavachOS supports this with a permission constraint called `requireApproval`. When the permission engine sees it, authorization is denied with the reason `"This action requires human approval before execution"`. Your application catches that signal, creates an approval request, notifies a human, and retries the action after a response comes back.

The flow is async. The agent does not block waiting. The human can respond minutes or hours later, within the request's TTL.

<Info>
  KavachOS creates the approval request and persists it. Delivering the notification to the human, email, Slack, push notification, is your application's job. Use `webhookUrl` or `onApprovalNeeded` to hook into your existing notification stack.
</Info>

## How the flow works

<Steps>
  <Step>
    **Agent triggers approval**

    The agent calls an action protected by `requireApproval: true`. Authorization is denied. Your application detects the denial reason and calls `kavach.approval.request()`.
  </Step>

  <Step>
    **Human gets notified**

    KavachOS fires your `webhookUrl` or `onApprovalNeeded` handler with the request details. Your app sends an email, opens a Slack DM, or surfaces a notification in your dashboard.
  </Step>

  <Step>
    **Human approves or denies**

    The human clicks a button in your UI. Your UI calls your backend, which calls `kavach.approval.approve(requestId)` or `kavach.approval.deny(requestId)`.
  </Step>

  <Step>
    **Agent retries**

    Once approved, your application retries the original action. For maximum simplicity, pass the request ID back to the agent so it can poll or be woken up when a decision arrives.
  </Step>
</Steps>

## ApprovalRequest fields

<ParamField path="id" type="string">Unique identifier prefixed apr\_.</ParamField>
<ParamField path="agentId" type="string">The agent requesting approval.</ParamField>
<ParamField path="userId" type="string">The user who owns the agent and should receive the notification.</ParamField>
<ParamField path="action" type="string">The action the agent wants to perform.</ParamField>
<ParamField path="resource" type="string">The resource the action targets.</ParamField>
<ParamField path="arguments" type={`Record<string, unknown> | undefined`}>The arguments the agent passed at the time of the call.</ParamField>
<ParamField path="status" type="'pending' | 'approved' | 'denied' | 'expired'">Current state. Transitions are one-way.</ParamField>
<ParamField path="expiresAt" type="Date">When the request expires if no response is received. Default TTL is 5 minutes.</ParamField>
<ParamField path="respondedAt" type="Date | undefined">When the human responded.</ParamField>
<ParamField path="respondedBy" type="string | undefined">Identifier of the person who approved or denied.</ParamField>
<ParamField path="createdAt" type="Date">When the request was created.</ParamField>

## Configuration

Pass approval config to `createKavach`:

```typescript theme={"system"}
const kavach = await createKavach({
  database: { provider: 'sqlite', url: 'kavach.db' },
  approval: {
    ttl: 600,                                          // 10-minute window (seconds)
    webhookUrl: 'https://your-app.com/webhooks/approval',
  },
});
```

Or use a custom handler for full control over delivery:

```typescript theme={"system"}
const kavach = await createKavach({
  database: { provider: 'sqlite', url: 'kavach.db' },
  approval: {
    ttl: 300,
    onApprovalNeeded: async (request) => {
      await sendSlackDm(request.userId, {
        text: `Agent ${request.agentId} wants to ${request.action} on ${request.resource}.`,
        approveUrl: `https://your-app.com/approvals/${request.id}/approve`,
        denyUrl: `https://your-app.com/approvals/${request.id}/deny`,
      });
    },
  },
});
```

Both `webhookUrl` and `onApprovalNeeded` fire asynchronously so the `request()` call is not delayed by notification latency.

## How webhooks work

When `webhookUrl` is set, KavachOS sends a `POST` to that URL with a JSON body:

```json theme={"system"}
{
  "event": "approval_needed",
  "request": {
    "id": "apr_...",
    "agentId": "agt_...",
    "userId": "user-123",
    "action": "delete",
    "resource": "file:prod-data/*",
    "arguments": { "path": "/prod/dataset.csv" },
    "status": "pending",
    "expiresAt": "2026-03-21T10:15:00.000Z",
    "createdAt": "2026-03-21T10:10:00.000Z"
  }
}
```

Webhook delivery failures are non-fatal. The request is already persisted, your app can poll `listPending()` as a fallback.

## Code examples

### Set up a permission that requires approval

```typescript theme={"system"}
const agent = await kavach.agent.create({
  ownerId: 'user-123',
  name: 'file-manager',
  type: 'autonomous',
  permissions: [
    {
      resource: 'file:prod-data/*',
      actions: ['read'],
    },
    {
      resource: 'file:prod-data/*',
      actions: ['delete'],
      constraints: { requireApproval: true },
    },
  ],
});
```

### Catch the denial and create a request

```typescript theme={"system"}
const result = await kavach.authorize(agent.id, {
  action: 'delete',
  resource: 'file:prod-data/dataset.csv',
  arguments: { path: '/prod/dataset.csv' },
});

if (!result.allowed && result.reason?.includes('requires human approval')) {
  const approvalRequest = await kavach.approval.request({
    agentId: agent.id,
    userId: agent.ownerId,
    action: 'delete',
    resource: 'file:prod-data/dataset.csv',
    arguments: { path: '/prod/dataset.csv' },
  });

  return { pending: true, approvalId: approvalRequest.id };
}
```

### Approve or deny from your UI handler

```typescript theme={"system"}
// In your API route handler
app.post('/approvals/:id/approve', async (req, res) => {
  const updated = await kavach.approval.approve(req.params.id, req.user.email);
  res.json({ status: updated.status });
});

app.post('/approvals/:id/deny', async (req, res) => {
  const updated = await kavach.approval.deny(req.params.id, req.user.email);
  res.json({ status: updated.status });
});
```

### List pending requests for a user

```typescript theme={"system"}
// All pending approvals across all users
const all = await kavach.approval.listPending();

// Pending approvals for a specific user
const forUser = await kavach.approval.listPending('user-123');

console.log(`${forUser.length} approvals waiting on user-123`);
```

### Expire stale requests

Requests that exceed their TTL are still stored with `status: 'pending'` until you run cleanup. Call this from a cron job:

```typescript theme={"system"}
const result = await kavach.approval.cleanup();
console.log(`Expired ${result.expired} stale approval requests`);
```

### Check the status before retrying

```typescript theme={"system"}
const request = await kavach.approval.get('apr_...');

if (request?.status === 'approved') {
  // Safe to retry the action
  await deleteFile(agent.id, '/prod/dataset.csv');
} else if (request?.status === 'denied') {
  console.log('Human denied the request.');
} else if (request?.status === 'expired') {
  console.log('Request expired without a response.');
}
```

## Next steps

<CardGroup cols={2}>
  <Card title="Permissions" href="/permissions">
    Add requireApproval constraints to individual permissions.
  </Card>

  <Card title="Trust scoring" href="/trust">
    Use trust levels to decide which agents need approval.
  </Card>

  <Card title="Audit trail" href="/audit">
    Every approval decision is linked to an audit entry.
  </Card>
</CardGroup>
