Bot Platform Plugins
Add custom messaging-platform bots to PraisonAI via Python entry points
from praisonaiagents import Agent
agent = Agent(name="platform-bot", instructions="Use platform-specific bot plugins.")
agent.start("Enable Telegram and Discord plugins for this bot.")Third-party bot packages can register via Python entry points to extend PraisonAI with custom messaging platforms.
The user installs a third-party bot package; entry points register new platforms alongside built-in channels.
Five registration paths reach the same adapter class: three that need Python (built-ins, entry point, register_platform()) and two zero-code paths (YAML adapter: ref and .praisonai/channels/ drop-in files). See Custom Gateway Channel (Zero-Code) for the full zero-code story.
Quick Start
- Programmatic Registration
Register a bot platform directly in your code:
python from praisonai.bots._registry import register_platform class MyBot: def __init__(self, **kwargs): self.kwargs = kwargs async def start(self): print("Starting MyBot...") async def stop(self): print("Stopping MyBot...") register_platform("mybot", MyBot) - Entry-point Plugin
Create a pip-installable plugin using
pyproject.toml:toml # pyproject.toml [project.entry-points."praisonai.channels"] mybot = "my_pkg.bot:MyBot"After installation, use the bot platform:
python from praisonai.bots import Bot bot = Bot("mybot", agent=my_agent) await bot.start()
Zero-Code Paths
Two paths register a channel with no packaging and no bootstrap Python — both self-register the adapter before startup.
YAML adapter: import ref. Point a channel at a dotted "module:Class" string; the gateway imports, validates, and registers it.
# gateway.yaml
channels:
my_intranet_chat:
adapter: "my_company.adapters:IntranetBot"
token: ${INTRANET_TOKEN}Filesystem drop-in. Any BasePlatformAdapter subclass in ./.praisonai/channels/.py (project, gated by PRAISONAI_ALLOW_PROJECT_PLUGINS=true) or ~/.praisonai/channels/.py (user-global, trusted) registers under its platform_name.
mkdir -p ~/.praisonai/channels
cp adapters.py ~/.praisonai/channels/
praisonai gateway startSee Custom Gateway Channel (Zero-Code) for the trust model, error surface, and copy-paste recipes.
How It Works
The bot platform registry provides a central point for managing bot implementations:
| Operation | Description | When Called |
|---|---|---|
| Discovery | Entry points auto-loaded on registry access | Import time |
| Registration | Bot platforms registered by name | Plugin installation |
| Creation | Bot instances created on demand | Bot() constructor |
| Availability | Platform dependencies checked | Before execution |
Configuration
The bot platform registry supports both programmatic and entry-point registration:
| Function | Purpose |
|---|---|
register_platform(name, cls, capabilities=None) | Register at runtime (optional PlatformCapabilities descriptor) |
get_platform_capabilities(name) | Get the capabilities descriptor for a registered platform |
list_platforms() | List all registered platform names |
resolve_adapter(name) | Get class for a platform name |
get_platform_registry() | Backward-compat: returns dict[name, class] of all platforms |
get_default_bot_registry() | Get the process-default BotPlatformRegistry (advanced) |
Built-in Platforms
PraisonAI includes these built-in bot platforms:
telegram– Telegram bot integrationdiscord– Discord bot integrationslack– Slack bot integrationwhatsapp– WhatsApp bot integrationlinear– Linear issues integrationemail– Email bot integrationagentmail– AgentMail integrationwebhook– Generic HTTP webhook trigger (declarative routes) — see Webhook Channel
Entry-point Groups
| Group | Purpose |
|---|---|
praisonai.channels | Recommended for new packaged connectors — idiomatic, zero-config auto-registration |
praisonai.bots | Legacy group — still scanned for backward compatibility |
Both groups are scanned by BotPlatformRegistry on startup. A connector that would shadow a built-in platform is skipped with a warning.
Discovery via entry points (praisonai.channels)
The praisonai.channels entry-point group is the idiomatic way to distribute a bot connector as a pip-installable package. Once installed, the platform is available with no extra Python code:
# pyproject.toml of your connector package
[project.entry-points."praisonai.channels"]
irc = "praisonai_irc:IRCBot"After pip install praisonai-irc:
from praisonai.bots import Bot
bot = Bot("irc", agent=my_agent, server="irc.libera.chat")
await bot.start()List all registered platforms — built-in, entry-point, and custom — with the CLI:
praisonai gateway channels --available
# => telegram, discord, slack, whatsapp, linear, email, agentmail, ircOr in Python:
from praisonai.bots._registry import list_platforms
print(sorted(list_platforms()))list_platforms() also returns anything registered by register_platform() in the current process. The entry-point group is loaded lazily on first registry access, so a plugin that is installed but never imported still appears after the first resolve_adapter() call.
Plugin channel on the gateway runtime
Register a plugin platform, then reference it in gateway.yaml:
# my_plugin.py
from praisonai.bots._registry import register_platform
class MattermostBot:
def __init__(self, **kwargs): self.cfg = kwargs
async def start(self): ...
async def stop(self): ...
register_platform("mattermost", MattermostBot)# gateway.yaml
agents:
assistant:
instructions: "You are a helpful assistant."
channels:
mattermost:
token: ${MATTERMOST_TOKEN}python -c "import my_plugin" && praisonai gateway start --config gateway.yaml
# Before PR #3579: gateway silently skipped 'mattermost' with a single WARNING; channel never started.
# After PR #3579: resolve_adapter('mattermost') succeeds, adapter is constructed, channel runs;
# an unresolvable name is recorded as a degraded channel instead of dropped.The gateway walks the same path a built-in channel does:
sequenceDiagram
participant User
participant Gateway as WebSocketGateway
participant Registry as resolve_adapter
participant Adapter as PluginBot
User->>Gateway: praisonai gateway start --config gateway.yaml
Gateway->>Gateway: _create_bot(channel_type, ...)
Gateway->>Gateway: _build_channel_adapter(...)
Gateway->>Registry: resolve_adapter("mattermost")
Registry-->>Gateway: PluginBot class (register_platform or entry point)
Gateway->>Gateway: seed kwargs + _EXTRA_ENV_MAP backfill + ch_cfg pass-through
Gateway->>Adapter: PluginBot(**init_kwargs)
Adapter-->>Gateway: instance
Gateway->>Adapter: await bot.start()
classDef agent fill:#8B0000,stroke:#7C90A0,color:#fff
classDef process fill:#189AB4,stroke:#7C90A0,color:#fff
classDef result fill:#10B981,stroke:#7C90A0,color:#fff
class User,Adapter agent
class Gateway,Registry processWhen resolution fails, the gateway calls self._mark_degraded_owner("channel", "mattermost", reason="unresolved_platform") instead of raising — the channel is skipped but stays visible in health / doctor.
How the gateway constructs a plugin channel
The gateway builds every channel adapter through one resolution seam and a three-step kwarg assembly.
Adapter resolution. resolve_adapter(channel_type) is the single lookup, resolved in ladder order: a registered platform (built-in / entry point / register_platform) wins, then a YAML adapter: import ref, then a .praisonai/channels/ drop-in file. Only when none resolve does the gateway mark the channel unresolved_platform.
Kwarg assembly. The gateway layers three sources in order; later sources override earlier ones:
| Order | Source | Contents |
|---|---|---|
| 1 | Seed | `{“token”: <token>, “agent”: |
, “config”: <config>} | | 2 | _EXTRA_ENV_MAP[channel_type] | For each param → env_key, if os.environ.get(env_key) is non-empty, set init_kwargs[param] = env_val (e.g. Slack's SLACK_APP_TOKEN → app_token) | | 3 | ch_cfg (from channels: YAML) | Every key except “platform” and “token”` copied verbatim; overrides step 2 |
The gateway then calls adapter_cls(**init_kwargs). ch_cfg wins over env backfill, and platform / token are stripped so they never double up. This mirrors probe_channels and Bot._build_adapter.
Token env-var fallback. When the token from channels: YAML is falsy, the gateway consults _TOKEN_FALLBACK_ENV — the first non-empty value wins:
channel_type | Env vars tried in order |
|---|---|
linear | LINEAR_OAUTH_TOKEN, LINEAR_API_KEY |
email | EMAIL_APP_PASSWORD |
agentmail | AGENTMAIL_API_KEY |
Other platforms have no token fallback here — Slack’s app-token env still comes from step 2 (_EXTRA_ENV_MAP), not this map.
Where a plugin author sets each value:
Degraded channels: what happens when a plugin fails
A channel that cannot resolve or construct is recorded as degraded, not silently dropped.
Two exact reason strings surface via _mark_degraded_owner("channel", <channel_type>, reason=...):
reason | Trigger | Log line |
|---|---|---|
unresolved_platform | resolve_adapter(channel_type) raised ValueError — no built-in, register_platform, entry-point, YAML adapter: ref, or .praisonai/channels/ drop-in resolves it | Unknown channel type %r — no built-in, registered, or entry-point adapter resolves it; channel not started. |
adapter_construction_failed | adapter_cls(**init_kwargs) raised any Exception (e.g. wrong kwargs, connection error) | Failed to construct channel %r adapter: %s |
Both paths return None from _create_bot, so the two call sites (start_channels and hot-reload) keep their existing None contract. The failure is queryable via the degraded-owner surface (health / doctor) — see Gateway Degraded Channels — instead of vanishing into logs.
Common Patterns
Declare Platform Capabilities
from praisonaiagents.bots import PlatformCapabilities
from praisonai.bots._registry import register_platform
register_platform(
"mybot",
MyBot,
capabilities=PlatformCapabilities(
max_message_length=2000,
length_unit="codepoints",
supports_edit=True,
markdown_dialect="markdown",
),
)Capabilities let PraisonAI’s shared delivery layer chunk, stream, and rate-limit messages correctly for your platform — see Bot Platform Capabilities for the full field list.
Register a Native Presentation Renderer
Channel plugins that ship a PresentationRenderer should call register_presentation_renderer(platform, MyRenderer) from praisonaiagents.bots at setup time, so interactive UI (buttons, selects, approvals) renders natively instead of degrading to plain text.
from praisonaiagents.bots import register_presentation_renderer
from my_matrix_plugin.renderer import MatrixPresentationRenderer
register_presentation_renderer("matrix", MatrixPresentationRenderer)This is the same extension point built-in channels use. See Presentation Renderers for the renderer contract and validation rules.
Use the Canonical Admission Primitive
Reuse resolve_ingress_admission() in your adapter’s message handler so your custom channel inherits the same allowlist / blocklist / group-policy semantics as every built-in — and drops become inspectable (reason_code) instead of silent logger.debug lines.
from praisonaiagents.bots import resolve_ingress_admission
class MyBot:
async def on_message(self, msg):
decision = resolve_ingress_admission(
chat_type=msg.chat_type, # "dm" / "private" / "group" / "channel" / ...
sender_id=msg.sender_id,
is_mention=msg.mentions_bot,
is_command=msg.text.startswith("/"),
allowlist=self.config.allowed_users,
blocklist=self.config.blocked_users,
group_policy=self.config.group_policy,
paired=self.pairing.is_paired(msg.sender_id),
)
if not decision.admit:
# Record the reason so operators can answer "why didn't the bot reply?"
logger.info(
"message dropped: gate=%s reason=%s sender=%s",
decision.gate, decision.reason_code, msg.sender_id,
)
if decision.observe:
# `observe` group policy: passively record but do not run the agent
await self.session.record_passive(msg)
return
await self.agent.run(msg.text)The ladder (first matching gate wins): block-list → allow-list → pairing → direct-chat bypass → group policy. An unset group_policy defaults to mention_only — the live BotConfig default — so a forwarded unset policy fails safe rather than replying to everything in a group.
Adapters compute structured MentionFacts (explicit, reply_to_bot, quoted_bot, thread_participant) and hand them to resolve_ingress_admission(mention=…). Under mention_only / observe, a reply to the bot is admitted as an implicit mention (see Implicit Mentions). The primitive is back-compatible with the old is_mention: bool argument, so existing adapters keep working unchanged.
Reason codes an operator can grep for: allowed, blocked, not_in_allowlist, pairing_required, group_mention_only, command_only, observe.
IngressDecision fields:
| Field | Type | Meaning |
|---|---|---|
admit | bool | Whether the adapter should run the agent on this message. |
reason_code | str | Machine-readable reason (one of the seven above). |
gate | str | Which stage decided: blocklist, allowlist, pairing, direct, group_policy. |
observe | bool | When True, do not admit but do record the message as passive session context (observe group policy). |
Override a Built-in Platform
Registry uses last-write-wins with lower-cased keys:
from praisonai.bots._registry import register_platform
class CustomSlackBot:
def __init__(self, **kwargs):
self.token = kwargs.get('token')
async def start(self):
# Custom Slack implementation
pass
async def stop(self):
pass
# Override built-in Slack bot
register_platform("slack", CustomSlackBot)Lazy Heavy Imports
Follow the pattern used by built-ins to avoid import-time failures:
class HeavyFrameworkBot:
def __init__(self, **kwargs):
self.config = kwargs
self._client = None
async def start(self):
# Only import when actually starting
import heavy_networking_sdk
self._client = heavy_networking_sdk.Client(
token=self.config.get('token')
)
await self._client.connect()
async def stop(self):
if self._client:
await self._client.disconnect()Multi-tenant Isolation
Construct your own BotPlatformRegistry to avoid leaking between tenants:
from praisonai.bots._registry import BotPlatformRegistry
# Each tenant gets their own registry
tenant_registry = BotPlatformRegistry()
tenant_registry.register("custom-slack", TenantSpecificSlackBot)Best Practices
Use Lazy Imports
Never import heavy networking SDKs at module top level:
# ❌ Bad - imports at module level
import heavy_sdk
class BadBot:
def __init__(self, **kwargs):
self.client = heavy_sdk.Client()
# ✅ Good - lazy imports
class GoodBot:
async def start(self):
import heavy_sdk
self.client = heavy_sdk.Client()Implement Proper Protocol
Follow the expected bot lifecycle pattern:
class ProperBot:
def __init__(self, **kwargs):
# Store config, don't establish connections yet
self.config = kwargs
self.running = False
async def start(self):
# Establish connections, start listening
self.running = True
async def stop(self):
# Clean shutdown
self.running = FalseHandle Errors Gracefully
Use logging instead of raising on initialization:
import logging
logger = logging.getLogger(__name__)
class RobustBot:
def __init__(self, **kwargs):
self.config = kwargs
async def start(self):
try:
# Connection logic here
pass
except Exception as e:
logger.error(f"Failed to start bot: {e}")
# Don't re-raise, let caller handleDelivery lifecycle
Bot plugin authors are the primary consumers of the enriched message identity — platform and channel_id on inbound messages, plus successful and failed delivery signals on outbound. Override message_sent and message_undelivered in a Plugin subclass to add delivery telemetry or dead-letter escalation without patching adapters.
Related
Add a channel from YAML adapter: or a .praisonai/channels/ drop-in file
React to inbound, outbound, delivered, and undelivered messages
Bot Platform CapabilitiesDeclare streaming, chunking, and rate-limit behaviour
Bot GatewaySee how group_policy, allowed_users, and pairing are configured in YAML
See where unresolved_platform and adapter_construction_failed surface in health/doctor
Register a native renderer so buttons, selects, and approvals render natively
Framework Adapter PluginsLearn about extending PraisonAI with custom execution frameworks
Bot GatewayRun agents on messaging channels