Skip to main content

Exceptions

All exceptions live in proofrail.exceptions. The base class is ProofRailPolicyError; catching it catches every policy-related exception the SDK raises. A few exceptions extend Exception directly when they’re not policy decisions (timeouts, backend connectivity, auto-pause).
ProofRailPolicyError                # base for policy-driven exceptions
├── ActionDeniedError               # policy denied an action or human reviewer denied it
├── PolicyViolationError            # a policy condition was violated outside the normal evaluation path
└── ProofRailKillSwitchError        # org-wide kill switch denied the action

Exception
├── BackendUnavailableError         # the backend is unreachable and fail-mode is deny
├── ChainTimeoutError               # the chain ran past its configured timeout
└── ChainAutoPausedError            # an auto-pause rule (event count, duration, token budget) fired
Catch ProofRailPolicyError when you want to handle every policy-driven denial uniformly:
import proofrail
from proofrail.exceptions import ProofRailPolicyError

try:
    async with proofrail.Chain("workflow") as chain:
        await chain.record_agent_action(...)
except ProofRailPolicyError as exc:
    logger.error(f"Policy denied the action: {exc}")
    fallback_workflow()
For operational issues (backend down, chain timed out, runaway behavior auto-paused), catch the relevant Exception subclass instead.

ActionDeniedError

Raised when a policy denies an action, when a human reviewer denies an approval, or when an approval times out without a decision. Also raised when fail_mode="deny" is active and the backend is unreachable for that action class.
class ActionDeniedError(ProofRailPolicyError):
    message: str
    policy_name: str | None
    condition: str | None
    chain_context: dict | None
    remediation: str | None
    docs_url: str | None
    decision_source: str | None

Attributes

AttributeDescription
messageHuman-readable summary of why the action was denied.
policy_nameWhich policy made the decision (default policy name or custom policy name).
conditionWhat about the action triggered the denial — the policy’s decision_reason, the approver’s notes, or a timeout message like "Approval was not resolved within the configured timeout window."
chain_contextDict containing chain-level context. Includes chain_id and other state at the point of denial.
remediationSuggested fix (e.g., "update cumulative_financial_threshold_usd in init() or approve via dashboard").
docs_urlLink to the relevant docs page for this kind of denial.
decision_sourceWhere the decision came from: "backend_evaluation", "local_fast_path", "human_approval", or "offline_stub".

Distinguishing denial reasons

ActionDeniedError doesn’t have a single reason enum. To tell what kind of denial occurred, check decision_source and condition together:
Scenariodecision_sourcecondition
Policy denied at the backend"backend_evaluation"Policy’s decision_reason
Policy denied locally via fast-path"local_fast_path"Policy’s decision_reason
Human reviewer clicked Deny"human_approval"The reviewer’s notes
Approval expired without a decision"human_approval""Approval was not resolved within the configured timeout window."
Backend unreachable and fail_mode="deny""offline_stub"The fail-mode reason

Example

from proofrail.exceptions import ActionDeniedError

try:
    await chain.record_agent_action(
        agent_name="commitment-agent",
        action_type="tool_call",
        action_name="record_purchase",
        payload={"amount_usd": 75000},
    )
except ActionDeniedError as exc:
    print(f"Policy: {exc.policy_name}")
    print(f"Condition: {exc.condition}")
    print(f"What to do: {exc.remediation}")
    print(f"Docs: {exc.docs_url}")
    print(f"Chain context: {exc.chain_context}")

    if exc.decision_source == "human_approval":
        if exc.condition and "not resolved within" in exc.condition:
            # No reviewer responded — retry tomorrow
            schedule_retry()
        else:
            # Reviewer explicitly denied with notes in `condition`
            chain_id = (exc.chain_context or {}).get("chain_id")
            escalate_to_manual_review(chain_id)
    else:
        # Policy denial (backend, local fast-path, or offline stub)
        log_policy_denial(exc)

Don’t suppress and continue

Suppressing ActionDeniedError and retrying the same action accomplishes nothing — the policy will deny it again. If you catch it, either:
  1. Change the situation (lower amount, different recipient, different agent) and retry
  2. Escalate the work to a human
  3. Log the denial and continue with the next part of your workflow
The message, remediation, and docs_url fields are built to be shown to your application’s user — they tell a human reading logs what to do.

PolicyViolationError

Raised when a policy condition is violated outside the normal action-decision path — for example, when chain-level cumulative limits are checked at chain start or during background reconciliation rather than at an individual action evaluation. Inherits from ProofRailPolicyError. In practice, most application code can catch ProofRailPolicyError to handle this and ActionDeniedError uniformly.

