Skip to main content
Set approval="presentation" on your Agent and run a Telegram bot — Allow/Deny buttons render on chat, resolve against a SQLite store, and survive a restart. It works the same in standalone (Bot("telegram", ...)) and gateway mode.

Quick Start

1

Auto-wire from the agent

Set approval="presentation" on the Agent — the bot wires the durable backend to the chat on start(). No register_approval_backend() call needed.
The bot detects the backend on the agent and connects it to the chat automatically. Unknown decision values (anything outside allow, deny, always) are rejected before reaching the backend — fail closed.
2

Manual wire (advanced)

Wire a backend you constructed separately — for tests or custom bootstrap where the backend is built by hand.
register_approval_backend gives the backend the channel renderer and target, then enables the /approve callback route.

How It Works

An agent tool call persists to the store, renders Allow/Deny buttons, and unblocks only when an authorised tap resolves the approval ID. Each decision binds to an unguessable approval_id, not a message id — so a late tap after a redeploy still resolves against the SQLite store instead of falling through as an unhandled callback.

Gateway mode

The same approval="presentation" setting works when Telegram runs under the gateway — no extra wiring. The gateway’s Telegram channel handles inline-button taps itself, so pairing Approve/Deny and tool-approval taps resolve against the SQLite store without a second getUpdates poller on the token.
Gateway Telegram also forwards chat and message IDs with each turn, so duplicate-message dedup, session_scope: per_chat, and /new behave the same as standalone. A turn waiting on an approval no longer blocks the tap that resolves it.
Before PraisonAI #6102, gateway Telegram channels silently dropped inline-button taps, and allow_shell: true started a second getUpdates poller (409 Conflict errors, user messages lost during an approval). If you see “Approve/Deny does nothing”, 409 Conflict: terminated by other getUpdates request, or missing messages during a tool approval, upgrade praisonai-bot — no config change required.

What the user sees

The whole point of the feature — a non-developer only ever sees two buttons and a result. The original message updates in place to one of four outcomes:
  • ✅ Approved — an authorised reviewer tapped Allow and the tool ran.
  • ❌ Denied — an authorised reviewer tapped Deny and the tool did not run.
  • ⌛ Timed out — not run — nobody tapped before the deadline. The agent gets a fail-closed DENY, so the tool did not run.
  • ⚠️ Approval could not be processed — the tap was unauthorised, or the originating turn was /stopped or superseded while the card was still open.
Why “timed out” is distinct from “denied”. Before PR #5916, an unanswered card was edited to ”❌ Denied”, so an operator could not tell “a reviewer said no” from “nobody saw it in time”. Both outcomes still fail closed — only the card text differs.
If the originating turn was /stopped or superseded while the Allow / Deny card was still open, the tap is dropped fail-closed by the core registry. The card updates to a “no longer applicable” state and the decision recorded is ApprovalDecision(approved=False, reason="Turn no longer live (stopped or superseded)") — no session/always grant is persisted for the abandoned turn.

Superseded turns

A reviewer can tap Allow minutes or hours after the card first appears. If the user cancelled the run (or a new turn took over) in the meantime, that late tap must not fire the tool. The core SDK enforces exactly this since PraisonAI #4950, so it applies to every backend on this page — not just the durable Telegram one:
  • The tool does not run.
  • No session/always grant is persisted for the abandoned turn.
  • The returned decision carries reason="Turn no longer live (stopped or superseded)".
See Approval Turn Liveness and Approval Protocol → Live-Authority Binding.

Configuration Options

Backend construction lives on the secure-backend page — this page only wires it to Telegram.

Secure Approval Backend

PresentationApprovalBackend constructor args (store, allowed_actors, timeout)

Durable Approvals

The SQLite ApprovalStore schema behind the durable path

Common Patterns

Auto-wire from agent (default). Set approval="presentation" on the Agent and run the bot — nothing else. This is the Quick Start path.
Manual wire with explicit target. Address approval buttons to a specific chat.
Restart drill. Restart the bot; a late Allow tap on a message sent before the restart still resolves. rehydrate_approvals() runs automatically on start() and returns the restored count.

Best Practices

The backend is fail-closed — set the actor allowlist so only your approvers can resolve a tap. Without it, no actor is authorised.
Do not pass an already-wired backend from Slack to Telegram. When a backend already owns a sender, the coupling guard leaves it untouched so it never renders on one transport while addressing another.In the gateway, shell-routed agent clones reuse the channel’s wired backend only when the target chat matches and the approver set is the same or narrower. Otherwise the clone keeps its own backend.
Reserve register_approval_backend() for tests or custom bootstrap. In normal deployments, approval="presentation" on the Agent is enough.
start() logs how many approvals were restored. Watch that number to catch a broken deploy where pending approvals never come back.

Durable Approvals

The SQLite store behind the durable path

Secure Approval Backend

The PresentationApprovalBackend this page wires

Gateway Approval Durability

The gateway sibling of this transport wiring

Messaging Bots

Deploy agents to chat channels

Approval Turn Liveness

A tap on a /stopped or superseded turn is dropped fail-closed

Live-Authority Binding

Why a late tap on a stopped turn is dropped fail-closed