Skip to main content

Human approval

When a policy decides an action needs human review, ProofRail pauses execution, notifies an approver, and resumes only when a human makes a decision. This guide walks through the full flow so you know what your code sees, what your approvers see, and how to configure timeouts and fallbacks.

When approval gets triggered

Approval is one of four policy outcomes (see Policies). It fires when:
  • A single transaction exceeds the configured medium-financial threshold ($5,000 by default)
  • Cumulative chain financial exposure exceeds your threshold ($10,000 by default)
  • An agent makes its first external communication
  • A bulk operation touches 100+ records, or a bulk deletion touches 10+ items
  • An action is tagged irreversible (contract signing, public posts, legal filings)
  • A high-risk agent executes any action
  • An agent accesses resources outside its declared scope
  • Content containing PII is being sent to an external domain
  • A custom policy you’ve defined matches the action
Each trigger is configurable. See Configuration Reference for the full list of thresholds.

What your code sees

When approval is triggered, the SDK blocks until a decision arrives. Your record_agent_action call (or the wrapped framework call) waits:
async with proofrail.Chain("vendor-purchase") as chain:
    # This call may block for minutes or hours, waiting for the approver
    decision = await chain.record_agent_action(
        agent_name="commitment-agent",
        action_type="tool_call",
        action_name="record_purchase",
        payload={"amount_usd": 7500},
    )
    # On approval: execution continues here.
    #   `decision.policy_decision` will be "allow"
    #   `decision.decision_source` will be "human_approval"
    # On denial or timeout: ActionDeniedError is raised
The SDK polls the backend for the approval decision every 5 seconds for the entire duration of the wait, up to the configured timeout. The polling is asynchronous — your code is blocked at the record_agent_action call, but the event loop is free to handle other coroutines while waiting.

What the approver sees

When approval is triggered, an email goes to your configured approvers via Resend. The email contains:
  • Chain name and ID
  • Triggering action (which agent, what it tried to do)
  • The cumulative metrics for the chain so far (financial exposure, external comms, records modified, etc.)
  • Signed one-click approve and deny links
Clicking the link takes the approver through Clerk authentication, then to the approval page at /dashboard/approvals where they see:
  • The full chain history — every action taken so far
  • The triggering action highlighted with policy reasoning
  • A clear summary of what executing the action will do
  • Approve and Deny buttons, with an optional notes field
  • A “Grant time-boxed exception” checkbox with a duration selector (1 hour, 1 day, 1 week, 1 month) — see Time-boxed exceptions below
Once they decide, the SDK’s next poll picks up the result and unblocks your code.

Configuring approvers

Approvers are configured in your dashboard under /dashboard/settings/team. Any user with the approver role receives approval emails by default. Currently the SDK applies the same approver list and timeout to every chain in your organization — there is no per-chain approver override via Chain(metadata={...}). If you need different approvers for different workflows, two options:
  1. Define custom policies in the dashboard that match on chain name and route to different approver groups
  2. Initialize the SDK separately per workflow boundary with the appropriate config (this means separate processes or separate init() calls, which has trade-offs)

Timeouts

By default, approvals expire after 24 hours. If no decision arrives in that window, the approval is marked timed-out and the SDK raises ActionDeniedError with a timeout condition. You can configure the default timeout globally:
proofrail.init(
    api_key="prail_...",
    default_approval_timeout_hours=24,
)
For workflows where you’d rather have a human approval block forever than auto-deny, set a long timeout (168 for 7 days). For workflows where you need quick decisions, set a short one (0.25 for 15 minutes).

Fallback approvers

If your primary approvers don’t respond within half the configured timeout, the approval escalates to a fallback list:
proofrail.init(
    api_key="prail_...",
    default_approval_timeout_hours=24,
    fallback_approvers=["lead@company.com", "cto@company.com"],
)
With a 24-hour timeout, fallback emails fire at the 12-hour mark. Both primary and fallback approvers can still approve until expiration. This pattern is useful when your primary approver is on vacation, in different time zones, or otherwise unreachable. The fallback chain gives you operational reliability without making approval gates a single point of failure.

Time-boxed exceptions

When approving an action, the approver can grant an exception that covers future similar actions for a chosen duration: 1 hour, 1 day, 1 week, or 1 month. For example: an approver approves email-agent sending to vendor.com for the first time. Without an exception, the next email to vendor.com triggers approval again. With a “1 week” exception, future emails to vendor.com from email-agent proceed without approval for the next 7 days. Exceptions are scoped — they cover the specific combination of conditions that triggered the original approval (agent, action type, destination domain), not “anything goes” for that agent. When an exception applies, the action is treated as allow_with_flag rather than require_approval. The audit trail records which exception authorized which action. Exceptions expire automatically at the configured duration. Once expired, the next matching action will trigger approval again normally.
Early revocation of an active exception is not yet exposed in the dashboard. If you need to revoke an exception before it expires, contact us — we can revoke directly via the backend. A self-service revocation UI is on the roadmap.

Polling behavior

The SDK polls GET /v1/chains/{chain_id}/approval-status (or the chain’s status endpoint) every 5 seconds while waiting for an approval to resolve. The interval is constant for the entire wait window — there is no tiered cadence and no exponential backoff in the current version. If the backend is unreachable during polling, the SDK retries on the next 5-second tick. Backend transient errors don’t fail the approval — they just delay polling. This polling is asynchronous: your application code awaits the record_agent_action call, but the event loop continues running other coroutines, handling other requests, etc. while waiting.

Handling denial

If the approver clicks Deny or the approval times out, record_agent_action raises:
from proofrail.exceptions import ActionDeniedError

try:
    await chain.record_agent_action(...)
except ActionDeniedError as exc:
    if exc.decision_source == "human_approval":
        if exc.condition and "not resolved within" in exc.condition:
            # No reviewer responded in time
            notify_user("No reviewer responded — please retry tomorrow")
        else:
            # Reviewer explicitly denied with notes in `condition`
            notify_user(f"Action denied by reviewer: {exc.condition}")
    else:
        # Policy denial, not human (rare here — usually means a policy
        # caught the action before it ever reached the approver)
        notify_user(f"Policy denied: {exc.policy_name}")
The two attributes to check are decision_source (where the decision came from) and condition (what the decision said). See Exceptions reference for the full handling pattern. Your application decides how to handle this. Some options:
  • Show the user a “request reviewed and denied” message
  • Trigger an escalation workflow (page a human, retry tomorrow)
  • Log the denial and move on with the next workflow
The chain itself isn’t terminated by a single deny — you can choose to catch the exception and continue with other actions in the chain. Once the chain context exits (either normally or via raised exception), the chain is sealed and the receipt is generated.

Approval and receipts

Every approval decision is recorded in the chain receipt. The receipt’s approval history captures:
  • Who approved or denied
  • The decision and any notes the reviewer left
  • Timestamp of the decision
  • Whether the approval created a time-boxed exception
This means: months later, you can verify exactly which human approved which action, with their notes, included in the receipt’s HMAC-signed payload. See Audit receipts for verification details.

Where to go next

Policies

What triggers approval in the first place.

Audit receipts

How approvals get cryptographically recorded.

Kill switch

Halting all agent activity in an emergency.

Configuration

Tune approval thresholds and timeouts.