Approvals queue
Give it an owner, measure the response time, and understand why an expired approval is worse than a denied one.
The queue is the thing most likely to rot
ENFORCE with approval rules and an unwatched queue is just a slower kind of
blocked. Approvals reaching APPROVAL_EXPIRED are actions that silently did not
happen, and nobody gets an alert about silence.
Before you promote to ENFORCE, decide: who owns it, what the response-time
expectation is, and what happens out of hours.
#Work it
/approvals in the console, with a pending-count badge.
Each card carries the full decision context, which is deliberate:
- The agent, its delegation chain, and what it is acting for
- 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 every signal that fired, each with its explanation
- The agent's recent action history
- The
purposethe agent supplied - The blast radius, for a destructive tool
An approver without context approves everything
Which is worse than having no approval step, because now there is a record saying a human reviewed it.
If approvers are routinely approving without reading, the problem is one of two things: the cards do not carry enough context, or the rule fires too often to be meaningful. Both are fixable. Neither is fixed by asking people to read more carefully.
#Deny with a real reason
bashcurl -X POST $CP/v1/approvals/apr_01ABC/deny \
-H "Authorization: Bearer $KEY" \
-H 'content-type: application/json' \
-d '{ "reason": "Order ord_8812 was already refunded on 1 Oct" }'
The reason reaches the agent as the message on APPROVAL_DENIED, so it can
report something meaningful upstream instead of "the request failed". "Denied"
on its own wastes the one chance to explain.
#Measure it
promqlhistogram_quantile(0.95,
rate(veefore_approval_decision_duration_seconds_bucket[1h]))
Your approval SLA, measured rather than asserted.
If p95 exceeds your rule TTLs, your approvals are expiring systematically
Default TTL is 900 seconds. A p95 of 1,400 seconds means a large share of approvals never get decided — and each one is an action that did not happen with nothing to indicate it.
Fix one of: the TTL, the staffing, or the rule that fires too often.
Alert on it:
yaml- alert: VeeforeApprovalsNotBeingWorked
expr: |
histogram_quantile(0.95,
rate(veefore_approval_decision_duration_seconds_bucket[1h])) > 900
for: 30m
labels: { severity: warning }
#Route it somewhere people are
Subscribe a webhook to approval.requested and push it into chat.
bashcurl -X POST $CP/v1/webhooks \
-H "Authorization: Bearer $KEY" -H 'content-type: application/json' \
-d '{
"name": "approvals-to-chat",
"url": "https://hooks.internal.example.com/veefore-approvals",
"event_types": ["approval.requested"]
}'
Verify the signature, then read state from the API
A webhook is a notification, not an authorization. Approving must happen through
the console or the API by an authenticated person holding approval:approve.
Do not build a chat bot that approves on a reaction without re-authenticating. That turns chat membership into approval authority.
#Set the TTL per rule
json{
"then": {
"decision": "REQUIRE_APPROVAL",
"reason": "Refunds above ₹1,000 require a human approver.",
"approval": {
"routingMode": "SINGLE",
"requiredApprovals": 1,
"approverRoleKeys": ["APPROVER"],
"ttlSeconds": 900
}
}
}
Match the TTL to how fast a human can realistically respond to that particular thing, not to a round number.
| Situation | TTL |
|---|---|
| Business-hours refund review | 900–3600 |
| Overnight batch needing morning sign-off | 43200 |
| Production deployment during a change window | 1800 |
| Anything a customer is waiting on | as short as your staffing supports |
An approval that always expires is a rule that always blocks, dressed up as a rule that asks.
#Routing
| Mode | Use when |
|---|---|
SINGLE |
One named person decides. The default |
GROUP |
Anyone on duty may decide. Best for an on-call rota |
QUORUM |
Two or more must agree. For irreversible or large-blast-radius actions |
SEQUENTIAL |
Ordered sign-off. For a genuine escalation chain |
requireStepUp: true 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.
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.
#Separation of duties
By default SECURITY_ADMIN does not hold approval:approve, and neither
does DEVELOPER. Only APPROVER, ADMIN and OWNER do.
That is deliberate: the person who wrote the rule requiring approval should not be the person satisfying it. Grant the overlap explicitly if your organization genuinely wants it, having decided to.
#Tuning the rules themselves
Review monthly. For each approval rule ask:
Is it firing too often? A rule that fires on most actions is not a control, it is a tax, and approvers will start rubber-stamping. Narrow the condition or raise the threshold.
Is it never firing? Either the threshold is wrong or the rule is describing something that does not happen. Both are worth knowing.
Is it being worked around? If a human is regularly doing the thing manually after an approval is denied, the rule is wrong, not the control. Find out what it should have said.
Is the right person deciding? An approver who always defers to someone else is not the approver; the person they ask is. Change the routing.