Veefore docs

Approvals

A human decision bound to one specific request, with quorum, escalation and expiry — never a standing permission.

When policy returns REQUIRE_APPROVAL, the action stops and an approval request is created. A human decides. Only then does the action proceed.

An approval authorises one request, not a permission

An approval is bound to the exact request that created it — the agent, the tool, the parameters, the amount. Approving does not grant the agent a standing permission, and an approval cannot be replayed against a different request.

Present an approval id with a different payload and the gateway refuses with APPROVAL_NOT_GRANTED. This is what stops "approved once" from becoming "approved forever", which is the failure mode every approval system that keys on the approver rather than the request eventually has.

#The flow

  1. Policy returns REQUIRE_APPROVAL.
  2. Veefore creates the approval, routes it to approvers, and returns APPROVAL_REQUIRED with an approvalId and a pollAfterMs.
  3. An approver sees the request with full decision context and approves or denies.
  4. The agent retries with the approvalId. The pipeline runs again, the approval validates against this exact request, and the action proceeds.

Step 4 matters: the pipeline runs again. An approval does not bypass anything. If the agent was revoked between the approval and the retry, the retry is refused.

#Handling it in code

The SDK gives you three levels, from most to least control.

ts
import { VeeforeApprovalRequiredError, VeeforeApprovalDeniedError } from '@veefore/sdk';

// 1. Wait for the decision and then proceed. Good for a background worker.
const decision = await agent.veefore.actWithApproval(
  { tool: 'refund_customer', amount: 250_00, currency: 'INR' },
  { timeoutMs: 15 * 60 * 1000 },
);

// 2. Return control to a caller while the human decides.
try {
  await agent.act({ tool: 'refund_customer', amount: 250_00, currency: 'INR' });
} catch (error) {
  if (error instanceof VeeforeApprovalRequiredError) {
    return { status: 'waiting', approvalId: error.approvalId };
  }
  if (error instanceof VeeforeApprovalDeniedError) {
    return { status: 'denied', reason: error.message };
  }
  throw error;
}

// 3. Decide as data, no exceptions.
const result = await agent.evaluate({ tool: 'refund_customer', amount: 250_00 });
if (result.approval?.required) {
  await persist(result.approval.approvalId);
}
python
from veefore import (
    ActionRequest,
    VeeforeApprovalDeniedError,
    VeeforeApprovalRequiredError,
)

request = ActionRequest(tool="refund_customer", amount=250_00, currency="INR")

# 1. Wait for the decision and then proceed.
decision = agent.veefore.act_with_approval(request, timeout_seconds=900)

# 2. Return control to a caller while the human decides.
try:
    agent.act(request)
except VeeforeApprovalRequiredError as error:
    return {"status": "waiting", "approval_id": error.approval_id}
except VeeforeApprovalDeniedError as error:
    return {"status": "denied", "reason": str(error)}

# 3. Decide as data, no exceptions.
result = agent.evaluate(request)
if result.approval and result.approval.get("required"):
    persist(result.approval["approval_id"])

Respect `pollAfterMs`

The response tells you when to poll next. Honour it rather than busy-looping — polling faster will not make a human decide faster, and it will earn you RATE_LIMITED. waitForApproval already does this correctly.

#Routing

The policy effect controls who decides and how many of them.

json
{
  "then": {
    "decision": "REQUIRE_APPROVAL",
    "reason": "Refunds above ₹1,000 require a human approver.",
    "approval": {
      "routingMode": "QUORUM",
      "requiredApprovals": 2,
      "approverRoleKeys": ["APPROVER", "SECURITY_ADMIN"],
      "ttlSeconds": 900,
      "requireStepUp": true
    }
  }
}
Mode Behaviour
SINGLE One named approver decides
GROUP Anyone in the group may decide
QUORUM requiredApprovals distinct approvers must agree
SEQUENTIAL Approvers decide in order; a denial at any step ends it

requireStepUp forces the approver to re-authenticate at the moment of approving. Use it for anything irreversible: it means an unattended, still-valid browser session is not enough to approve a destructive action.

When several rules both require approval, the effects merge conservatively — the maximum requiredApprovals, the minimum ttlSeconds, the union of approver roles, and requireStepUp true if any rule asked for it.

#Expiry

An approval has a TTL. When it passes, the status becomes EXPIRED and the action is refused with APPROVAL_EXPIRED.

Expiry is not optional. A pending approval is a half-finished action holding an agent's attention, and an approval queue that only grows is one nobody reads. Default TTL is 900 seconds; tune it per rule to match how fast a human can realistically respond to that particular thing.

Statuses: PENDING, APPROVED, DENIED, EXPIRED, CANCELLED.

#What the approver sees

The approval card carries the full decision context, not just a yes/no prompt:

  • Which agent, under which delegation chain, acting for whom
  • The tool, the capability, the parameters, the amount
  • The policy and rule that required approval, with the conditions that matched
  • The risk level, score and the signals that fired, each with its explanation
  • The agent's recent action history
  • The blast radius if it is a destructive tool

An approver without that context is guessing, and an approver who is guessing approves everything. Which is worse than having no approval step, because now there is a record saying a human reviewed it.

#Denying

bash
curl -X POST https://veefore.example.com/v1/approvals/apr_01ABC/deny \
  -H "Authorization: Bearer $VEEFORE_API_KEY" \
  -H 'content-type: application/json' \
  -d '{ "reason": "Order was already refunded on 1 Oct" }'

A reason is required. It reaches the agent as the message on APPROVAL_DENIED, so the agent can report something meaningful upstream instead of "the request failed".

#Permissions

Permission Who
approval:read Every built-in role
approval:approve APPROVER, ADMIN, OWNER
approval:deny APPROVER, ADMIN, OWNER
approval:delegate APPROVER, ADMIN, OWNER
approval:configure ADMIN, OWNER

Notably SECURITY_ADMIN does not hold approval:approve by default, and DEVELOPER does not either. Separation of duties: the person who wrote the rule that requires approval should not be the person satisfying it. Grant it explicitly if your organization genuinely wants that overlap.

Identity, authority, policy and enforcement for autonomous AI agents.