Agent Run Outcomes
Typed, exhaustively-matchable results for agent runs, validation, and handoffs
Run outcomes give every agent execution a typed status so your code can handle success, failure, timeout, cancellation, and invalid output exhaustively — no string matching.
Python
from praisonaiagents import Agent, AgentRunOutcome
agent = Agent(name="Research Agent", instructions="Research and report clearly")
outcome = agent.start("Summarise agent run outcomes")
assert isinstance(outcome, AgentRunOutcome)TypeScript
import { AgentRunOutcome } from 'praisonai';
const outcome = AgentRunOutcome.success('Summarised agent run outcomes');
console.log(outcome.status); // 'success'
console.log(outcome.isSuccess()); // true
const failed = AgentRunOutcome.failure('validation failed');
console.log(failed.status); // 'failure'The user handles every finish state in code; the agent returns a typed AgentRunOutcome instead of an ambiguous string.
Quick Start
- Simple Usage
Every agent execution returns an
AgentRunOutcomethat you can check for success.python from praisonaiagents import Agent, AgentRunOutcome agent = Agent( name="Research Agent", instructions="Research the given topic thoroughly" ) # Agent executes and produces an outcome result = agent.start("Research renewable energy") # Check the outcome if result.outcome.is_success(): print(f"Success: {result.outcome.output}") else: print(f"Failed: {result.outcome.error}") - Exhaustive Matching
Match all five status types exhaustively to handle every possible outcome.
python from praisonaiagents import Agent, AgentRunOutcome agent = Agent(name="Validator") result = agent.start("Validate this data") # Handle all possible outcomes if result.outcome.status == "success": process_success(result.outcome.output) elif result.outcome.status == "timeout": retry_with_longer_timeout(result) elif result.outcome.status == "invalid_output": fix_validation_and_retry(result) elif result.outcome.status == "cancelled": handle_cancellation(result) elif result.outcome.status == "failure": log_permanent_error(result.outcome.error) - Validation Routing
Access typed outcomes from task validation instead of parsing strings.
python from praisonaiagents import Agent, Task, PraisonAIAgents # Setup workflow with validation workflow = PraisonAIAgents( agents=[validator_agent, executor_agent], tasks=[ Task(description="Validate input", task_type="decision"), Task(description="Execute if valid") ] ) result = workflow.kickoff() # Check validation outcome validation_task = result.tasks[0] if validation_task.validation_outcome.status == "invalid_output": print(f"Validation failed: {validation_task.validation_outcome.error}") retry_with_fixes() else: proceed_to_execution()
How It Works
Every agent execution produces an AgentRunOutcome with a typed status that enables exhaustive pattern matching:
| Status | Meaning | Retryable | When It Occurs |
|---|---|---|---|
success | Completed successfully | No | Agent produced valid output |
failure | Non-retryable logic error | No | Unhandled exception, logic error |
timeout | Operation exceeded time limit | Yes | Slow LLM, network hang |
cancelled | External cancel signal | No | User Ctrl-C, parent cancelled child |
invalid_output | Output didn’t pass validation | Yes | Wrong format, validator rejected |
The Five Statuses
| Status | Meaning | Retryable | Typical Cause |
|---|---|---|---|
success | Completed successfully | No | Agent produced valid output |
failure | Non-retryable logic error | No | Unhandled exception, validation logic error |
timeout | Operation exceeded time limit | Yes | Slow LLM, slow tool, network hang |
cancelled | External cancel signal | No | User Ctrl-C, parent agent cancelled child |
invalid_output | Output didn’t pass validation | Yes | Wrong format, missing fields, validator rejected |
Creating Outcomes
Create outcomes using factory methods — each sets appropriate defaults automatically.
from praisonaiagents import AgentRunOutcome
# Success with output
outcome = AgentRunOutcome.success(
output="Task completed successfully",
elapsed_s=2.5,
agent_name="Research Agent"
)
# General failure
outcome = AgentRunOutcome.failure(
error="Unexpected error occurred",
error_category="execution",
agent_name="Analysis Agent"
)
# Timeout - auto-sets error_category="timeout"
outcome = AgentRunOutcome.timeout(
error="Agent timed out after 30s",
elapsed_s=30.0
)
# Cancelled - auto-sets error_category="cancelled"
outcome = AgentRunOutcome.cancelled(
error="User cancelled the operation"
)
# Invalid output - auto-sets error_category="validation"
outcome = AgentRunOutcome.invalid_output(
error="Output format is invalid"
)Retry Decisions
Use is_retryable() to determine if an operation can be retried safely.
from praisonaiagents import Agent
agent = Agent(name="Processor")
for attempt in range(3):
result = agent.start("Process data")
if result.outcome.is_success():
break
elif result.outcome.is_retryable():
print(f"Attempt {attempt + 1} failed: {result.outcome.error}")
continue
else:
print(f"Permanent failure: {result.outcome.error}")
breakReading the Outcome From a Task
After task validation routing, check both the new typed outcome and legacy feedback.
from praisonaiagents import Task, PraisonAIAgents
# Task with decision routing
validation_task = Task(
description="Validate the user input",
task_type="decision"
)
workflow = PraisonAIAgents(
agents=[validator_agent],
tasks=[validation_task]
)
result = workflow.kickoff()
# Access typed outcome (recommended)
outcome = validation_task.validation_outcome
if outcome.status == "invalid_output":
print(f"Validation failed: {outcome.error}")
# Legacy dict still available for backward compatibility
feedback = validation_task.validation_feedback
print(f"Legacy status: {feedback['status']}")HandoffResult Integration
Handoff results now include typed outcomes automatically derived from legacy fields.
from praisonaiagents import Agent, handoff
billing_agent = Agent(name="Billing Agent")
support_agent = Agent(
name="Support Agent",
handoffs=[billing_agent]
)
# Handoff to billing agent
result = support_agent.handoff_to(billing_agent, "Handle billing issue")
# Check typed outcome
if result.outcome.status == "timeout":
print(f"Handoff timed out: {result.outcome.error}")
retry_handoff()
elif result.outcome.is_success():
print(f"Handoff successful: {result.outcome.output}")Migration from String-Based Validation
Replace string parsing with typed status matching for safer code.
Before:
# String parsing - error-prone
if decision_str in ["invalid", "retry", "failed", "errors"]:
handle_validation_failure()
elif decision_str in ["success", "valid", "approved"]:
proceed_with_task()After:
# Typed matching - exhaustive and safe
if outcome.status == "invalid_output":
handle_validation_failure()
elif outcome.status == "success":
proceed_with_task()For gradual migration, use the compatibility helper:
from praisonaiagents import validate_decision_string
# Convert legacy string to typed status
status = validate_decision_string(legacy_decision_str)
if status == "invalid_output":
handle_invalid_output()Process normalises its routing key with .lower().strip() — the same normalisation validate_decision_string applies at praisonaiagents/run_outcome.py:303. Before the fix in MervinPraison/PraisonAI#4260, the two disagreed on a whitespace-padded verdict like "invalid\n"; now they agree, so the typed outcome is reliably stored even when the model pads its verdict.
Decision Flow
Best Practices
Prefer status matching over string parsing
Always match against the typed status field instead of parsing error messages or legacy strings.
# Good - exhaustive type-safe matching
if outcome.status == "invalid_output":
retry_with_fixes()
# Bad - brittle string parsing
if "invalid" in str(outcome.error):
retry_with_fixes()Use `is_retryable()` to drive retry logic
Let the outcome determine retry behavior instead of hardcoding status lists.
# Good - use built-in retry detection
if outcome.is_retryable():
schedule_retry()
# Bad - maintain your own retry logic
if outcome.status in ["timeout", "invalid_output"]:
schedule_retry()Put structured data in `context`, not in `error`
Use the context field for machine-readable metadata, keep error human-readable.
# Good - structured context
outcome = AgentRunOutcome.failure(
error="Database connection failed",
context={
"database_host": "db.example.com",
"timeout_seconds": 30,
"retry_count": 3
}
)
# Bad - structured data in error message
outcome = AgentRunOutcome.failure(
error="Database connection failed: host=db.example.com, timeout=30s"
)Always handle all five statuses
Exhaustive matching prevents bugs from unhandled cases. Avoid fall-through else clauses.
# Good - explicit handling
if outcome.status == "success":
handle_success(outcome.output)
elif outcome.status == "timeout":
handle_timeout()
elif outcome.status == "invalid_output":
handle_validation_error()
elif outcome.status == "cancelled":
handle_cancellation()
elif outcome.status == "failure":
handle_permanent_error()
# No else clause needed - all cases covered
# Bad - fall-through else
if outcome.status == "success":
handle_success()
else:
# What exactly happened? Unknown!
handle_generic_error()CLI Mapping (praisonai run)
The praisonai run CLI wraps the SDK outcome into a success / truncated / failure exit signal for pipelines: any falsy or empty result (None, "", all-whitespace) exits 1 with a machine-readable run_failed payload, unless the core recorded a provider block/refusal/truncation — in which case the specific reason wins and exits 2 with the reason as status. A non-empty result that hit the step limit exits 2 with a truncated payload; any other non-empty result exits 0 with the existing text/JSON output.
| SDK outcome | CLI exit code | --output json status |
|---|---|---|
success (non-empty output, agent.last_stop_reason != "max_steps") | 0 | absent (success payload) |
success (non-empty output, agent.last_stop_reason == "max_steps") — a step-limit-truncated finalisation summary | 2 | "truncated" (result preserved as the summary text) |
content_filtered (agent.last_stop_reason == "content_filtered") | 2 | "content_filtered" (result preserves any partial text) |
refused (agent.last_stop_reason == "refused") | 2 | "refused" (result preserves any partial text) |
length_truncated (agent.last_stop_reason == "length_truncated") | 2 | "length_truncated" (result preserves any partial text). Surfaced only after the auto-continuation cap is exhausted — see Output Continuation. |
failure / timeout / cancelled / invalid_output (falsy or empty result surfaced to Agent.start / run) | 1 | "failed" (with code: "run_failed") |
agent.last_stop_reason is one of "completed", "max_steps", "content_filtered", "refused", "length_truncated", "timeout", or "error". "timeout" means the turn was cut by ExecutionConfig.max_execution_time or a run_autonomous(timeout_seconds=...) deadline (see Execution Time Caps); the CLI has no distinct exit code for it, so a non-empty partial result exits 0.
See praisonai run → Exit Codes for the full CLI contract, the failure and truncated payload schemas, and CI usage examples.