> ## Documentation Index
> Fetch the complete documentation index at: https://praison.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Plan Approval Callback

> Async plan-approval callbacks that work from inside a running event loop

`ApprovalCallback` gates a plan before it runs, and its async `approve_fn` now works even when called from inside a running event loop — such as a FastAPI async route.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    subgraph "Plan Approval"
        A[📋 Plan] --> B[🛡️ ApprovalCallback]
        B --> C[🔀 Safe Async Bridge]
        C --> D[✅ Approved / Rejected]
    end

    classDef plan fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef cb fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef bridge fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef out fill:#10B981,stroke:#7C90A0,color:#fff

    class A plan
    class B cb
    class C bridge
    class D out
```

> [!NOTE]
>
> **Async approval callbacks now work from a running event loop.** As of [MervinPraison/PraisonAI#4533](https://github.com/MervinPraison/PraisonAI/pull/4533), `ApprovalCallback(approve_fn=<async fn>)` can be reached from inside a running loop — a FastAPI async route, a Textual app, a Jupyter cell — without crashing with `RuntimeError: Cannot run the event loop while another loop is running`. The callback routes through the package's shared safe bridge and preserves the caller's loop.
>
> **Update (PR #5377):** the async approval bridge now also **preserves the caller's `ContextVar`s** (trace / session / approval context is visible inside `approve_fn`) and **enforces the timeout** — a blocking approval that ignores cancellation cannot pin the caller past its deadline. See [Async Bridge](https://praison.ai/docs/features/async-bridge.md#async-guardrails-callbacks-and-approvals-inside-a-running-loop-pr-5377).

## Quick Start

**1. Attach an async approval function**

Pass an async `approve_fn` to `ApprovalCallback`. It returns `True` to approve, `False` to reject.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents.planning import ApprovalCallback

async def review_plan(plan) -> bool:
    # e.g. call a Slack / email / DB check
    return await my_async_approval_check(plan)

approval = ApprovalCallback(approve_fn=review_plan)
```

**2. Works inside a running loop**

The synchronous `__call__` can now be reached from inside a running loop without crashing — the async `approve_fn` runs on the caller's loop.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
import asyncio
from praisonaiagents.planning import ApprovalCallback, Plan

async def review_plan(plan) -> bool:
    return True  # your async check here

approval = ApprovalCallback(approve_fn=review_plan)

async def main():
    plan = Plan(name="Weekly report")
    # Reached from inside a running loop — no new-loop crash.
    approved = approval(plan)
    print("approved" if approved else "rejected")

asyncio.run(main())
```

***

## How It Works

The sync `__call__` detects an async `approve_fn` and forwards it through the package's shared safe bridge `run_coroutine_from_any_context`, which detects a running loop and schedules the coroutine on it instead of spinning up a fresh loop.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant Route as FastAPI Route
    participant Agent
    participant Callback as ApprovalCallback
    participant Bridge as run_coroutine_from_any_context

    Route->>Agent: await agent.astart(task)
    Agent->>Callback: __call__(plan)
    Callback->>Bridge: run(approve_fn(plan))
    Bridge->>Bridge: detects running loop
    Bridge-->>Callback: result
    Callback-->>Agent: bool
    Agent-->>Route: response
```

| Path | Behaviour |
| - | - |
| Sync `approve_fn` | Called directly and its result used |
| Async `approve_fn` | Routed through the shared safe bridge — runs on the caller's loop |
| No `approve_fn`, non-interactive | Auto-approves (CI / scripts) |
| No `approve_fn`, interactive TTY | Requires explicit approval |

***

## FastAPI async route

Embed PraisonAI in a FastAPI async endpoint and the plan-approval prompt no longer raises `RuntimeError` when the plan is presented.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from fastapi import FastAPI
from praisonaiagents import Agent, PlanningConfig
from praisonaiagents.planning import ApprovalCallback

app = FastAPI()

async def review_plan(plan) -> bool:
    # e.g. call a Slack / email / DB check
    return await my_async_approval_check(plan)

approval = ApprovalCallback(approve_fn=review_plan)

@app.post("/plan")
async def run_plan():
    agent = Agent(
        name="Planner",
        instructions="Solve step by step",
        planning=PlanningConfig(auto_approve=False),
    )
    # Works inside the route's running loop — no new-loop crash.
    return await agent.astart("Plan the weekly report")
```

### What the user experiences

* A user hits a FastAPI endpoint that runs `await agent.astart("...")`.
* The planning phase produces a plan; `ApprovalCallback` is invoked.
* The user-supplied async `approve_fn` runs on the caller's loop (no new-loop crash).
* The plan proceeds if approved, or halts if the callback returns `False`.

> [!WARNING]
>
> Before PR #4533 the sync `__call__` hand-rolled `asyncio.new_event_loop()` + `run_until_complete()`, which doesn't dodge CPython's thread-global running-loop flag — so reaching it from a running loop crashed with `RuntimeError: Cannot run the event loop while another loop is running`. The dedicated async entry point `async_call` was already correct.

***

## Common Patterns

> [tab] Async approval
>
> Route approval through any async check — a database lookup, an HTTP call, a message-queue round-trip.
>
> ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
> from praisonaiagents.planning import ApprovalCallback
>
> async def review_plan(plan) -> bool:
>     return await my_async_approval_check(plan)
>
> approval = ApprovalCallback(approve_fn=review_plan)
> ```

> [tab] Sync approval
>
> A plain sync function still works unchanged.
>
> ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
> from praisonaiagents.planning import ApprovalCallback
>
> def review_plan(plan) -> bool:
>     return len(plan.steps) <= 5
>
> approval = ApprovalCallback(approve_fn=review_plan)
> ```

> [tab] Auto-approve
>
> Skip prompts entirely for trusted, unattended runs.
>
> ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
> from praisonaiagents.planning import ApprovalCallback
>
> approval = ApprovalCallback(auto_approve=True)
> ```

***

## Best Practices

> [accordion] Prefer an async approve\_fn for I/O-bound checks
>
> When your approval decision calls out to Slack, a database, or an HTTP endpoint, use an async `approve_fn` so it runs on the caller's loop without blocking — safe from a FastAPI route as of PR #4533.

> [accordion] Return a plain bool from approve\_fn
>
> `approve_fn` must return `True` to approve or `False` to reject. Keep the decision logic inside the function and let `ApprovalCallback` handle the plan's `approve()` transition.

> [accordion] Use auto\_approve only for unattended runs
>
> `ApprovalCallback(auto_approve=True)` skips all checks — reserve it for trusted CI or scripted runs where no human is available to review.

***

## Related

> [card] [Planning Mode](https://praison.ai/docs/features/planning-mode.md)
>
> Configure planning with PlanningConfig

> [card] [Planning Concepts](https://praison.ai/docs/concepts/planning.md)
>
> Core planning architecture and the ApprovalCallback denial snippet


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.