Telegram Setup
Create a BotFather bot, register an authenticated webhook through the AGH setup wizard, and prove topic-aware delivery.
- Audience
- Operators running durable agent work
- Focus
- Bridges guidance shaped for scanability, day-two clarity, and operator context.
This guide connects a Telegram bot to one AGH workspace. AGH uses Telegram webhooks, not long polling. The guided setup creates the disabled bridge, stores the bot token and webhook secret, and can register the webhook with Telegram before the provider begins accepting routed work.
How the Telegram bridge behaves
| Surface | AGH behavior |
|---|---|
| Inbound transport | Receives Bot API webhook updates for messages, edited messages, channel posts, and edited channel posts. |
| Authentication | Requires the configured X-Telegram-Bot-Api-Secret-Token when the webhook listener is enabled. |
| Routing | One instance selects one shape: private chat (peer), ordinary group (group), or forum topic (group + thread). The guided default is forum-topic routing. |
| Reply context | Uses the embedded reply_to_message snapshot first, then a bounded local cache. It does not fetch arbitrary chat history on a cache miss. |
| Outbound delivery | Uses sendMessage, editMessageText, and deleteMessage. MarkdownV2 parse failures retry once as plain text. |
| Long responses | Splits after MarkdownV2 formatting at 4,096 UTF-16 code units, preserving ordered, numbered, fence-balanced continuations in the same topic. |
| Tool progress | Defaults to new + accumulate with edits, typing actions, and reactions enabled. The final answer is sent separately in the same chat/topic. |
Before you start
| Value | Required | Where it comes from |
|---|---|---|
| Bot token | yes | BotFather after /newbot; format is <bot-id>:<token>. |
| Webhook secret | yes here | Operator-provided or generated by guided setup; Telegram returns it in the webhook request header. |
| Public callback URL | yes | Stable public HTTPS URL that forwards to the Telegram provider listener. |
| Local listener address | yes | provider_config.webhook.listen_addr or AGH_BRIDGE_TELEGRAM_LISTEN_ADDR. |
| AGH workspace ID | yes | Workspace whose default agent receives the routed Telegram turn. |
| Chat administration | groups | Permission to add the bot and, when required, change BotFather privacy mode. |
Before configuring Telegram, prove that the locally built provider is installed and visible in the daemon catalog:
PROVIDER=telegram
agh extension status "$PROVIDER" -o json
curl -sS http://127.0.0.1:2123/api/bridges/providers | \
jq --arg provider "$PROVIDER" '.providers[] | select(.extension_name == $provider)'Continue only when the extension is enabled and the catalog row is not disabled or unhealthy. If either command has no provider, follow Install an in-tree provider first.
Do not paste the bot token into a URL stored in shell history or documentation. Guided setup sends it through the write-only binding operation.
Step 1: Create the bot in BotFather
- Open the verified
@BotFatheraccount in Telegram. - Run
/newbot. - Choose the display name and unique username.
- Copy the bot token once and place it in your secret manager or current shell environment.
- Optionally use
/setdescription,/setabouttext, and/setuserpicto make the bot recognizable.
The token controls the bot. If it leaks, revoke it in BotFather and replace the AGH binding before reenabling the bridge.
Step 2: Decide group privacy behavior
Telegram privacy mode changes which group messages the bot receives:
- Keep privacy mode enabled when commands, replies, and explicit mentions are enough.
- Disable privacy mode when the workspace agent must receive ordinary group conversation.
Use BotFather /setprivacy, select the bot, and choose the intended setting before testing a group.
After a privacy change, remove and re-add the bot when Telegram requires membership refresh.
Privacy mode is a Telegram delivery filter. AGH dm_policy and route dimensions remain separate
controls.
Step 3: Map the public and local endpoints
Telegram → https://bridge.example.com/telegram/support
→ reverse proxy or tunnel
→ http://127.0.0.1:18082/telegram/supportThe proxy must preserve the secret-token header and the exact path. The provider accepts JSON POSTs up to its bounded request size and deduplicates update IDs.
Step 4: Run guided setup
Interactive setup prompts for the workspace, public URL, local path, routing shape, bot token, and webhook secret:
agh bridge setup telegramThe normal path creates the instance disabled, writes both secret bindings, and asks the provider to
call Telegram setWebhook. The setup result includes:
bridge_instance_id;- masked secret slots;
- webhook registration status and remediation;
- the next verification command.
Copy the returned bridge ID:
BRIDGE_ID=brg_123Select one routing shape
Every enabled route dimension is required on every inbound event. The wizard therefore asks for one closed routing shape and persists it as part of creation:
| Wizard value | Stored routing_policy | Accepts |
|---|---|---|
private | include_peer=true, group/thread false | One-to-one bot chats |
group | include_group=true, peer/thread false | Ordinary groups without a topic |
forum | include_group=true, include_thread=true, peer false — guided default | Forum topics |
Choose the shape that you can prove immediately after setup. On a rerun with --instance, the
wizard reads the stored policy and presents the matching value instead of silently resetting it.
Do not enable all three dimensions to make one instance “universal”; that policy would require peer, group, and thread IDs on every event. If the bot must serve more than one shape, create a separate bridge instance for each shape and give each instance its own callback path. See Message routing for the identity contract.
Headless setup
Strict JSON input is useful for agents and automation:
agh bridge setup telegram --json <<'JSON'
{
"scope": "workspace",
"workspace_id": "ws_8f33a913d23c4fd1",
"routing_policy": {
"include_peer": true,
"include_group": false,
"include_thread": false
},
"webhook_public_url": "https://bridge.example.com/telegram/support",
"webhook_path": "/telegram/support",
"bot_token": "123456:abcdefghijklmnopqrstuvwxyzABCDE",
"webhook_secret": "replace-with-a-high-entropy-secret"
}
JSONStrict JSON accepts the typed routing_policy, not the wizard-only routing_shape label. Use
exactly one of these shapes:
{
"include_peer": true,
"include_group": false,
"include_thread": false
}{
"include_peer": false,
"include_group": true,
"include_thread": false
}{
"include_peer": false,
"include_group": true,
"include_thread": true
}The first object is private, the second is an ordinary group, and the third is a forum topic. Mixed, empty, unknown, and trailing JSON values are rejected before AGH creates or updates an instance. Secret values are masked in the result.
If webhook_secret is omitted during normal setup, AGH can generate and consume it without printing
it. --reveal-generated-secrets is JSON-only and reveals only values generated during that one
invocation; supplied and pre-existing secrets are never returned.
Print the registration request without calling Telegram
--print-only emits a safe setWebhook curl template. The template uses environment-variable
placeholders rather than embedding the bot token in the URL or the secret in the command text:
agh bridge setup telegram --json --print-only < telegram-setup.jsonSupply webhook_secret in the JSON input. If AGH generates it for a print-only run, add
--reveal-generated-secrets; otherwise setup stops before writing anything because the external
command would depend on a value the operator cannot recover.
Step 5: Verify the disabled bridge
agh bridge verify "$BRIDGE_ID" --jsonExpected evidence:
provider.identitypasses when the bot token succeeds against TelegramgetMe;webhook.secretpasses when the secret binding exists;- webhook reachability is
skippedwhile disabled.
The setup registration result proves Telegram accepted the setWebhook request. It does not prove
that the callback remains reachable or that a chat is permitted by privacy and DM policy.
Step 6: Control direct-message access
Do this before enabling a private-chat bridge. An omitted or empty dm_policy normalizes to open
and admits every Telegram user whose private-chat update passes webhook authentication. Use stable
numeric Telegram user IDs where possible:
{
"dm": {
"allow_user_ids": ["123456789"],
"allow_usernames": ["alice"],
"paired_user_ids": [],
"paired_usernames": []
}
}agh bridge create and agh bridge update do not currently expose --dm-policy. Use the Web editor
or HTTP API procedure in Control direct-message access, then keep the bridge disabled
until agh bridge get "$BRIDGE_ID" -o json reports the intended policy and lists.
pairing reads pre-populated paired_user_ids and paired_usernames, then falls back to
allow_user_ids and allow_usernames; it does not start an interactive pairing flow. Usernames can
change, so numeric IDs are the safer long-lived rule.
dm_policy applies only to private chats. BotFather privacy mode, group membership, Telegram's event
delivery, and the bridge routing policy govern groups and channel posts. A group-only or forum bridge
still rejects a private-chat route even when the DM policy is open.
Step 7: Enable and confirm the selected shape
agh bridge enable "$BRIDGE_ID"
agh bridge verify "$BRIDGE_ID" --json
agh bridge get "$BRIDGE_ID" -o jsonThe routing_policy in bridge get must exactly match the shape selected above. A mismatch is a
setup failure even if provider identity checks pass. Do not continue to the provider test until this
checkpoint is correct.
Step 8: Prove one real route and reply
Use only the subsection that matches this instance. Trigger one inbound event first, inspect the persisted route, and reuse its numeric provider IDs for the real outbound test.
Private chat — peer-only instance
Open the bot in Telegram, select Start, and send a short message. Then inspect the route:
agh bridge routes "$BRIDGE_ID" -o jsonThe route must contain peer_id and no required group/thread dimensions. Use that numeric chat ID:
agh bridge send-test "$BRIDGE_ID" \
--message "AGH Telegram connection check" \
--peer-id "123456789" \
--jsonThe result should include a numeric Telegram remote message ID.
Ordinary group — group-only instance
- Add the bot to a test group.
- Confirm BotFather privacy mode matches the events you expect.
- Send an allowed command, reply, mention, or ordinary group message.
- Inspect
agh bridge routesand confirm the route hasgroup_idwithout a required thread.
agh bridge send-test "$BRIDGE_ID" \
--message "AGH Telegram group check" \
--group-id "-1001234567890" \
--jsonForum topic — guided default
- Add the bot to a forum-enabled group.
- Send an allowed event inside the target topic.
- Inspect
agh bridge routesand confirm bothgroup_idandthread_id. Telegram's general forum topic normalizes to thread1.
Reply-mode delivery reuses the route's topic:
agh bridge send-test "$BRIDGE_ID" \
--message "AGH Telegram topic check" \
--group-id "-1001234567890" \
--thread-id "42" \
--mode reply \
--jsonDo not substitute the group title or topic label for provider IDs.
Delivery and progress behavior
Telegram MarkdownV2 escaping happens before the provider measures the 4,096 UTF-16-unit wire limit. Long terminal answers are split on natural boundaries, with code fences closed and reopened where needed. If Telegram rejects a chunk's MarkdownV2 syntax, that chunk is retried once as plain text.
Telegram defaults to:
{
"progress": {
"tool_progress": "new",
"grouping": "accumulate",
"typing": true,
"reactions": true
}
}Typing has no explicit clear operation in the Bot API. The provider stops issuing typing actions when content is delivered and closes progress state at terminal delivery.
For a quieter mobile surface:
agh bridge update "$BRIDGE_ID" \
--delivery-progress off \
--delivery-progress-typing=false \
--delivery-progress-reactions=falseConfiguration reference
| Field | Default | Purpose |
|---|---|---|
webhook.public_url | none | Complete public callback registered through Telegram setWebhook. |
webhook.listen_addr | AGH_BRIDGE_TELEGRAM_LISTEN_ADDR | Local listener shared by Telegram instances. |
webhook.path | /telegram/<bridge-id> | Local callback path; each instance needs a unique path. |
batching.delay_ms | 0 | Enables inbound batching when positive. |
batching.split_delay_ms | delay_ms | Delay after the configured batching threshold. |
batching.split_threshold | 0 | Provider batching split threshold. |
dm.allow_user_ids / paired_user_ids | empty | Stable user IDs admitted by allowlist or pairing. |
dm.allow_usernames / paired_usernames | empty | Username fallbacks; IDs are safer. |
bot_token is required by the provider manifest. webhook_secret is optional at the catalog level,
but the in-tree webhook listener requires it when enabled. Guided setup always establishes both.
The Telegram Bot API destination is an operator-owned process setting. Bridge configuration cannot redirect the bot token to another host.
Common operations
Rotate the bot token or webhook secret
Disable the bridge, then rerun setup against the existing instance:
agh bridge disable "$BRIDGE_ID"
agh bridge setup telegram --instance "$BRIDGE_ID"
agh bridge enable "$BRIDGE_ID"
agh bridge verify "$BRIDGE_ID" --jsonRerun setWebhook when the public URL or webhook secret changes. Telegram registration is rejected
while the bridge is enabled so configuration cannot change underneath a live listener.
Inspect Telegram's webhook state
Use the Bot API getWebhookInfo through an operator-controlled secret-aware environment if setup
reports a registration problem. Compare the returned URL with provider_config.webhook.public_url
and inspect Telegram's last error. Do not paste the token-bearing request into tickets or logs.
Troubleshooting
| Symptom | What to check |
|---|---|
| Setup rejects the token format | Copy the full BotFather token, including numeric bot ID and colon. |
provider.identity fails | Revoke/reissue the BotFather token if needed, replace the binding, and rerun setup/verification. |
| Registration reports auth required | The bot token or webhook secret binding is missing or invalid; keep the bridge disabled while repairing it. |
| Telegram reports webhook delivery errors | Check DNS, TLS, public path forwarding, and that the provider listener is running at the configured local address. |
| Provider returns unauthorized for inbound updates | Confirm setWebhook.secret_token and the AGH webhook_secret binding are identical and the proxy preserves the header. |
| Direct messages work but group chatter is silent | Check BotFather privacy mode, group membership, command/mention behavior, and AGH routing/DM policy. |
| Replies leave the forum topic | Inspect the route's message_thread_id and keep include_thread enabled for topic isolation. |
| MarkdownV2 errors appear | The provider retries the rejected chunk as plain text; inspect upstream content only if failures continue beyond that fallback. |
| A second Telegram instance degrades | Keep one shared listener address and assign a distinct webhook.path to each instance. |
| Reactions fail while text succeeds | Treat reactions as an optional affordance; disable them while checking Bot API permissions and target support. |
Security notes
- Treat the BotFather token as a root credential for the bot.
- Use a high-entropy webhook secret and rotate it together on Telegram and AGH.
- Prefer numeric user allowlists to mutable usernames.
- Restrict the public proxy to the configured Telegram paths.
- Never expose a token-bearing Bot API URL in logs, shell history, screenshots, or issue reports.
Completion checklist
- The Telegram extension is installed, enabled, and healthy in the provider catalog.
setWebhookaccepted the exact public URL and secret-token configuration.- Disabled identity and secret checks pass.
- A private-chat bridge reports the intended
dm_policyand sender lists before enablement. - Enabled public reachability passes or has an explained warning.
- A real event of the selected shape creates a route in the intended workspace.
- The route contains only the peer, group, or group-plus-thread dimensions selected during setup.
send-testreturns a Telegram message ID.- The bridge returns to
readyafter one restart.