Skip to main content
Last updated on

Error Handling

Trust decisions surface as Temporal ApplicationError exceptions in your activities. The SDK uses ApplicationError.type to distinguish between different governance outcomes.

Governance Error Types

The SDK raises ApplicationError with one of these type strings:

Error TypeDecisionRetryableDescription
"GovernanceStop"BLOCK or HALTNoOperation blocked
"ApprovalPending"REQUIRE_APPROVALYesAwaiting human review
"ApprovalRejected"REQUIRE_APPROVAL (rejected)NoHuman rejected request
"ApprovalExpired"REQUIRE_APPROVAL (timeout)NoNo response before timeout

All governance errors are standard Temporal ApplicationError instances with these properties:

PropertyTypeDescription
messagestrHuman-readable description (e.g., "Governance blocked: PII detected")
typestrThe governance type string from the table above
non_retryableboolIf True, Temporal will not retry the activity

Import

from temporalio.exceptions import ApplicationError

Handling Each Type

These patterns apply inside your existing Temporal activity functions. The SDK intercepts activity execution automatically — you only need to add error handling if you want custom behavior beyond the default (which is to let the exception propagate and fail the activity).

GovernanceStop

Raised when an operation is blocked (BLOCK) or the agent session is terminated (HALT).

@activity.defn
async def sensitive_operation(data: dict) -> str:
try:
result = await perform_action(data)
return result
except ApplicationError as e:
if e.type == "GovernanceStop":
logger.error(f"Operation blocked: {e.message}")

# Option 1: Raise to fail the activity
raise

# Option 2: Return alternative result
return "Operation not permitted"
raise

ApprovalPending

Raised when the operation requires human approval. Because non_retryable=False, Temporal automatically retries the activity — the SDK polls for an approval decision on each retry.

@activity.defn
async def requires_approval_operation(data: dict) -> str:
# No special handling needed - SDK manages retries.
# Activity will retry until approved/rejected/expired.
result = await perform_action(data)
return result

If you need custom handling:

@activity.defn
async def custom_approval_handling(data: dict) -> str:
try:
result = await perform_action(data)
return result
except ApplicationError as e:
if e.type == "ApprovalPending":
logger.info(f"Awaiting approval: {e.message}")
# Re-raise to trigger retry
raise
raise

ApprovalRejected

Raised when a human rejects the approval request.

@activity.defn
async def handle_rejection(data: dict) -> str:
try:
result = await perform_action(data)
return result
except ApplicationError as e:
if e.type == "ApprovalRejected":
logger.warning(f"Approval rejected: {e.message}")

# Option 1: Fail the activity
raise

# Option 2: Handle gracefully
return f"Operation rejected: {e.message}"
raise

ApprovalExpired

Raised when approval times out without a decision.

@activity.defn
async def handle_timeout(data: dict) -> str:
try:
result = await perform_action(data)
return result
except ApplicationError as e:
if e.type == "ApprovalExpired":
logger.warning(f"Approval timed out: {e.message}")
raise
raise

Workflow-Level Handling

For workflow-level trust handling, catch ApplicationError and check the type:

@workflow.defn
class MyAgentWorkflow:
@workflow.run
async def run(self, input: WorkflowInput) -> WorkflowOutput:
try:
result = await workflow.execute_activity(
sensitive_operation,
input.data,
start_to_close_timeout=timedelta(minutes=10),
)
return WorkflowOutput(result=result)

except ApplicationError as e:
if e.type == "GovernanceStop":
# Workflow is being blocked or terminated
await self.cleanup()
return WorkflowOutput(error=e.message)

if e.type == "ApprovalRejected":
# Human rejected - may want different handling
return WorkflowOutput(
status="rejected",
reason=e.message,
)

raise

Best Practices

  1. Let ApprovalPending propagate - The SDK handles retries
  2. Log GovernanceStop with context - Helps debugging
  3. Consider fallback behavior - Not all denials should crash
  4. Clean up on GovernanceStop - Release resources before re-raising
  5. Don't catch and ignore - These exceptions are intentional

Configuration Exceptions

The SDK raises configuration exceptions from openbox.config during create_openbox_worker() calls — not during activity execution. Handle these where you initialize your worker.

ExceptionCause
OpenBoxConfigErrorBase class for all configuration errors
OpenBoxAuthErrorInvalid or missing API key
OpenBoxNetworkErrorCannot reach OpenBox Core
OpenBoxInsecureURLErrorHTTP used for a non-localhost URL

Next Steps

Now that you understand how to handle trust decisions in code:

  1. Event Types - Understand the semantic event types that trigger these decisions
  2. Troubleshooting - Common issues and solutions
  3. Handle Approvals - Review and process HITL requests in the dashboard