ProofRailKillSwitchError

Raised when the organization’s kill switch is active and an action is attempted.
class ProofRailKillSwitchError(ProofRailPolicyError):
    message: str
    organization_id: str | None
    reason: str | None

When raised

Whenever the kill switch is on for your org and record_agent_action is called. This is distinct from a regular ActionDeniedError so applications can show a different message (maintenance mode vs. policy violation).

Attributes

AttributeDescription
messageDefault: "All agent actions are denied: organisation kill switch is active".
organization_idThe org the kill switch applies to.
reasonThe reason the admin gave when activating the kill switch.

Example

from proofrail.exceptions import ProofRailKillSwitchError

try:
    async with proofrail.Chain("workflow") as chain:
        await chain.record_agent_action(...)
except ProofRailKillSwitchError as exc:
    return {
        "status": "maintenance",
        "message": "Agent activity is temporarily paused.",
        "reason": exc.reason,
    }
See Kill switch for activation and resumption.

BackendUnavailableError

Raised when the SDK can’t reach the backend within backend_timeout_seconds AND the fail_mode for that action class is "deny". Inherits from Exception, not ProofRailPolicyError — it’s an operational issue, not a policy decision. If fail_mode is "allow" for the action class, the SDK does NOT raise — it proceeds locally and buffers the event for later replay.

Example

from proofrail.exceptions import BackendUnavailableError

try:
    await chain.record_agent_action(...)
except BackendUnavailableError as exc:
    logger.warning(f"ProofRail backend unreachable: {exc}")
    # The action did NOT execute. Decide whether to retry, defer, or proceed.
    queue_for_retry()

ChainTimeoutError

Raised when a chain exceeds its configured runtime without completing. Inherits from Exception. Long-running batch workflows should configure a longer timeout via SDK config. The exception is meant to catch chains that have hung — not chains that are doing legitimate long work.

ChainAutoPausedError

Raised when a chain hits one of the auto-pause limits — event count, chain duration, or token budget. Inherits from Exception. See Policies for the default auto-pause thresholds.

Example

from proofrail.exceptions import ChainAutoPausedError

try:
    await chain.record_agent_action(...)
except ChainAutoPausedError as exc:
    # The chain has stopped to prevent runaway behavior
    logger.error(f"Chain auto-paused: {exc}")
    investigate_chain(chain.id)

Handling patterns

Catch all policy denials, distinguish by source

from proofrail.exceptions import (
    ProofRailPolicyError,
    ActionDeniedError,
    ProofRailKillSwitchError,
    BackendUnavailableError,
)

try:
    async with proofrail.Chain("workflow") as chain:
        await chain.record_agent_action(...)
except ProofRailKillSwitchError as exc:
    show_maintenance_mode(exc)
except ActionDeniedError as exc:
    handle_denial(exc)
except BackendUnavailableError as exc:
    queue_for_retry()
except ProofRailPolicyError as exc:
    # Catch-all for any other policy issues
    log_and_fail_gracefully(exc)

Distinguish human denial from timeout

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 — retry tomorrow
            queue_for_retry(chain.id)
        else:
            # Reviewer explicitly denied
            notify_workflow_owner(exc)
    else:
        # Policy denial (not human)
        log_policy_block(exc)

Log denials for analytics

try:
    await chain.record_agent_action(...)
except ActionDeniedError as exc:
    metrics.increment(
        "proofrail.denial",
        tags={
            "policy": exc.policy_name or "unknown",
            "source": exc.decision_source or "unknown",
        },
    )
    raise  # re-raise after logging
Tracking which policies deny most often is a strong signal for what to revisit — either the policy is wrong, the workflow is wrong, or the thresholds need adjusting.

Don’t catch what you can’t fix

ProofRailKillSwitchError and ChainAutoPausedError shouldn’t be caught and worked around in normal application code:
  • ProofRailKillSwitchError is an admin saying “stop everything”. Honor it; don’t bypass.
  • ChainAutoPausedError means runaway behavior was detected. Stop and investigate; don’t retry blindly.
ActionDeniedError is the exception you’d routinely catch in application logic. BackendUnavailableError is the one to catch when you need explicit retry or queue handling.

Where to go next

SDK API

Type signatures for everything else.

Configuration

The init() options that drive these errors.

Human approval

Where approval-related denials come from.

Kill switch

Activating, resuming, and handling in code.