# telegram-bot

> Telegram bridge to the harness stack — polling or webhook ingress, live message edits, approvals, and configurable verbosity.

| field | value |
|-------|-------|
| version | 0.3.8 |
| type | binary |
| license | Apache-2.0 |
| repo | https://github.com/iii-hq/workers |
| supported_targets | x86_64-apple-darwin, aarch64-apple-darwin, x86_64-pc-windows-msvc, aarch64-pc-windows-msvc, x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu, x86_64-unknown-linux-musl, armv7-unknown-linux-gnueabihf |
| author | iii |

## installation

```sh
iii trigger compose::add worker=telegram-bot@0.3.8
```

## dependencies

- `configuration` @ `latest`
- `harness` @ `latest`
- `queue` @ `latest`
- `session-manager` @ `latest`
- `state` @ `latest`

## readme

# telegram-bot

A Telegram bridge to the harness agent stack. The worker owns Telegram UX — commands, inline keyboards, live message edits, and approval prompts — and delegates turns, streaming, and durability to `harness`, `session-manager`, and `approval-gate`.

Update ingress uses a session-manager-style `updates` adapter: **polling** (default, no public URL) or **webhook** (HTTPS production).

## Install

```bash
iii trigger compose::add worker=harness worker=session-manager worker=llm-router \
  worker=context-manager worker=approval-gate worker=telegram-bot
```

## Quickstart (polling — default)

1. Set configuration (configuration id `telegram-bot`):

```yaml
bot_token: "${TELEGRAM_BOT_TOKEN}"
verbosity: minimal
default_model:
  provider: anthropic
  id: claude-sonnet-4
functions_allow:
  - "shell::*"
```

2. Message the bot on Telegram. The worker long-polls `getUpdates` in the background; user text is sent to `harness::send` with update-id idempotency; assistant output streams back via `session::message-updated` edits.

Commands: `/start` (new session + model picker or default model), `/stop` (cancel turn), `/model` (re-pick model), `/help`, `/thinking` (reasoning depth), `/verbosity` (transcript detail), `/settings`.

Each `/start` clears the chat's active harness session. The next message after model selection creates a fresh session (harness-assigned ID); later messages continue that session until the next `/start`.

Assistant output streams via `sendMessageDraft` when available (Bot API 9.3+), with `editMessageText` fallback. Model thinking blocks stream via `sendRichMessageDraft` (`RichBlockThinking`). Drafts are finalized to persistent messages on turn completion.

The worker does not send a `sendChatAction(typing)` indicator: Telegram has no stop-typing API and each action lingers ~5s on clients, leaving the indicator visible for seconds after the answer arrives. Progress is shown by streamed thinking/answer drafts (draft transport) or the streamed message bubble (edit transport) instead.

When a tool call needs approval, the bot posts an inline keyboard: **Approve**, **Reject**, and **Approve always** (per-session grant via `approval::approve-always`).

## Webhook mode (production)

```yaml
bot_token: "${TELEGRAM_BOT_TOKEN}"
updates:
  name: webhook
  config:
    base_url: "https://your-engine.example"   # iii engine root only
    secret: "your-webhook-secret"             # recommended
```

`base_url` is the **public root of your iii engine** — the bot appends its own
path (`/telegram-bot/webhook`) to build the URL handed to Telegram, so operators
never repeat the path. Selecting the `webhook` adapter (at boot or via
hot-reload) does everything automatically, with no restart:

1. registers the `telegram-bot/webhook` HTTP route on the engine, then
2. calls Telegram `setWebhook` with `{base_url}/telegram-bot/webhook`.

Switching back to `polling` reverses both — it calls `deleteWebhook` and then
removes the HTTP route. No manual step is required.

> The legacy full-URL form (`url: https://…/telegram-bot/webhook`) is still
> accepted for backward compatibility. A `secret` is strongly recommended:
> without it, anyone who learns the URL can inject forged updates.

`POST /telegram-bot/set-webhook` remains available to manually re-arm Telegram
(e.g. if it dropped the webhook) without changing configuration.

