Skip to main content
Subclass BasePlatformAdapter to add a new chat channel: implement four methods, declare capabilities, and inherit robust chunking, retry, typing, and edit-fallback delivery. An adapter also inherits canonicalize(platform, raw_user_id) — an optional identity-reconciliation hook that defaults to the identity function. See Identity canonicalization.

Quick Start

A minimal adapter implements four methods and calls deliver() to send.
1

Minimal adapter (4 methods)

Subclass BasePlatformAdapter, declare capabilities, and implement connect, disconnect, send, and get_chat_info.
2

Declare more capabilities to unlock defaults

Turn on supports_edit and supports_typing, then override only what the platform genuinely does — here a lightweight edit_message.

How It Works

deliver() formats, chunks, sends a typing heartbeat, then sends each chunk with retry — all keyed off capabilities.

What Do I Need to Override?

Start with the four abstract methods, then reach for defaults only when the platform can do better. An adapter that defers a send to a durable outbox returns SendResult(ok=True, queued=True, ...); one that re-sends after crash recovery returns SendResult(ok=True, duplicate=True, ...) — the status derives automatically so the caller can branch on the outcome.

Identity canonicalization (optional override)

BasePlatformAdapter implements IdentityCanonicalizerProtocol out of the box — the default canonicalize() returns the raw id unchanged.
Override it when a platform addresses the same person with two interchangeable ids — a WhatsApp LID vs phone JID, a handle-as-id rename, or a number↔UUID alias flip — so the conversation collapses to one session instead of silently forking.
The override must be deterministic, total (never raise), and fail-open — return raw_user_id unchanged when no mapping is known. Because a vanilla subclass already satisfies the protocol, hand the adapter itself to identity_canonicalizer= wherever the protocol is accepted. See Identity Canonicalization for the full wiring story.

Opting Out of Default Supervision

When Bot(...) runs your adapter, it wraps the inbound run loop in ChannelSupervisor by default — auto-reconnect with capped backoff plus health-based restart. Set the class attribute supervised_inbound = False when your adapter already manages its own reconnect loop:
The supervised path drives the start()/stop() seam: start() runs the inbound source until stopped and raises on an unexpected drop; the default stop() delegates to disconnect() — override it if your start() blocks and needs an explicit unblock.
Built-in Telegram sets supervised_inbound = False because it already runs its own reconnect loop internally, and the relay transport opts out because the connector owns out-of-process reconnect.

User Interaction Flow

A long user reply flows through deliver() as three chunks, with one retry honouring retry_after. Four behaviours are worth calling out:
  1. Chunking is text-only. Dict content passes straight through to send() without chunking or formatting — ideal for rich payloads like attachments or buttons.
  2. Reply-to only on the first chunk. In a multi-chunk reply, chunk #1 carries reply_to; chunks 2..N are unthreaded follow-ups. SendResult.message_ids gives you every chunk’s id in order.
  3. Typing is best-effort. Failures in send_typing() are swallowed, so a broken indicator never breaks delivery.
  4. edit_message declares “not supported” instead of crashing. Callers on channels without edits use the edit_not_supported fallback to decide whether to re-send.
If any chunk in a multi-chunk reply comes back queued or duplicate, deliver() carries that outcome onto the aggregate SendResult.status — a deferred or at-least-once delivery is no longer masked as "sent".

Branching on delivery outcome

A swallowed exception must never become a silent drop. Branch on result.status after deliver() — the four outcomes are a closed set, so the caller always knows what happened without depending on an exception a wrapper layer might eat.
An adapter that persists to a durable outbox returns SendResult(ok=True, queued=True, ...), and one that re-sends after crash recovery returns SendResult(ok=True, duplicate=True, ...) — the status property derives automatically from those flags.

Configuration Options

SendResult is the transport-neutral value returned by every send/edit path. SendResult.status is a closed union — the SendStatus literal alias "sent" | "failed" | "queued" | "duplicate" — so a caller can exhaustively branch on the outcome instead of treating it as an arbitrary string. Import both from the top-level package:
SendResult.to_dict() returns a plain dict for logging and observability, now including the status, queued, and duplicate keys.
Backward compatible. send_message may still return a legacy BotMessage — it is accepted and treated as status == "sent". New or refactored adapters SHOULD return a SendResult so the caller can branch on the typed status.
BasePlatformAdapter class attributes declare adapter behaviour. The delay between attempts follows retry_after first, then exponential backoff:
A send() implementation that raises is treated as a failure and retried — you do not have to catch transport errors yourself. The four abstract methods every subclass must implement:

BasePlatformAdapter SDK Reference

Full auto-generated Python API surface for BasePlatformAdapter and SendResult.

Common Patterns

Override chunk() when the platform needs code-fence-aware splitting.
Send a dict payload to pass rich content straight through — deliver() skips chunking and formatting.
Register the adapter at runtime via the platform registry — see Bot Platform Plugins for register_platform(name, cls). The same adapter class is reachable three ways with no code change: register_platform(), a YAML adapter: "module:Class" import ref, or a .praisonai/channels/*.py drop-in file — one class, three registration surfaces. See Custom Gateway Channel (Zero-Code).

Formatting: the markdown_dialect seam

format_message is no longer identity — it auto-calls format_for_dialect(text, caps.markdown_dialect), so a custom adapter usually does not need to override it. Declare markdown_dialect on PlatformCapabilities and the base class renders each reply in the right flavour.
Set markdown_dialect and inherit format_message() — replies render correctly with no extra code.
format_for_dialect(text, dialect) returns (rendered_text, parse_mode). See How markdown_dialect is consumed for the full dialect table. Override format_message only when rendering can’t be expressed as a dialect string — embed builders, non-text payloads, or ML-driven templating.
Plain-text fallback preserves identifiers. The default "markdown" dialect returns a safe plain-text reduction. As of PraisonAI #3506, strip_markdown unwraps only paired emphasis/code spans, so identifiers (svc_1), globs (*.py), and arithmetic (a*b) are preserved in the fallback rendering rather than mangled.

Best Practices

Chunking, retry, and typing belong to deliver(). Keep send() a thin wrapper around one platform API call so the shared machinery stays in control.
Populate SendResult(ok=False, retry_after=X) from send() when the platform reports a rate limit. The default retry loop honours it and beats fixed backoff.
A truthful PlatformCapabilities gives the shared code the best information for graceful degradation. Overstating a capability breaks the fallback path.
Callers rely on the built-in edit_not_supported fallback to decide whether to re-send. Setting supports_edit=True without an override raises NotImplementedError.

Bot Platform Capabilities

The PlatformCapabilities descriptor that gates adapter defaults.

Bot Platform Plugins

Runtime registration and discovery of platform adapters.

Custom Gateway Channel (Zero-Code)

Reach this adapter from YAML adapter: or a .praisonai/channels/ drop-in.

Run Status Controller

Transport-agnostic run-progress state machine for your adapter.