---
url: https://docs.repoos.org/telegram.md
---
# Telegram

Telegram connects a bot to an existing RepoOS repository. It is a second way
for allowlisted users to reach RepoOS; it does not create new RepoOS accounts
or grant access based on group membership.

Bot credentials must be stored through RepoOS's authenticated secret store. Set
`REPOOS_SECRET_STORE_KEY` in the server process environment to a 32-byte key
encoded as 64 hexadecimal characters or base64. RepoOS refuses to encrypt or
decrypt secrets when the key is absent or invalid; it never stores them in
plaintext. Keep the key outside `repoos.toml` and out of task worktrees. Losing
the key makes existing encrypted credentials unrecoverable; changing it
requires rewrapping existing records with the old key still available.
Repositories that set `worktrees.inheritEnv = true` expose this key to their
task worktrees along with the rest of `.env`; use `false` when worktree agents
must not be able to decrypt repository credentials.

### The `telegram.enabled` master switch

`[telegram] enabled` (Settings → **Telegram bot**) gates every active
Telegram surface; Settings flips it live — no restart needed. While it is
off, connection-management calls that arm or change the integration are
refused ("the Telegram integration is disabled"), and any polling loop that
already exists goes quiet: it makes no Telegram calls and resumes on its own
when the switch returns. Two safe-direction calls stay available while off —
`transport {mode: "off"}` and `disconnect` — so stopping delivery or
forgetting the credential never requires re-enabling anything. A stored
connection and its encrypted credential are untouched by the switch, so
re-enabling restores the previous transport state.

## Connect a repository

An administrator connects Telegram from that repository's RepoOS Settings.
The connection flow provisions or configures the repository's project bot.
The bot can only send repository notifications to chats that an authenticated
RepoOS admin explicitly binds. Adding the bot to a group by itself does not
authorize that chat or give its members access.

### Bring Your Own Bot Token

