Skip to main content
Hook events are triggered at specific points in the agent lifecycle, allowing you to intercept, modify, or block operations.
This page covers in-process lifecycle hooks (SETUP, SESSION_START, BEFORE_AGENT, etc.) that fire inside a running agent. For HTTP inbound triggers that start agent runs from external services via POST /hooks/<path>, see Gateway Inbound Hooks.
Hooks that rewrite the payload via modified_input (before_llm, before_tool_definitions, before_tool) must be registered with sequential=True. See Sequential vs Parallel Hooks.
The user sends a prompt; hooks fire at each lifecycle event as the agent runs.

How It Works

Lifecycle Events

Quick Start

1

Import Hook Components

2

Register a Hook

3

Use with Agent


Core Events

Tool Events

BEFORE_TOOL and AFTER_TOOL fire on both sync (chat) and async (achat) paths. A blocking BEFORE_TOOL hook returning HookResult.block(reason=...) prevents tool execution on either path — use it for security gates without worrying which entrypoint the caller uses.
AFTER_TOOL can rewrite or block the tool result (PraisonAI PRs #3968 / #3969). Mutate event_data.tool_output in place to rewrite/redact the value the model sees, or return HookResult.block(reason) to suppress it. Applies on both sync (chat()) and async (achat()) paths. Prefer this seam over rewriting inside the tool body when the goal is a cross-cutting scrub or policy gate. See Redact / Block Tool Output.
BEFORE_TOOL fails closed (PR #3855). A BEFORE_TOOL hook that raises an exception or times out now returns HookResult(decision="deny", reason=...) — the tool does not run. A security gate can no longer silently fail open because of a bug in the hook body.
AFTER_TOOL result may be an error payload. If the tool raised, event_data.tool_output is {"error": "<message>"}; if it returned a non-JSON-serializable value (datetime, set, bytes, a custom class), it is {"result": "<str(value)>"}. The run continues either way (PR #3855), so AFTER_TOOL handlers should tolerate both shapes.
AFTER_TOOL returns can rewrite or block (PR #3969). Returning a value from an AFTER_TOOL hook replaces event_data.tool_output before the model sees it — use it to redact secrets or PII from a tool result. Raising GuardrailBlocked prevents the result from reaching the model, mirroring BEFORE_TOOL. Both apply on the sync and async tool-execution paths. Before PR #3969 the return value was silently discarded, so the hook was observe-only. See the Redact Secrets from Tool Output pattern.

Tool Definition Events

BEFORE_TOOL_DEFINITIONS lets you shape the tool list the LLM actually sees — without changing the agent’s permanent tool registration. Mutate tool_definitions in place; the runtime only adopts in-place mutations.
Mutate in place using event_data.tool_definitions[:] = [...]. Reassigning the local name (event_data.tool_definitions = [...]) is silently ignored by the runtime.

BeforeToolDefinitionsInput Fields

Per-model filtering using the model field:

Agent Events

LLM Events

Sync/async parity — since PraisonAI PR #3908. BEFORE_LLM and AFTER_LLM fire on both sync (chat / start) and async (achat / astart) paths. A blocking BEFORE_LLM hook returning HookResult(decision="deny", reason=...) short-circuits the LLM call on either path and the agent returns "[LLM request blocked by hook: <reason>]". Earlier releases silently skipped these hooks on achat(), so features registered on BEFORE_LLM (e.g. enable_pii_redaction()) had no effect on async workflows.

MODEL_FALLBACK — silent model switch became observable

Fires the moment the runtime swaps the primary model for the next entry in fallback_models after a retryable failure, so an otherwise silent quality/cost degradation becomes an observable state transition.
Observe-only. Notification only — the turn already continued on to_model. Return HookResult.allow(); do not attempt to redirect the recovery here. Provider internals are redacted; only the failure class (reason_category) reaches your hook. Zero overhead when unsubscribed, and errors inside the hook never break the fallback path.
Register on the agent-scoped registry passed via Agent(hooks=...) (as shown below) so the hook fires on both sync and async runs — see Model Fallback → Observing the Switch. Hooks registered only on the global default registry may be skipped on the async path.
ModelFallbackInput Fields
The paired MODEL_FALLBACK stream event carries the same fields for live UIs. See Model Fallback → Observing the Switch for the primary user-facing page.

Session Events

Plugin authors: subclass Plugin and override session_start / session_end — see Plugins → How the Bridge Works.
Bot runtime semantics: In the bot runtime (BotOS), SESSION_START fires exactly once per user session lifetime — on the first message, not on every message. SESSION_END fires when the user sends /new, when a policy auto-reset triggers, when stale sessions are reaped, or on reset_all. The reason field on SessionEndInput is one of clear, policy, stale, or clear_all.

SESSION_PERSIST_FAILED — a durable write failed

Fires when the session store cannot persist a turn (add_message / set_chat_history hits disk-full, SQLite/FTS corruption, or a permission / OSError). Before this hook, a failed durable write silently collapsed to False and the already-produced turn lived only in memory — lost on the next shutdown. Now the store salvages the turn to a spill file and fires this event so the failure is observable.
Observe-only. This is an observability signal, not a policy gate — it never re-runs the write and never raises on the caller’s hot path. Return HookResult.allow() (or nothing). It mirrors the outbound-side MESSAGE_UNDELIVERED posture. Skipped entirely when no such hook is registered (zero overhead). See Write-failure salvage for the spill + re-ingest flow.
On a corrupt read the error field starts with corrupt session file:, and spilled carries the quarantine path (<file>.json.corrupt-<epoch_ms>) instead of the write-failure spill path. role/content are empty because there is no in-flight turn. See Corruption-quarantine on load for the full flow.
SessionPersistFailedInput Fields
Spill & recovery flow
A failed durable write salvages the turn to a spill file, fires this hook, then re-ingests the spill on the next session load. Spill files land under ~/.praisonai/state/session_spill/ (written 0600, atomic temp-file + os.replace).
The hook is a notification. Do not retry the write from it — the store has already spilled the turn and re-ingests it on the next load.
Spill filenames carry a random token, so consecutive same-millisecond/PID failures never overwrite each other — no lost turns.
_reingest_spill runs _enforce_window before persisting, so recovered turns obey the retention policy like any ordinary write.
A non-object root, non-list messages, or non-object message is skipped — a bad spill never blocks recovery.

Error Events

Plugin authors: subclass Plugin and override on_error — see Plugins → How the Bridge Works.

Async vs Sync

ON_RETRY fires on both sync and async retry paths. If you register the handler as a regular def, it is dispatched in a thread executor on the async path; if you register an async def, it is awaited directly. Either form is safe on both paths.
Retry is on by default, so ON_RETRY fires with no config needed on both the native OpenAI-client path and the LiteLLM path (default max_retries=3). It only stops firing if you disable retry with retry=False. See Agent Retry.
Released in PraisonAI #2386 — earlier versions skipped this event on the async path.

OnRetryInput Fields

The OnRetryInput event includes both new tool-specific fields and legacy fields for backward compatibility: New fields (recommended):
  • tool_name: Name of the failing tool
  • attempt: Current attempt number (1-based)
  • max_attempts: Maximum attempts configured
  • delay_ms: Delay before this retry in milliseconds
  • error_type: Classified error type (timeout, rate_limit, connection_error, unknown)
  • error: Original exception object
LLM retry fields (populated whenever a transient LLM error is retried, including the default RetryBackoffConfig() policy applied to every Agent — see Agent Retry). ON_RETRY now fires on default Agents too; pass retry=False to disable retries and stop these fires:
  • delay_seconds: Seconds the agent will sleep before the next attempt
  • attempt: Current attempt number (0-based)
  • operation: "llm_request" (sync) or "async_llm_request" (async)
  • error_message: String representation of the failing LLMError
  • max_retries: Configured RetryBackoffConfig.max_retries
  • retry_count: Same as attempt + 1 (1-based legacy alias)
Legacy fields (for backward compatibility):
  • retry_count: Same as attempt
  • max_retries: Same as max_attempts
  • error_message: String representation of error

Agent-Level Error Callbacks

In addition to hook events, agents support a direct on_error callback for LLM failures:

Hook Events vs Agent Callbacks


Extended Events

User Interaction Events

Subagent Events

System Events

Plugin persisters subscribe to AFTER_COMPACTION to read event_data.summary. See Compacted Session Resume for the built-in checkpoint that ships with PraisonAI, and Context Compaction for the full CompactionResult fields.

Message Events

MESSAGE_RECEIVED is a first-class control point: hooks can drop an inbound message so the agent never runs, or rewrite the message content before the agent (or memory) sees it. This is symmetric with MESSAGE_SENDING on the outbound side.
MESSAGE_RECEIVED and MESSAGE_SENDING payloads now include the enriched identity fields (platform, sender_id, channel_id, channel_type, message_id, session_id) when the underlying input provides them — the same fields that reach the Plugin _message_payload bridge.
The hook now returns a decision that every platform adapter (Telegram, Slack, Discord, WhatsApp, Email, AgentMail) honours:
  • HookResult.deny(reason=...) → the message is dropped; agent dispatch is skipped entirely.
  • HookResult(modified_input={"content": "..."}) → the inbound message content is rewritten before dispatch.
  • None or HookResult.allow() → message passes through unchanged (default).
  • Hook errors are non-fatal — a raising hook logs the error and lets the message through.
MESSAGE_RECEIVED and MESSAGE_SENDING are the two message-lifecycle events that do gatedeny drops the message entirely, modified_input["content"] rewrites it. The gate is safe from both sync and async adapters (Telegram, Slack, Discord, WhatsApp, Email, AgentMail) — no async def required in your hook. See PraisonAI #2589.

Drop / block an inbound message

Redact PII before the agent sees it

Authorise sender against an allowlist

How the gate applies decisions

When multiple MESSAGE_RECEIVED hooks run, the last matching modification wins. Hook errors are non-fatal — the message passes through unchanged.
Since PraisonAI PR #2589, every messaging adapter (Telegram, Slack, Discord, WhatsApp, email, agentmail) honours these decisions. Hook exceptions are non-fatal — the message passes through. See Inbound Message Gate for patterns and failure semantics.

MESSAGE_UNDELIVERED — close the loop on a permanent failure

Fires when the gateway’s DeliveryRouter classifies a send as permanently failed. Observability only — the hook does not gate anything.
See Undelivered Message Notice for the full opt-in flow and the gateway.notify_on_undelivered config.
Plugin bridge: inside a Plugin subclass, MESSAGE_SENT and MESSAGE_UNDELIVERED surface as the base methods message_sent(message) and message_undelivered(message) — see Plugins → Message-Lifecycle Plugins. The plugin bridge only wires these when your subclass overrides them.

Gateway Events

Schedule & background events

Schedule hooks fire when scheduled jobs are managed and triggered by BotOS. JOB_COMPLETED fires when a background subagent job launched via spawn_subagent(background=True) reaches a terminal state — after the internal on_complete callback runs, best-effort (a raising handler cannot crash the worker).

Background Job Events

JOB_COMPLETED fires when a background job reaches a terminal state (COMPLETED or FAILED). It fires after the internal on_complete callback runs — a raising handler cannot crash the worker.

JobCompletedInput Fields

Typical uses:
  • Observability and metrics on background job durations and failure rates
  • Custom delivery routing when the built-in deliver= token is insufficient
  • External side effects on completion (webhooks, database writes)

Storage Events

CLI Backend Events

CLI_BACKEND_EXECUTE fires on both success and failure, so subprocess startup errors and timeouts stay traceable — the payload’s error field carries the exception message on the failure path.
This hook is observe-only — it cannot gate the CLI backend call. Return HookResult.allow() (or return nothing). For a policy gate on tool use, register on BEFORE_TOOL instead.

CliBackendExecuteInput Fields

Two safety mechanisms apply at to_dict() time (the payload that reaches log sinks): prompt/system values that follow -p, --prompt, -i, --input, -m, --message, --system are replaced with "<redacted>", and content is truncated to the first 500 characters. The live in-memory command field is left untouched — only the serialised payload is redacted.

Trace CLI delegation programmatically

The prompt is redacted at the payload boundary:

Enable the built-in tracer plugin

The cli_backend_tracer plugin from praisonai-plugins is the batteries-included consumer of this hook — it logs every delegation to the standard praisonai logger. Enable it with one env var and a plugin toggle:
Or use the standard Python knob:
With either set and the plugin enabled, every codex exec / grok -p / gemini -p / claude delegation appears in the log stream, with the prompt value already redacted.

What gets masked

redact_command masks the value that follows these flags, leaving the flag itself visible for verification: Non-list command values (already-serialised strings, None) pass through unchanged.

Kanban Events

When a task with workspace_kind="worktree" fails to merge cleanly, KANBAN_TASK_BLOCKED fires with conflicted_files: list[str] in the payload so alerting agents can surface the exact merge conflict. See Kanban → Per-Task Worktree Isolation.
Preserved worktree comments (not a hook — a task comment). A clean-merge task may also carry a worktree_preserved at <path>: <reason> comment when the dispatcher’s lossless guard refuses to tear down a worktree with uncommitted changes or unmerged commits (or when git worktree remove itself fails). No hook fires — poll for the comment via kanban_show(task_id) if you need to alert on it. See Kanban → Lossless-only worktree teardown.

Dependency Auto-Promotion Events

When the dispatcher auto-promotes a child task, KANBAN_TASK_MOVED fires with this payload:
Auto-promotion events do not include from_status. Check for its absence if you need to distinguish auto-promotions from manual moves.

Complete Event Reference


Bot Runtime Lifecycle

Gateway and session hooks are emitted by BotOS and BotSessionManager — no extra wiring needed when your agent is passed to a bot.
  • All emission is best-effort and a no-op when no hooks are registered (zero overhead)
  • BEFORE_AGENT / AFTER_AGENT are fired by agent.chat() itself — they are not re-fired at the gateway boundary to avoid double-dispatch
  • In async contexts (e.g. inside BotSessionManager.chat), emission is fire-and-forget; in sync contexts it is blocking
MESSAGE_RECEIVED and MESSAGE_SENDING are policy gates, not passive observers. deny drops the message; modified_input["content"] rewrites it. Gateway/session lifecycle events (GATEWAY_START, GATEWAY_STOP, SESSION_START, SESSION_END) remain best-effort observability points and do not gate startup or shutdown. CLI_BACKEND_EXECUTE is also observe-only — it is orthogonal to the message gates and cannot stop the CLI delegation; it only reports that a subprocess turn happened.

Best Practices

Hooks run synchronously. Avoid heavy operations that could slow down agent execution.
Use pattern matchers to only run hooks for specific tools or operations.
Return HookResult.allow() quickly for non-matching cases to minimize overhead.
Wrap hook logic in try/except to prevent breaking agent execution.

Inbound Message Gate

Drop or redact incoming messages before the agent sees them

Hooks

Hook system overview

Kanban Tasks

Kanban hook events and lifecycle

Plugins

Plugin system with hooks