## Configuration

All fields hot-reload through the `configuration` worker — no restart required, including `bot_token` and `updates` adapter swaps.

| Field | Description |
|---|---|
| `bot_token` | **Required.** Telegram Bot API token (`${TELEGRAM_BOT_TOKEN}`) |
| `updates` | Ingress adapter: `polling` (default) or `webhook` — see below |
| `default_model` | Skip model picker when set (`provider` + `id`) |
| `verbosity` | `none` \| `minimal` \| `high` \| `debug` — controls transcript mirroring |
| `default_thinking_level` | Optional harness reasoning depth: `minimal` \| `low` \| `medium` \| `high` \| `xhigh` |
| `streaming` | Draft streaming — `transport` (`auto`/`draft`/`edit`), `draft_id_seed`, `draft_throttle_ms`, `create_settle_ms` |
| `steering_mode` | `steering` (default, harness merge) or `fifo` (local queue) |
| `functions_allow` | Globs for `harness::send` `options.functions.allow` |
| `system_prompt` | Optional system prompt on every send |
| `system_prompt_mode` | `override` (default, replaces the harness prompt) or `enrich` (appends `system_prompt` to it) |
| `channel_context` | `auto` (default, injects the built-in Telegram channel-context prompt) or `off` |
| `timeout_ms` | Timeout for harness, approval, state, and configuration RPCs (default `10000`) |

### Telegram channel awareness

By default (`channel_context: auto`) the bot layers a built-in **channel-context
prompt** onto every send, telling the agent that it is talking to a Telegram
user: its reply text is the only thing the user sees (no visible console — so
"log a reminder" reaches no one), how to format for Telegram, and how to reach
the user *later* (reminders/schedules) by targeting this session. The prompt
carries the live `chat_id` and `session_id`.

The agent reaches the user out-of-band via **`telegram-bot::notify`**
(`{ session_id, text, parse_mode? }`) — a session-scoped send that delivers only
to the chat bound to that session. For reminders the agent binds a `cron`
trigger to `telegram-bot::notify` (fixed text) or `harness::send` (generated
reply) targeting this session.

Composition with `system_prompt_mode`: `enrich` layers `[channel context] +
[system_prompt]` on top of the harness's built-in prompt; `override` uses
`[channel context] + [system_prompt]` alone. Set `channel_context: off` to drop
the channel layer entirely.

### `updates` adapter

**Polling** (default):

```yaml
updates:
  name: polling
  config:
    timeout_seconds: 50   # optional, Telegram max 50
```

**Webhook**:

```yaml
updates:
  name: webhook
  config:
    base_url: "https://your-engine.example"   # iii engine root; required
    secret: "your-webhook-secret"             # recommended
```