You can connect without any hosted service. Create a project bot for your
repository with [@BotFather](https://t.me/BotFather), copy its token, and send
it to your RepoOS instance once, server-side, over the admin API
(`POST /api/telegram/connect`; the Settings connection panel builds on the
same routes). RepoOS validates the bot identity with Telegram, stores the
token encrypted at rest, and returns the bot's non-secret identity. The token
is never echoed back — not to the browser, not into logs, `.env`,
`repoos.toml`, or a support bundle — and it is not readable through the API
after it is stored. Disconnect first removes and confirms the webhook. For a
BYO bot, Telegram does not provide a Bot API method to revoke its token: when
prompted, revoke it in @BotFather and retry disconnect. RepoOS confirms
revocation by checking that the old token returns 401, then removes the
encrypted credential and this instance's user links and chat bindings. A
failed step is reported as incomplete and leaves the credential and bindings
available for a retry; webhook-removal progress is retained safely.

The same connection procedure registers the bot's supported profile with
Telegram: its command list (starting with `/help`), and name/description text
that mentions the repository. Unauthorized senders get silence, by design; see
[Use the bot](#use-the-bot) for the read-only commands an authorized sender can
run.

### Update transport

RepoOS can receive updates two ways, and switching between them requires no
code change above the adapter:

* **Long polling** — the instance pulls updates from Telegram at run time.
  Works anywhere, including machines with no public inbound network, and is
  the recommended default. A connection starts with the transport **off** —
  the bot receives nothing until you switch polling (or webhook) on; the
  choice persists and is re-applied automatically when the server restarts.
* **Webhook** — Telegram POSTs updates to your instance's `webhookUrl`. The
  public Bot API accepts HTTPS URLs on ports 443, 80, 88, and 8443 only; a
  Cloudflare Tunnel in front of your instance satisfies this. Telegram validates
  each delivery with a secret token RepoOS generates and stores encrypted.

Incoming updates are normalized identically under both transports and
delivered to the intake/authorization pipeline. The intake handler
(`src/server/telegram/intake.ts`) resolves each sender against the allowlist
on every message, rate-limits per user and per chat (in-memory, per-process),
audits privileged intake, and silently drops unauthorized traffic per ADR 0007.
Authorized senders reach the read-only command handler below; other work
(agent chat, lifecycle actions) is registered on the same authorized callback
by later tasks.

### The bot's Telegram settings are the operator's to set

Telegram's Bot API cannot change several bot profile settings; the operations
below can only be done in BotFather, and RepoOS deliberately neither performs
nor claims them:

* **Group privacy mode** — BotFather-only. Keep it **on** (the default!): your
  bot then receives exactly the messages it needs — commands addressed to it,
  replies, and all private chats — and does not read bystanders' group
  messages. The API offers no toggling method; `getMe` only reports the current
  state (`can_read_all_group_messages`), which the connection status shows.
  Changing privacy mode in BotFather requires re-adding the bot to groups.
* **Profile photo** — upload it in BotFather; the Bot API method here accepts
  file uploads, which the connection flow does not perform.
* **Inline mode** — leave off unless a later task opts in.

## Bind a chat

Notifications and commands only flow through chats an administrator explicitly
binds. Adding the bot to a group — or opening a private chat — does **not**
bind that chat.

An administrator binds chats through the authenticated admin API (a Settings
panel for chat management ships in a later task):

* `POST /api/auth/telegram/chats/bind-codes` — create a short-lived bind code.
  An **allowlisted admin** who is also linked on Telegram runs `/bind <code>` in
  the target group or chat; only that live admin role can redeem the code.
* `POST /api/auth/telegram/chats` — bind a chat directly by `telegram_chat_id`
  when the id is already known (`chatType` is required for group/channel ids).

When a user completes account linking in a private chat (`/start <invite>`),
that 1:1 chat is bound automatically as a side effect of their own link.

## Chat with the repository guide (Ross)

Once your Telegram account is linked and the chat is bound, send any plain
message to the bot: in a private chat, or in a bound group by @-mentioning the
bot or replying to one of its messages. The message becomes a question for
**Ross**, the repository guide — the same agent the board UI's guide chat
uses — and the answer comes back to the chat you asked in. Ross is read-only:
it answers questions about the repository, tasks, and docs; it cannot edit
files or change task status.

* **Your conversation is yours.** State is kept per Telegram user, not per
  chat: two members of the same group never share a transcript. Your context
  follows you between the private chat and a group you both use.
* **Conversations expire.** After 24 hours of inactivity a conversation ends;
  your next message starts a fresh one and the bot tells you it did. Send
  `/new` to start a fresh conversation yourself at any time — while a reply is
  still being generated, the bot asks you to wait instead of cutting the
  turn short.
* **Pace yourself.** Telegram agent turns are metered per user and per chat
  (a small number per minute). When the limit applies you get a clear
  "too many questions per minute" message instead of silence, and no turn is
  started until you try again.
* Every turn books its tokens and cost to the guide role on the board's AI
  usage panel, so Telegram-driven spend is visible like every other AI role.

## Link your account

An administrator creates a Telegram link from **Settings → Authentication &
Users** for an email already on the repository's allowlist. Open the resulting
bot start payload (or `https://t.me/<projectBot>?start=<payload>` when the
project bot username is known) in Telegram to bind your Telegram account to
that email. Your Telegram numeric user ID is the identity key; your Telegram
username is only a display hint and does not prove who you are.

The link uses your current RepoOS role. RepoOS checks the role from the
allowlist on each message, so an admin changing your role or removing your
email takes effect on your next Telegram message. Removing you from a Telegram
group does not change your RepoOS account, and being in a bound group does not
grant one.

Unknown, unlinked, or no-longer-allowlisted senders receive no reply in any
chat. This avoids confirming the bot is active and flooding groups with access
errors.

### Verify the connection

The Settings connection panel (**Settings → Notifications → Telegram**) shows
whether a bot is connected — its display name, username, and transport — and
lists the private chats currently bound (one per linked Telegram account),
with a **Send test message** control for each so an admin can confirm
delivery before relying on the integration for real notifications: any
failure (blocked bot, chat left, rate limit) surfaces Telegram's own error
text instead of a generic "failed." Unbinding a chat from this panel and from
**Settings → Authentication & Users** are the same action against the same
underlying links — either one immediately reflects on both.

## Use the bot

Once your account is linked and the chat is bound, you can run read-only
commands in a private chat or an addressed group message:

* `/status` — repository overview: task counts, anything needing attention,
  and how many agents/reviews are running.
* `/tasks [scope] [page]` — list tasks. `scope` is `work` (active and
  in-review, the default) or a status name; `page` moves through long lists.
  Members can use the `work` scope; querying arbitrary statuses is admin-only.
* `/agents` — agents running now, plus recently finished ones.
* `/help` — the commands **and what your role can do**, based on your current
  RepoOS role.

Replies link back into the RepoOS web UI for detail — Telegram is a glance and
notification surface, not a full client. Long task lists paginate with an
explicit "Showing N–M of T" and a `/tasks … <page>` hint rather than being cut
off silently. Your role is checked from the allowlist on every message, so a
change to it takes effect immediately; an unlinked or no-longer-allowlisted
sender gets no reply at all.

In a group, the bot only acts on a command addressed to it, a reply to one of
its messages, or an `@mention` — the same trigger rules as notifications. It
answers one command at a time per chat, so a burst from several members does
not interleave replies.

### Create a task

Admins can send `/newtask <description>`, or reply to a message with
`/newtask` to use its text. You can attach images, documents, or a voice note;
images and documents are checked by their actual bytes, and a configured voice
transcription provider turns a voice note into prose. The bot infers a title,
type, priority, and area and replies with a **preview** — nothing is created
yet. Use **Edit** to change one field at a time, **Cancel** to discard it, or
**Create** to file the task. A created task is the same task the web UI would
create, including the raw prompt under `## Original prompt`, screenshots under
`## Screenshots`, and a link into the board.

Members cannot create tasks; the bot says which boundary applies rather than
ignoring them.

### Drive a task

Notifications for work that can move carry inline buttons: **Start**,
**Resume**, **Pause**, **Request review**, and **Move to done**. Every button
is re-checked against your current allowlist role when you press it, so a
demoted admin loses access immediately and an old button cannot be replayed or
used after it expires (15 minutes).

**Move to done** never happens from one tap. The first press shows a
confirmation card naming the repository, task id/title, and the exact branch;
only **Confirm merge** starts the close-out pipeline, and **Cancel** leaves
everything untouched. Closing a task out still runs the same merge and full
check gate as the web UI.

In a forum topic, replies and action cards stay in that topic. Topics are a
routing convenience only — access is still decided by the bound chat and the
sender's live role.

## Managed provisioning (the official service)

Managed provisioning lets RepoOS create and hand over a project bot without
touching BotFather. The local instance only ever asks the provisioning service
to *begin* a request, *observe* its state, and *redeem* the new bot's
credential server-to-server — a narrow client boundary documented in
`docs/telegram-adapter.md`, tested locally against a service fake. Until that
service is deployed and configured (point `[telegram] provisioningUrl` at it
and set `REPOOS_TELEGRAM_PROVISIONING_KEY` in the environment), connect reports
"managed provisioning is not configured" honestly and Bring Your Own Bot Token
keeps working. A repository instance never holds or accepts a manager-bot
credential of any kind.

The service itself — a standalone deployment hosted on Neon Functions and
Neon Postgres, source under `telegram-manager/` — is the operator's own
account to run. It is not something a repository instance can start or
depend on. When your operator has deployed it and shared the two values above
with you:

1. Set `[telegram] provisioningUrl` in `repoos.toml` to the service's URL.
2. Set `REPOOS_TELEGRAM_PROVISIONING_KEY` in your environment to the shared
   auth key the operator issued.
3. Use **Connect Telegram** as normal — it will now offer managed
   provisioning instead of (or alongside) Bring Your Own Bot Token, prompting
   you to send `/link <code>` to the official manager bot before tapping the
   bot-creation link it gives you. That `/link` step is what proves the bot
   you're about to create is *yours* — a bare creation link carries no such
   proof, so don't skip it even if the link looks ready to tap on its own.
4. Disconnecting a managed bot later asks the service to invalidate its
   token (the service rotates the bot's token so the one your instance held
   stops working). This needs the same provisioning URL and key to still be
   configured — if either goes missing, disconnect reports it as retryable
   rather than silently leaving the credential live.

Deploying and operating the service itself (creating the manager bot,
provisioning the Neon project, running migrations, setting secrets, going
live) is documented for the operator in
[`docs/telegram-manager-service.md`](../docs/telegram-manager-service.md); it
is out of scope for a repository instance's own setup.

For how RepoOS login, the email allowlist, and admin/member roles work, read
[Native authentication](/native-auth). The implementation decision is recorded
in [ADR 0007](../docs/adr/0007-telegram-identity-and-authorization.md), and the
adapter's implementation notes (including the full provisioning contract) are
in [docs/telegram-adapter.md](../docs/telegram-adapter.md).
