~/.praisonai/plugins/ and load it in one line.
Quick Start
1
Simple Usage
Create Load and run:
~/.praisonai/plugins/my_tools.py:2
With Configuration
Point the environment (or config file) at plugins and Agent construction wires them for you — no explicit Prefer to turn plugins on in code? Call
plugins.enable() call needed:plugins.enable(...) before creating the agent:Place plugins in
~/.praisonai/plugins/ (user-wide) or ./.praisonai/plugins/ (project-specific).discover_and_load_plugins() loads and registers plugins. To read plugin metadata without loading (e.g. to build a config-validation gate), use discover_plugins() and the capability manifest fields — see Static Capability Manifest.How It Works
plugins.enable() auto-calls wire_into_hook_registry(), which registers each enabled plugin’s lifecycle methods on the default hook registry the agent consults at runtime — and Agent init calls this for you when the env var or config file requests it (see Auto-Enable from Env or Config).
The hooks fire in this order around each agent run:
Choosing How to Load Plugins
Pick the loading method that fits your setup.Plugin Locations
Security: Project-Plugin Trust Gate
A single-file plugin is the most privileged extension surface there is — it can hook every lifecycle event and intercept every tool call. So a plugin dropped into a project’s./.praisonai/plugins/*.py (for example, one that arrived with a cloned repository) does not run until you opt in. This mirrors PRAISONAI_ALLOW_LOCAL_TOOLS for tools: a cloned repo carrying a malicious .praisonai/plugins/exfil.py stays inert by default.
Open the gate with an environment variable (accepts true/1/yes/on):
.praisonai/config.yaml:
Inspect the Plugin Registry
get_plugin_registry() returns a truthful, unified view of every discoverable plugin from three real sources — no hardcoded entries. It reads without a running agent, so a CLI or health check can render it directly.
{name, version, description, source, enabled, hooks} (single-file entries also include path). The source field records provenance:
enabled reflects both the live manager state and the persisted config allow-list, so it renders correctly even before any agent starts.Reload Plugins at Runtime
Edited a plugin, or dropped a new one in, and want it live in the current process? Runpraisonai plugins reload — it forces rediscovery and rewires hooks so newly-added or edited plugins take effect without a restart. See the Plugins CLI.
Hook Plugin Example
Create~/.praisonai/plugins/my_logger.py:
~/.praisonai/plugins/tool_sandbox.py to filter advertised tools:
Static Capability Manifest
Declare a plugin’s capabilities in its header so onboarding, doctor and config validation can read them without importing the plugin’s runtime. Add four optional fields to the header — they parse as plain text, so the plugin’s runtime is never imported to read them. Create~/.praisonai/plugins/telegram_channel.py:
Field reference
Read the manifest from code
discover_plugins() scans plugin directories and returns metadata dicts — the plugin body is never imported.
Omitted fields are absent from the raw
discover_plugins() dict, so read them with .get(key, []). PluginMetadata.to_dict() normalizes them to empty lists.Rules
- Fields are optional — omitting them yields empty lists (safe default).
- Field keys are case-insensitive —
Channels,channels, andCHANNELSall work. - Aliases:
Provides=Provides Tools=Tools;Config=Config Schema;Auto Enable When Configured=auto_enable_when_configured. - The plugin file’s runtime is never imported to read these fields — safe to scan untrusted plugin directories for a capability inventory.
Do I need a manifest?
Lifecycle-Method Plugins
SubclassPlugin and override a lifecycle method to transform prompts, messages, or responses. Call plugins.enable() — or set PRAISONAI_PLUGINS=true / [plugins] enabled = true and let Agent construction wire it — to activate the method at runtime.
Create ~/.praisonai/plugins/pii_redactor.py:
Session & Error Lifecycle Plugins
Overridesession_start, session_end, on_error, on_config, or on_auth to react to session boundaries, errors, config, and credential resolution.
Choosing a lifecycle event
Observe sessions
session_start and session_end observe when a session opens and closes.
~/.praisonai/plugins/session_logger.py:
Observe errors
on_error observes errors during a run — use it to log without changing behavior.
~/.praisonai/plugins/error_reporter.py:
Rewrite config
on_config returns a dict to rewrite runtime configuration in place.
~/.praisonai/plugins/config_defaults.py:
Inject credentials
on_auth returns a dict of credentials — the bridge writes them back even when credentials starts as None, so first-time injection works.
~/.praisonai/plugins/token_injector.py:
Message-Lifecycle Plugins
Overridebefore_message, after_message, message_sent, or message_undelivered to react to every hop of a message’s lifecycle — per-user policy, per-channel redaction, delivery telemetry, or dead-letter escalation.
Choosing a message hook
Enriched payload keys
Every message-lifecycle method receives (at most) these keys:Absent fields are skipped — the bridge does not fabricate empty strings for keys the concrete input doesn’t carry. Read fields with
.get(key), not subscripting, so a plugin written against inbound input degrades cleanly on outbound events.React per user, per channel, and per delivery
Lead with the outcome: rate-limit a noisy sender, count deliveries per channel, or escalate a dead letter.message_undelivered is the counterpart to the plain MESSAGE_UNDELIVERED hook already covered in Undelivered Messages and Gateway → Undelivered Notice — this hook exposes the same signal at the plugin surface, so Plugin subclass authors don’t need to reach for the raw HookRegistry.Function-Based Plugins (FunctionPlugin)
FunctionPlugin wraps plain functions into a plugin without subclassing — useful for one-off hooks or when the whole plugin is a single callable.
When to reach for it
Quick Start
Register aFunctionPlugin on the plugin manager, then start the agent — the callable runs before the agent sees the message.
Constructor
Hook dispatch table
Each key inhooks={} is a PluginHook enum value; the callable’s return value drives the effect below.
Message-lifecycle examples
Lead with the outcome: rate-limit a channel, log every delivery, or capture permanent failures.FunctionPlugin’s message-lifecycle dispatch (MESSAGE_RECEIVED, MESSAGE_SENDING, MESSAGE_SENT, MESSAGE_UNDELIVERED) is a recent addition (commit 857ebbe) — earlier versions silently no-op’d these four keys, so users had to subclass Plugin.For the full payload contract (identity keys, per-event availability,
error / notice_delivered special-cases), see Message-Lifecycle Plugins — FunctionPlugin and Plugin subclasses share the exact same _message_payload bridge.Auto-Enable from Env or Config
Agent construction auto-enables plugins when the environment or config file requests it — no explicitplugins.enable() call needed.
Set the env var, then any Agent(...) wires plugins before it runs:
.praisonai/config.toml:
plugins.maybe_enable_from_config(), which reads the env var and config file, then runs enable(get_enabled_plugins()) exactly once per process.
Precedence: explicit plugins.enable(...) > PRAISONAI_PLUGINS env var > [plugins] in config.yaml / config.toml > disabled.
maybe_enable_from_config() is idempotent and runs at most once per process, so instantiating multiple agents is safe — plugins are wired a single time.Suppress Plugins for One Run (Pure Mode)
The inverse of auto-enable — skip external-plugin discovery for a single run without touching persisted state. Pass--pure (or --no-plugins) to run, chat, or code:
disabled=True > PRAISONAI_NO_PLUGINS env var. The CLI flag is scoped and restored on return, and never mutates .praisonai/config.yaml.
See Pure Mode for the full guide.
Configure Plugins from .praisonai/config.yaml
Turn plugins on and hand each one its own options from a single YAML file — no code.
~/.praisonai/config.yaml:
app.py:
Agent construction auto-loads the config, enables plugins, and hands
pii_guardrail its {redact: [email, phone]} dict via on_config. Nothing else to wire up.Where the config file is found
The first file found wins — TOML and YAML have equal standing; whichever appears first in the search stops the walk.
The four shapes of enabled
The JSON config schema advertises
boolean and array for enabled; the loader also accepts a bare string at runtime for TOML users who prefer enabled = "pii_guardrail".A bare
enabled: true is now preserved — enabling a plugin while it is set is a no-op (leaving “all enabled” intact). You cannot run praisonai plugins disable X while enabled: true: the CLI refuses with a clear error, since collapsing “all” to an allow-list would silently disable every other plugin. Set plugins.enabled to an explicit list of names first, then disable individual plugins.Per-plugin option blocks
Reserved keys (enabled, auto_discover, directories, allow_project_plugins) configure the plugin system; any other key whose value is a mapping is treated as a per-plugin option map delivered to that plugin’s on_config hook.
allow_project_plugins: true opens the project-plugin trust gate so single-file plugins under ./.praisonai/plugins/ load. The PRAISONAI_ALLOW_PROJECT_PLUGINS env var takes precedence over this config key.An explicit top-level
enabled: false wins over any per-plugin block. Omit enabled at the top level and per-plugin blocks decide.Reading options inside a plugin — on_config
apply_plugin_options() invokes plugin.on_config(options) for every enabled plugin that has a configured option map — errors in a single plugin are logged and do not abort delivery to the others.
Create ~/.praisonai/plugins/pii_guardrail.py:
on_config fires once per enable — the reserved enabled flag (if present in the plugin block) is preserved so plugins can read it too.Deliver options from code — options_by_name
Skip the file and hand each plugin its options directly. plugins.enable() takes an options_by_name map that it delivers to each plugin’s on_config hook:
.praisonai/config.yaml calls this for you — maybe_enable_from_config() runs enable(get_enabled_plugins(), options_by_name=get_plugin_options()) under the hood. Call get_plugin_options() yourself to read the per-plugin maps the loader parsed.Choose a plugin config surface
Precedence
Explicitplugins.enable(...) in code > PRAISONAI_PLUGINS env var > [plugins] in config.yaml / config.toml > disabled by default.
Edge cases
- PyYAML missing — YAML configs silently return empty (debug log only); install
pyyamlto enable. - Unknown reserved key — the wrapper resolver’s typo-suggestion validator only checks against
{enabled, auto_discover, directories, allow_project_plugins}; per-plugin blocks (dicts) are always accepted. - Removed plugin block on reload — the manager stores options with
replace=Trueby default, so a block dropped from a later config no longer delivers stale options. - Plugin
on_configraises — logged as a warning; delivery to other plugins is not aborted.
Config-write single source of truth
praisonai plugins enable/disable write to the same file the runtime reads — no separate JSON. The write target is resolved in this order:
praisonai plugins disable <name> unloads a single-file plugin’s tools as well as its hooks — unload_plugin(module_name) removes the functions that module contributed (tracked via a pre-exec registry snapshot), so disable truly removes the plugin’s functionality for the rest of the run without touching tools owned by another plugin or the core.How the Bridge Works
plugins.enable() auto-calls wire_into_hook_registry() — no manual step. Only lifecycle methods a plugin actually overrides (or declares in PluginInfo.hooks) are bridged, so a plugin with one guardrail never fires on every event. Return a new value and the bridge writes it back onto the payload in place. Errors in a lifecycle method are non-fatal, and plugins.disable([...]) calls unwire_from_hook_registry(name) so the plugin truly stops firing.
The five
before_* methods (before_agent, before_llm, before_tool, before_tool_definitions, before_message) also accept a deny/block decision instead of a rewrite — see Blocking Plugins. after_tool additionally accepts a rewrite or block — see Redact / Block Tool Output.on_config and on_auth write their returned dict back onto the payload even when the target attribute starts as None — so a plugin can inject credentials the first time they’re requested, not only edit an existing dict.
after_tool is now a redaction / block seam, not an observer (PraisonAI PRs #3968 / #3969). A Plugin.after_tool return value is written back to the effective tool result before it reaches the model — mirroring before_tool (arg rewrite) and after_llm (response rewrite). Return a scrubbed value to redact, PluginDecision.deny("reason") to suppress, or raise GuardrailBlocked("reason") for the same effect via the exception form. See Redact / Block Tool Output.Reference Plugins in praisonai-plugins
Batteries-included plugins ship in the separate praisonai-plugins package — install once, then enable by name.
cli_backend_tracer overrides Plugin.cli_backend_execute(context) and logs every CLI backend delegation to the standard praisonai logger — with the prompt already redacted, so the log stream is safe to ship to an aggregator.
CliBackendExecuteInput payload reference.
Blocking Plugins (Guardrails & Policies)
Guardrail and policy plugins can now stop an action before it happens, not just rewrite it — refuse a dangerous tool call, drop a spam message, or decline an LLM request.Quick Start
1
Block a tool call
Return
PluginDecision.block(reason) from before_tool to skip a forbidden tool.2
Refuse an LLM request
Return
PluginDecision.deny(reason) from before_llm and the agent returns "[LLM request blocked by hook: <reason>]" without calling the model.3
Drop an inbound message
Return
PluginDecision.deny(reason) from before_message to drop a message before the agent sees it.Three Ways to Block
The same block, written three ways — pick the one that reads cleanest in your code.PluginDecision.deny(reason) and PluginDecision.block(reason) both stop the action (is_denied() is True for each); allow(reason=None) is an explicit no-op. GuardrailBlocked(reason: str = "Blocked by guardrail plugin") is caught by the bridge and converted to a block. It may be raised from any before_* method and from after_tool — the tool has already run there, but the output is suppressed before it reaches the model (see Redact / Block Tool Output).
Where Blocks Fire
The fivebefore_* methods and after_tool can block; the remaining after_* / observational methods cannot.
How a Block Flows
Which Style Should I Use?
Ship a Plugin as a pip Package
Register your plugin class in thepraisonai.plugins entry-point group in pyproject.toml:
plugins.enable()) auto-discovers and bridges it — no user code changes needed.
Once installed, verify the plugin registered with praisonai plugins add <package> --dry-run — see plugins add.
A shipped plugin cannot replace a built-in name. PraisonAI’s registries subclass the base
PluginRegistry, so since PR #4176 any entry point whose name (case-insensitive) matches a shipped built-in is skipped and the built-in is kept (a DEBUG line notes the collision). Pick a name unique to your package. Runtime register(...) is the deliberate override path. See Plugin Precedence.CLI Commands
praisonai plugins list shows every plugin with its source (entry_point:<dist>, registered, or single_file). enable/disable persist to the config the runtime reads, reload picks up edits without a restart, and doctor diagnoses issues. See the full reference on the Plugins CLI page.
Configuration Options
OnlyPlugin Name is required; every other field is optional.
Best Practices
Keep plugins single-purpose
Keep plugins single-purpose
One file per concern — weather tools, logging, or guardrails, not all three.
Call discover_and_load_plugins() once
Call discover_and_load_plugins() once
Load before creating the agent so tools and hooks register globally.
Reference tools by name
Reference tools by name
Pass
tools=["get_weather"] — the string must match the @tool function name.Use plugins.enable for built-ins
Use plugins.enable for built-ins
Enable
logging and metrics without writing plugin files.Call plugins.enable() to activate lifecycle methods
Call plugins.enable() to activate lifecycle methods
Tools and guardrails work without
plugins.enable(). But lifecycle-method plugins (subclasses of Plugin that override before_llm, after_llm, and so on) only fire after they are wired into the runtime hook registry. Call plugins.enable() explicitly, or set PRAISONAI_PLUGINS=true / [plugins] enabled = true and Agent construction wires them automatically.Reach for PluginDecision before HookResult
Reach for PluginDecision before HookResult
PluginDecision.deny(reason) reads as one line and only needs the plugin import. Use HookResult.deny(reason) only when you already import it for another reason, and raise GuardrailBlocked(reason) only when you’re inside a validator that already raises on failure. Prefer block over deny when the action is categorically forbidden; use deny when it’s contextually refused (this input, this user, this time).Related
Hooks
Hook events and the HookRegistry API
Toolsets
Create and register custom agent tools
Plugins CLI
List, enable, disable, reload, and diagnose plugins
Config File
Turn plugins on from
[plugins] in config.tomlTool Discovery
How Agent resolves tool names at runtime
Guardrails
Validate agent output — automatic retry on failure
Redact Tool Output
Scrub secrets or block tool results before the model sees them