The bot derives the Telegram webhook URL as `{base_url}/telegram-bot/webhook`
and registers/removes the HTTP route as the adapter is switched to/from
`webhook` — see [Webhook mode](#webhook-mode-production).

Verbosity levels:

- `none` — final assistant text, errors, approvals only (thinking still shown via native rich draft)
- `minimal` — same as `none` for transcript mirroring; thinking uses RichBlockThinking regardless
- `high` — also mirrors function call blocks
- `debug` — also mirrors function result entries

Persisted at `./data/configuration/telegram-bot.yaml` when using the default fs adapter.

Final assistant messages are rendered as Telegram HTML (LLM markdown converted to bold, code, links, lists). Live streaming edits stay plain text until finalization to avoid broken partial markup.

## Trace correlation

Telegram ingress and harness bindings share OpenTelemetry baggage so the console can group traces by session and turn:

- **`iii.session.id`** — harness session id (or `pending-{chat_id}` before the first send in a chat)
- **`iii.message.id`** — `tg-{update_id}` at ingress; harness `turn_id` after `harness::send` returns and for binding handlers

`harness::send` receives `options.metadata` with `{ session_id, message_id, surface: "telegram" }` for engine passthrough. Polling mode stamps baggage per update in the poller; webhook mode stamps it in the HTTP handler.

## Local development & testing

```bash
cargo run --release -- --url ws://127.0.0.1:49134 --config ./tests/fixtures/config.yaml
cargo test
UPDATE_GOLDENS=1 cargo test   # after schema changes
```

HTTP endpoints (engine default `http://127.0.0.1:3000`):

- `POST /telegram-bot/webhook` — Telegram update ingress. Registered **only while the `webhook` adapter is active**; removed in polling mode.
- `POST /telegram-bot/set-webhook` — manually (re-)register the Telegram webhook from config. Always available.

## skills

This version ships 1 skill file for agents, also browsable on the worker page:

- [`SKILL.md`](https://workers.iii.dev/workers/telegram-bot?tab=skills&skill=SKILL.md&version=0.3.8)

### SKILL.md

# telegram-bot

The telegram-bot is a bridge between a Telegram bot and the harness agent
stack. It owns the Telegram UX — slash commands, model-picker keyboards, live
message edits, and approval prompts — and delegates turn execution, streaming,
and durability to `harness`, `session-manager`, and `approval-gate`. Inbound
text becomes a `harness::send` turn; the assistant's output flows back as
Telegram messages. It is not a general bot framework and exposes no scriptable
"send a message" surface of its own.

Reach for it to put an existing harness agent in front of Telegram users.
Prerequisites: the sibling stack installed (`harness session-manager llm-router
context-manager approval-gate`) and a `telegram-bot` configuration entry with
a `bot_token`. A model is chosen per chat through the `/start` picker or fixed
with `default_model`; the agent only gets the tools you grant via
`functions_allow`. Update ingress is either long-polling (default, no public
URL) or an HTTPS webhook, and every config field hot-reloads.

## When to Use

- Expose a harness agent to Telegram users with streaming replies and live edits.
- Let a chat user pick or switch models and steer or cancel a running turn from chat.
- Approve or reject held tool calls inline from Telegram (Approve / Reject / Approve always).
- Mirror a session's transcript into a chat at a configurable verbosity.

## Boundaries

- No public "send arbitrary text to a chat" function. Output reaches a chat only
  as the rendered result of a harness turn on a session mapped to that chat.
- No scheduler, cron, or timer. The worker never fires delayed or proactive
  messages on its own — see Message flow for how a reminder is actually delivered.
- Renders output only for sessions it holds a `chat_id ↔ session_id` mapping for;
  a turn on an unrelated session produces nothing in Telegram.
- Turn execution, tool-calling, and approval policy live in `harness` and
  `approval-gate`; this worker is only the Telegram surface. Deploying it without
  the sibling stack is unsupported.

## Functions

HTTP-triggered (operator/ingress only — not agent-callable):

- `telegram-bot::webhook` — receive a Telegram update in webhook mode and
  route commands, messages, and callback queries.
- `telegram-bot::set-webhook` — register the configured webhook URL with
  Telegram.

Internal bindings (auto-registered against sibling triggers; not called directly):

- `telegram-bot::on-message-added` — post each new assistant or
  function_result entry into the chat (from `session::message-added`).
- `telegram-bot::on-message-updated` — stream throttled assistant edits (from
  `session::message-updated`).
- `telegram-bot::on-status-changed` — show a typing indicator while the
  session is working (from `session::status-changed`).
- `telegram-bot::on-turn-completed` — finalize streaming, post the outcome
  toast, and drain the FIFO queue (from `harness::turn-completed`).
- `telegram-bot::on-pending-created` — show an inline approval keyboard for a
  held call (from `approval::pending-created`).
- `telegram-bot::on-pending-resolved` — clear the approval prompt once the
  call resolves (from `approval::pending-resolved`).
- `telegram-bot::on-config-change` — hot-reload configuration.

## Message flow

The worker registers no trigger type for others to bind. It is a bidirectional
bridge between one Telegram chat and one harness session, and that round-trip is
the whole job.

Inbound: a Telegram update arrives by long-poll or webhook and lands in the
`telegram-bot::webhook` handler. Slash commands are handled locally; any other
text is sent to `harness::send` with the chat's session, the selected model, the
composed system prompt (built-in Telegram channel context per `channel_context`
plus the configured `system_prompt`, sent per `system_prompt_mode`), and the
`functions_allow` policy. The first message in a chat mints the session id
bot-side and persists a `chat_id ↔ session_id` mapping; later messages continue
that session until the next `/start`.

Proactive outbound: the agent can reach the user out-of-band via
`telegram-bot::notify` (session-scoped), e.g. a cron-bound reminder targeting
this session.

Outbound: as the turn runs, `session-manager`, `harness`, and `approval-gate`
emit events. The worker's bound handlers consume them and render into the
originating chat — resolving which chat by looking the session up in the
`chat_id ↔ session_id` mapping. Assistant text streams via message edits (or
native drafts) and is finalized on `harness::turn-completed`.

Reminder round-trip: a user asking the bot to "remind me at 5pm" gets an
immediate reply in the same turn — that is just the normal inbound-to-outbound
path. The later notification is a *second* turn: when the time comes, something
must call `harness::send` on that same chat-bound `session_id`, and its assistant
output then streams back through the same bindings. Because this worker has no
scheduler and no push API, the timing lives elsewhere (an external scheduler,
cron, or another worker that holds the `session_id`); the bridge's only role is
routing that session's output back to the chat.

## api reference

```json
{
  "functions": [
    {
      "description": "Internal: track approval-gate worker add/remove.",
      "metadata": {},
      "name": "telegram-bot::__on-approval-gate-worker",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "operation": {
            "default": null,
            "type": [
              "string",
              "null"
            ]
          },
          "source": {
            "default": null
          },
          "stage": {
            "default": null,
            "type": [
              "string",
              "null"
            ]
          },
          "worker": {
            "default": null,
            "type": [
              "string",
              "null"
            ]
          }
        },
        "title": "WorkerLifecycleEvent",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "ok": {
            "type": "boolean"
          }
        },
        "required": [
          "ok"
        ],
        "title": "WorkerLifecycleAck",
        "type": "object"
      }
    },
    {
      "description": "Send a message to the Telegram chat bound to a session. Use this to reach the user out-of-band (e.g. a scheduled reminder or when a long task finishes); the message is delivered only to that session's chat.",
      "metadata": {},
      "name": "telegram-bot::notify",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "parse_mode": {
            "description": "Optional Telegram `parse_mode` (`MarkdownV2` / `HTML`). When set, `text` is sent verbatim with that mode instead of being auto-formatted.",
            "type": [
              "string",
              "null"
            ]
          },
          "session_id": {
            "description": "The session whose chat receives the message. Must be a session this bot created (e.g. the `session_id` from the channel-context prompt).",
            "type": "string"
          },
          "text": {
            "description": "The message text. Markdown is auto-formatted for Telegram unless `parse_mode` is set explicitly. Long text is split across messages.",
            "type": "string"
          }
        },
        "required": [
          "session_id",
          "text"
        ],
        "title": "NotifyRequest",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "delivered": {
            "type": "boolean"
          },
          "message_ids": {
            "description": "Telegram message ids of the (possibly chunked) messages sent.",
            "items": {
              "format": "int64",
              "type": "integer"
            },
            "type": "array"
          }
        },
        "required": [
          "delivered",
          "message_ids"
        ],
        "title": "NotifyResponse",
        "type": "object"
      }
    },
    {
      "description": "Internal: reload telegram-bot configuration from the authoritative store on change.",
      "metadata": {
        "internal": true
      },
      "name": "telegram-bot::on-config-change",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "title": "ConfigChangeRequest",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "ok": {
            "type": "boolean"
          }
        },
        "required": [
          "ok"
        ],
        "title": "ConfigChangeAck",
        "type": "object"
      }
    },
    {
      "description": "Create a Telegram message for each new assistant or function_result entry.",
      "metadata": {},
      "name": "telegram-bot::on-message-added",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "AgentMessage": {
            "properties": {
              "content": {
                "items": {
                  "$ref": "#/definitions/ContentBlock"
                },
                "type": "array"
              },
              "role": {
                "type": "string"
              }
            },
            "required": [
              "role"
            ],
            "type": "object"
          },
          "ContentBlock": {
            "oneOf": [
              {
                "properties": {
                  "text": {
                    "type": "string"
                  },
                  "type": {
                    "enum": [
                      "text"
                    ],
                    "type": "string"
                  }
                },
                "required": [
                  "text",
                  "type"
                ],
                "type": "object"
              },
              {
                "properties": {
                  "text": {
                    "type": "string"
                  },
                  "type": {
                    "enum": [
                      "thinking"
                    ],
                    "type": "string"
                  }
                },
                "required": [
                  "text",
                  "type"
                ],
                "type": "object"
              },
              {
                "properties": {
                  "arguments": true,
                  "function_id": {
                    "type": "string"
                  },
                  "id": {
                    "type": "string"
                  },
                  "type": {
                    "enum": [
                      "function_call"
                    ],
                    "type": "string"
                  }
                },
                "required": [
                  "arguments",
                  "function_id",
                  "id",
                  "type"
                ],
                "type": "object"
              },
              {
                "properties": {
                  "type": {
                    "enum": [
                      "other"
                    ],
                    "type": "string"
                  }
                },
                "required": [
                  "type"
                ],
                "type": "object"
              }
            ]
          }
        },
        "properties": {
          "entry_id": {
            "type": "string"
          },
          "message": {
            "anyOf": [
              {
                "$ref": "#/definitions/AgentMessage"
              },
              {
                "type": "null"
              }
            ]
          },
          "parent_id": {
            "default": null,
            "description": "Parent entry in the session chain (authoritative append order).",
            "type": [
              "string",
              "null"
            ]
          },
          "session_id": {
            "type": "string"
          },
          "timestamp": {
            "default": 0,
            "description": "Append time in ms; used as the per-entry ordering key.",
            "format": "int64",
            "type": "integer"
          }
        },
        "required": [
          "entry_id",
          "session_id"
        ],
        "title": "MessageAddedEvent",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "ok": {
            "type": "boolean"
          }
        },
        "required": [
          "ok"
        ],
        "title": "BindingAck",
        "type": "object"
      }
    },
    {
      "description": "Stream assistant edits into Telegram, throttled by revision.",
      "metadata": {},
      "name": "telegram-bot::on-message-updated",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "AgentMessage": {
            "properties": {
              "content": {
                "items": {
                  "$ref": "#/definitions/ContentBlock"
                },
                "type": "array"
              },
              "role": {
                "type": "string"
              }
            },
            "required": [
              "role"
            ],
            "type": "object"
          },
          "ContentBlock": {
            "oneOf": [
              {
                "properties": {
                  "text": {
                    "type": "string"
                  },
                  "type": {
                    "enum": [
                      "text"
                    ],
                    "type": "string"
                  }
                },
                "required": [
                  "text",
                  "type"
                ],
                "type": "object"
              },
              {
                "properties": {
                  "text": {
                    "type": "string"
                  },
                  "type": {
                    "enum": [
                      "thinking"
                    ],
                    "type": "string"
                  }
                },
                "required": [
                  "text",
                  "type"
                ],
                "type": "object"
              },
              {
                "properties": {
                  "arguments": true,
                  "function_id": {
                    "type": "string"
                  },
                  "id": {
                    "type": "string"
                  },
                  "type": {
                    "enum": [
                      "function_call"
                    ],
                    "type": "string"
                  }
                },
                "required": [
                  "arguments",
                  "function_id",
                  "id",
                  "type"
                ],
                "type": "object"
              },
              {
                "properties": {
                  "type": {
                    "enum": [
                      "other"
                    ],
                    "type": "string"
                  }
                },
                "required": [
                  "type"
                ],
                "type": "object"
              }
            ]
          }
        },
        "properties": {
          "entry_id": {
            "type": "string"
          },
          "message": {
            "$ref": "#/definitions/AgentMessage"
          },
          "revision": {
            "format": "uint64",
            "minimum": 0,
            "type": "integer"
          },
          "session_id": {
            "type": "string"
          },
          "timestamp": {
            "default": 0,
            "description": "Mutation time in ms; refines the per-entry ordering key (min wins).",
            "format": "int64",
            "type": "integer"
          }
        },
        "required": [
          "entry_id",
          "message",
          "revision",
          "session_id"
        ],
        "title": "MessageUpdatedEvent",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "ok": {
            "type": "boolean"
          }
        },
        "required": [
          "ok"
        ],
        "title": "BindingAck",
        "type": "object"
      }
    },
    {
      "description": "Send an inline approval keyboard when a function call is held.",
      "metadata": {},
      "name": "telegram-bot::on-pending-created",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "arguments_excerpt": {
            "default": null
          },
          "function_call_id": {
            "type": "string"
          },
          "function_id": {
            "type": "string"
          },
          "session_id": {
            "type": "string"
          }
        },
        "required": [
          "function_call_id",
          "function_id",
          "session_id"
        ],
        "title": "PendingApprovalRecord",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "ok": {
            "type": "boolean"
          }
        },
        "required": [
          "ok"
        ],
        "title": "BindingAck",
        "type": "object"
      }
    },
    {
      "description": "Clear the approval prompt when a held call is resolved.",
      "metadata": {},
      "name": "telegram-bot::on-pending-resolved",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "function_call_id": {
            "type": "string"
          },
          "outcome": {
            "type": "string"
          },
          "session_id": {
            "type": "string"
          }
        },
        "required": [
          "function_call_id",
          "outcome",
          "session_id"
        ],
        "title": "PendingResolvedEvent",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "ok": {
            "type": "boolean"
          }
        },
        "required": [
          "ok"
        ],
        "title": "BindingAck",
        "type": "object"
      }
    },
    {
      "description": "Observe session status changes.",
      "metadata": {},
      "name": "telegram-bot::on-status-changed",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "session_id": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "status_reason": {
            "default": null,
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "session_id",
          "status"
        ],
        "title": "StatusChangedEvent",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "ok": {
            "type": "boolean"
          }
        },
        "required": [
          "ok"
        ],
        "title": "BindingAck",
        "type": "object"
      }
    },
    {
      "description": "Finalize streaming, drain FIFO queue, and post turn outcome toasts.",
      "metadata": {},
      "name": "telegram-bot::on-turn-completed",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "reason": {
            "default": null,
            "type": [
              "string",
              "null"
            ]
          },
          "result_error": {
            "default": null,
            "type": [
              "string",
              "null"
            ]
          },
          "session_id": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "terminal": {
            "default": true,
            "description": "`false` while the session still owns an armed wake: the run continues and a later turn carries the real outcome. Missing (legacy harness) → terminal.",
            "type": "boolean"
          },
          "turn_id": {
            "type": "string"
          }
        },
        "required": [
          "session_id",
          "status",
          "turn_id"
        ],
        "title": "TurnCompletedEvent",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "ok": {
            "type": "boolean"
          }
        },
        "required": [
          "ok"
        ],
        "title": "BindingAck",
        "type": "object"
      }
    },
    {
      "description": "Register the Telegram webhook URL from configuration.",
      "metadata": {},
      "name": "telegram-bot::set-webhook",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "title": "SetWebhookRequest",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "url": {
            "type": "string"
          }
        },
        "required": [
          "ok",
          "url"
        ],
        "title": "SetWebhookResponse",
        "type": "object"
      }
    },
    {
      "description": "Receive Telegram updates; route commands, messages, and callbacks.",
      "metadata": {},
      "name": "telegram-bot::webhook",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "body": {
            "default": null
          },
          "headers": {
            "additionalProperties": true,
            "default": null,
            "type": [
              "object",
              "null"
            ]
          }
        },
        "title": "HttpTriggerRequest",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "ok": {
            "type": "boolean"
          }
        },
        "required": [
          "ok"
        ],
        "title": "WebhookResponse",
        "type": "object"
      }
    }
  ],
  "triggers": []
}
```
