Skip to main content

Telegram

Send, edit, pin and delete messages through the Bot API — paced to Telegram's per-chat limit rather than retrying into it.

npm i @forge/tools-telegram

Tools

ToolEffectApprovalNotes
telegram_get_chatreadneverConfirms the bot can see a chat before sending to it
telegram_send_messageexternal-writealwaysPaced per chat
telegram_send_mediaexternal-writealwaysOne photo or document by URL. Paced
telegram_edit_messageexternal-writealwaysThe bot's own messages only
telegram_pin_messageexternal-writealwaysVisible to every member. Silent by default
telegram_delete_messagedestructivealwaysIrreversible; under 48 hours old only

There is no getUpdates tool. Inbound delivery is a deployment's webhook, not a tool call — a polling tool would compete with it for the same updates, and Telegram delivers each one once.

Wire it up

import { createAgent } from "@forge/agentkit/providers";
import { createStaticCredentialResolver } from "@forge/agentkit/tools";
import { createTelegramToolkit } from "@forge/tools-telegram";

const agent = createAgent({
manifest: {
id: "notifier",
name: "Notifier",
instructions: "Answer questions in the team group. Keep replies short.",
modelPolicy: { role: "smart" },
},
tools: [
createTelegramToolkit({
credentialRef: "telegram",
resolver: createStaticCredentialResolver({ telegram: process.env.TELEGRAM_BOT_TOKEN ?? "" }),
}),
],
});

Credentials and scopes

A bot token from @BotFather. Telegram has no scopes: the token carries everything the bot can do, and the constraints are about where it has been added rather than what it may call.

The token goes in the URL path/bot<token>/sendMessage — not a header, which is why this package places the credential itself rather than letting the shared header helper do it. It is still resolved per call through credentialRef, so one deployment can serve several bots.

Then the bot must be added, and this is the step people miss:

To reachWhat is required
A userThey must message the bot first. A bot cannot open a conversation — there is no way around this
A groupAdd the bot to the group
Reading group messagesDisable Privacy Mode in BotFather, or the bot sees only messages that mention it or reply to it
telegram_delete_message in a groupThe bot must be an administrator
telegram_pin_message in a groupAdministrator with Pin Messages

Privacy Mode is on by default and is the usual explanation for a bot that "sees nothing" in a group.

Behaviour worth knowing

Sends are paced by construction, not by retrying into the limit. Telegram allows roughly one message a second to a chat and about twenty a minute to a group. Exceeding that earns a 429 with a retry_after, and — unlike a quota — the remedy is not to wait and try again but to not have sent that fast: Telegram escalates repeat offenders to longer cooldowns.

So sends to the same chat are spaced automatically, and sends to different chats do not wait for each other — a bot serving fifty conversations would otherwise serialise all of them behind the slowest. Concurrent sends to one chat are queued rather than both reading the same stale timestamp. An edit is not paced, because Telegram's send limit does not apply to it.

An uninvited bot fails like a bad token. 403 Forbidden: bot is not a member and 401 Unauthorized are both permission-shaped, and only the description distinguishes them. They are classified apart and name different remedies — one is adding the bot to a chat, the other is a new token from BotFather.

Telegram answers 200 with ok: false. The envelope is read rather than the status — the fourth vendor in this project to do this, after Slack, GraphQL and Reddit. A retry_after inside it becomes a real retryAfterMs rather than a guess.

Plain text is the default; MarkdownV2 is opt-in. It requires escaping a dozen characters, and one unescaped . or - makes Telegram reject the whole message.

Pinning is silent by default. A pin notifies every member of a group, and a notification to a thousand people is not a side effect an agent should cause by omission. Pass silent: false to mean it.

Message content is untrusted. It arrives fenced.

Limits

Not offeredWhy
getUpdates pollingInbound delivery is a webhook the deployment owns; a polling tool would steal updates from it
Inline keyboards and callback buttonsA button implies handling its callback, which is inbound work and not a tool call
Chat administration — promote, restrict, banPowers over people, where a wrong call cannot be walked back
Setting the bot's commands, name or descriptionStanding configuration, which is a deployment decision
Polls, dice, locations, contactsEach is a distinct message type with its own semantics; naming them as declined rather than forgotten
Media upload from bytesSends take a URL. Accepting bytes would mean this package hosting a file, which is a storage decision a toolkit should not make
Editing anyone else's messageTelegram does not permit it, and pretending otherwise would produce a tool that always fails