Human-in-the-Loop (HITL)
Human-in-the-Loop authorization means some access requests cannot be decided automatically — a human must explicitly approve or deny them before the requester is granted access.
When a request triggers HITL, the evaluation does not return a final allow or deny. Instead it returns a pending state that your application must handle.
The evaluation response
A HITL-triggered evaluation returns decision: false with a reason_code of authorization_pending:
{
"decision": false,
"context": {
"reason_code": "authorization_pending",
"approval_id": "apr-c3d4e5f6",
"auth_req_id": "apr-c3d4e5f6",
"expires_in": 300,
"interval": 5
}
}
| Field | Meaning |
|---|---|
reason_code | authorization_pending — a human approval is required |
approval_id / auth_req_id | ID of the approval request. Use this to track or display the pending request |
expires_in | Seconds until this pending request expires. If nobody decides in time, the next evaluation call starts a fresh approval cycle — see What happens on expiry |
interval | Suggested polling interval in seconds. Poll no faster than this — see slow_down |
The recommended way: authorizeWithPolling()
If you're using the Go or Node.js Vengtoo SDK, use authorizeWithPolling() instead of calling evaluate directly. It handles the entire pending → poll → resolve loop for you:
// Node.js
const result = await vengtoo.authorizeWithPolling(params, {
timeoutMs: 5 * 60_000,
onPending: (authReqId, expiresIn) => {
// Called once, when the request first enters the pending state.
// Show a waiting UI here — not called again on subsequent polls.
console.log(`Waiting for approval: ${authReqId}`);
}
});
if (result.decision) {
// proceed
} else {
// denied, timed out, or a safety re-check blocked the reused approval
// — inspect result.context.reason_code
}
// Go
result, err := client.AuthorizeWithPolling(ctx, req, &vengtoo.PollingOptions{
Timeout: 5 * time.Minute,
OnPending: func(authReqID string, expiresIn int) {
log.Printf("waiting for approval: %s", authReqID)
},
})
authorizeWithPolling():
- Calls
onPendingexactly once, when the request first enters the pending state — not on every poll. - Polls at the server's suggested
interval, backing off automatically onslow_downwithout surfacing it to your code. - Returns (never throws) on timeout, with
reason_code: "approval_timeout"— handle it the same way as any other denial.
For languages without SDK support yet, see Manual polling.
Manual polling
- Detect the pending state — check for
decision: falseandreason_code: authorization_pending. - Show a waiting state to the user — the action is blocked until a human decides.
- Poll the evaluate endpoint at the
intervalcadence, passing the same request. Vengtoo returns the pending state on each poll until a human acts or the request expires. - Handle
slow_down— if you poll faster than the server expects, you receivereason_code: "slow_down". Back off and retry; don't surface this to the end user, it's a rate signal not a decision. - Handle the outcome:
- Approved →
decision: true,reason_code: "approved_by_human". - Denied →
decision: false,reason_code: "access_denied". - Expired with no decision → see What happens on expiry.
- Approved →
{
"decision": true,
"context": {
"reason_code": "approved_by_human",
"approval_id": "apr-c3d4e5f6",
"access_path": "direct"
}
}
What happens on expiry
If nobody approves or denies before expires_in runs out, the pending request expires — it does not become a permanent denial. The next evaluation call for the same subject, resource, and action creates a fresh approval cycle, returning a new authorization_pending response with a new approval_id.
This means an unanswered HITL request is never a dead end. The caller can retry, and the human will be prompted again.
Approvals and denials are time-bounded, not permanent
When a human approves or denies a request, that decision is reused for a configurable window (default: one hour from the moment of decision). Within that window:
- A repeated call for the same subject, resource, and action gets the same outcome instantly — without prompting a human again.
- After the window expires, the next call creates a new pending request and the human is asked to decide again.
This applies equally to approvals and denials — neither is permanent. The window is configured per policy via approval_ttl_hours in the policy's HITL modifier settings.
The safety re-check on reuse
Before honoring a previously approved decision, Vengtoo re-checks a narrow set of safety invariants to confirm the approval is still valid to act on:
- Is the policy that triggered the approval still active?
- Does the subject still exist?
If a re-check fails, the approval is not honored and you receive a distinct reason code instead of approved_by_human:
| Reason code | Meaning |
|---|---|
policy_inactive | The policy that originally required approval has since been disabled |
subject_deleted | The subject no longer exists |
These appear distinctly in the audit log — "approved, then blocked by a safety re-check" is never confused with a standard policy denial.
Treating pending as deny
If your application cannot handle a waiting state, treat authorization_pending as a deny and surface a clear message. Never default to allowing access while a decision is pending.
When to use HITL
- An action is high-risk or irreversible (e.g. deleting production data, transferring funds).
- Compliance requires a human audit trail before sensitive access is granted.
- An AI agent is requesting access on behalf of a user and the action scope exceeds a pre-approved threshold.
- You want a hard boundary outside the model — a human gate that holds regardless of what an AI agent was told or convinced to attempt.
Related
- Access Paths — how evaluation works for standard requests.
- Delegation — granting an agent pre-approved access without requiring per-request human approval.
- Evaluation API — the full request and response shape.