Skip to main content

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
}
}
FieldMeaning
reason_codeauthorization_pending: a human approval is required
approval_id / auth_req_idID of the approval request. Use this to track or display the pending request
expires_inSeconds until this pending request expires. If nobody decides in time, the next evaluation call starts a fresh approval cycle (see What happens on expiry)
intervalSuggested polling interval in seconds. Poll no faster than this (see Manual polling)

If you're using the Go or Node.js Vengtoo SDK, use evaluateWithApproval() instead of calling evaluate directly. It handles the entire pending → poll → resolve loop for you:

// Node.js
const result = await vengtoo.evaluateWithApproval(req, {
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.EvaluateWithApproval(ctx, req, &vengtoo.ApprovalOptions{
Timeout: 5 * time.Minute,
OnPending: func(authReqID string, expiresIn int) {
log.Printf("waiting for approval: %s", authReqID)
},
})

evaluateWithApproval():

  • Calls onPending exactly once, when the request first enters the pending state, not on every poll.
  • Polls at the server's suggested interval, backing off automatically on slow_down without 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

  1. Detect the pending state: check for decision: false and reason_code: authorization_pending.
  2. Show a waiting state to the user: the action is blocked until a human decides.
  3. Poll the evaluate endpoint at the interval cadence, passing the same request. Vengtoo returns the pending state on each poll until a human acts or the request expires.
  4. Handle slow_down: if you poll faster than the server expects, you receive reason_code: "slow_down". Back off and retry; don't surface this to the end user, it's a rate signal not a decision.
  5. 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.
{
"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_seconds 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 codeMeaning
policy_inactiveThe policy that originally required approval has since been disabled
subject_deletedThe 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

Never allow access while a decision is pending

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.
  • 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.