A portable, SDK-agnostic Agent Skill for designing and improving the user interface of Telegram bots — texts, buttons, keyboards, navigation, colors, custom emoji, streaming feedback, and message rendering.
A Telegram bot has no CSS and no layout engine. Its entire interface is message text, formatting, keyboards, chat-level chrome (commands, menu button, deep links), and the transitions between states — plus what the user sees while the bot is working. This skill teaches an AI coding agent to treat all of that as one design surface: audit the project first, fix words before structure and structure before color, and change presentation without touching business logic.
This repository is the skill. It doesn't modify any bot — you install it, and your agent uses it when a Telegram UI task comes up.
Formerly
telegram-rich-messages. All of the message-formatting capability is still here — it is now one layer of a larger UI skill.
Make a bot's interface clear, consistent, and safe to change:
- decide what a screen says and how it says it;
- label, group, order, color, and lay out buttons;
- design navigation between screens and states;
- keep every screen consistent with the rest of the bot;
- keep localization intact;
- render message bodies with the richest format that actually helps;
- give honest feedback while the bot works —
across any language and any Telegram SDK, without breaking existing handlers,
callback_data, deep links, or translations.
- "Improve the bot's UX/UI", "redesign this menu", "this keyboard is overloaded", "users don't understand this screen", "add a back button".
- Button labels, ordering, grouping, colors, custom emoji icons; inline vs reply keyboards; pagination; confirmations.
- Rewriting bot messages, titles, captions, prompts, errors, and empty states.
- Commands list, menu button,
/startand deep links, first-run experience. - Consistency work: one action named three different ways, back buttons that exist on some screens only, state that doesn't update after a tap.
- Streaming AI answers, progress bars, "typing" statuses, button loaders — "the bot feels frozen while it thinks".
- Personalization (greeting by name, locale hints) and link surfaces (URL buttons, link previews, deep links, mentions).
- Message rendering: HTML / MarkdownV2 / entities / Rich Messages / tables,
can't parse entities, broken or oversized messages, RTL.
Bot logic that never reaches the interface: webhook infrastructure, payment
processing, authorization, databases and migrations, group administration and
moderation logic, scraping, analytics pipelines. Mini App internals are
ordinary web front-end work — only the Telegram-side surface (the button or menu
that opens it, BottomButton, WebApp.ready()) is in scope. Not for Microsoft
.rtf files.
| Area | What the skill covers |
|---|---|
| Message texts | Rewriting with facts, placeholders, and i18n keys preserved; tone; length; front-loading; one job per screen |
| Titles & captions | Screen titles, caption limits, media captions |
| Button text | Verb+object labels, uniqueness, truncation in the longest locale, emoji budget |
| Button structure | Exactly one action field, callback_data schemes, copy-text/URL/Web App/switch-inline buttons |
| Inline keyboards | Layout, grouping, pagination, editing in place, stale-message handling, callback answers |
| Reply keyboards | Persistence, placeholders, request_* buttons, ForceReply, keyboard removal, label-routing traps |
| Action order & grouping | Probability-then-risk ordering, homogeneous rows, isolated destructive actions |
| Visual hierarchy | One focal action per screen, emphasis budget, formatting as emphasis not decoration |
| Button colors | style: primary / success / danger, assigned by meaning |
| Custom emoji | icon_custom_emoji_id on buttons, custom emoji in text, obtaining real IDs, fallbacks |
| Navigation | Screen/state maps, back/home, wizards, commands, menu button, deep links, topics, ephemeral messages |
| Dynamic feedback | sendMessageDraft, sendRichMessageDraft + <tg-thinking>, progress bars, sendChatAction, button loaders, throttling |
| Personalization & links | Safe display names, optional user fields, URL buttons vs text links, link previews, deep-link hygiene |
| Consistency | Action vocabulary, navigation slots, emoji vocabulary, state variants, cross-surface agreement |
| Localization | Localized labels and command descriptions, longest-translation layout, RTL |
| Rendering | Tables, Rich Messages, HTML/MarkdownV2/entities, splitting, media, channels, escaping |
- Identify what the text must accomplish, and for whom.
- Keep every fact — prices, deadlines, legal wording, IDs, links.
- Keep the placeholders — same names, same count, no reordering.
- Edit the i18n catalog, not the call site; update every locale or flag the ones that need translation.
- First line states the point; one job per screen.
- Structured data becomes a table, not a monospace grid.
- Formatting is emphasis, not decoration — bold the one thing that matters.
- No raw internals (stack traces, SQL, HTTP codes) in user-facing text.
- Errors = what happened + what to do next. Empty states = what would be here + how to fill it. Confirmations = object + consequence + reversibility.
- Re-check escaping for the parse mode and re-check length limits after every edit.
- Exactly one action field per inline button (
url,callback_data,web_app,login_url,switch_inline_query*,copy_text,callback_game,pay).text,style, andicon_custom_emoji_iddon't count. callback_datais 1–64 bytes — namespaced (screen:action:id), ASCII, ids not labels.- Never change existing
callback_datafor a visual redesign — messages already in users' chats still send the old payload. - Labels are verb + object, unique within a keyboard, and the same word for the same action everywhere in the bot.
- Order by likelihood, then risk. Destructive actions last and isolated.
- 1–2 buttons per row for sentence-length labels, up to 3 for short ones; more only for tokens. Verify against the longest translation.
- Navigation row last, identical on every screen.
- Words before pixels — fix the label before adding color or an emoji.
- Every tap gets feedback; every callback query gets answered.
- The keyboard must be fully usable with no color and no emoji.
Official wording: "Optional. Style of the button. Must be one of 'danger' (red), 'success' (green) or 'primary' (blue). If omitted, then an app-specific style is used." There is no HEX, RGB, or CSS color in the Bot API.
style |
Color | Use when the action… | Examples |
|---|---|---|---|
primary |
blue | moves the user forward — the single most likely next step | Continue · Start · Open · Next |
success |
green | commits, confirms, or completes positively | Save · Confirm · Publish · Submit · Pay |
danger |
red | destroys, resets, or is hard to undo | Delete · Reset · Unsubscribe · Leave · Cancel plan |
| (omit) | app default | is secondary, neutral, or navigational | Back · Home · Settings · Help |
Budget: at most one primary per screen, danger only for real destruction
(always behind a confirmation), roughly ≤2 styled buttons per keyboard, and
never style a list of equal options — coloring everything removes the
hierarchy color was meant to create.
{
"text": "Finish the test",
"icon_custom_emoji_id": "5368324170671202286",
"style": "success",
"callback_data": "finish_test"
}The custom emoji renders before the button text. The value is the ID of one
specific custom emoji — not a file_id, not a pack name, not a link, not a
Unicode character. It cannot be guessed or derived.
Where to get a real ID:
MessageEntity.custom_emoji_idfrom a message that contains the emoji;Sticker.custom_emoji_idfromgetStickerSet;getCustomEmojiStickers(≤200 ids per call) to verify ids you already have.
The skill never invents an ID: it plumbs the field through from config, asks the user for the value, and shows them how to capture it.
Official wording for the field: "Can only be used by bots that purchased additional usernames on Fragment or in the messages directly sent by the bot to private, group and supergroup chats if the owner of the bot has a Telegram Premium subscription."
So a bot qualifies via either:
- Fragment — the bot purchased additional usernames there; or
- Owner Premium (added in Bot API 9.4) — the bot's owner has Telegram Premium and the message is sent directly by the bot to a private, group, or supergroup chat.
Consequences the skill states out loud: channels aren't covered by route 2; the capability depends on the owner's subscription staying active, not the viewer's; and if neither route applies, use Unicode emoji instead. Custom emoji in message text follow the same eligibility and have existed since Bot API 6.2 (2022-08-12), where they always require a valid Unicode fallback character.
- ERROR: request failed with code 500. Something went wrong.
- Please try again later or contact the administrator if the problem persists.
+ ⚠️ Couldn't load your orders.
+ This is on our side — try again in a minute.reply_markup, handler, parse mode, and i18n key unchanged.
{"inline_keyboard": [[
{"text": "OK", "callback_data": "ok"},
{"text": "Delete", "callback_data": "del"},
{"text": "Back", "callback_data": "back"}
]]}{"inline_keyboard": [
[{"text": "Save changes", "callback_data": "ok", "style": "success"}],
[{"text": "Delete draft", "callback_data": "del", "style": "danger"}],
[{"text": "← Back", "callback_data": "back"}]
]}Same payloads, clearer intent: one commit action, destruction separated and confirmed, navigation predictable.
{"inline_keyboard": [
[{"text": "Finish the test", "icon_custom_emoji_id": "5368324170671202286",
"style": "success", "callback_data": "finish_test"}],
[{"text": "Continue later", "icon_custom_emoji_id": "5368324170671202111",
"callback_data": "pause_test"}],
[{"text": "← Back to topics", "callback_data": "topics"}]
]}{
"keyboard": [
[{"text": "🧾 My orders"}, {"text": "🛒 Catalog"}],
[{"text": "📞 Share phone", "request_contact": true, "style": "primary"}],
[{"text": "⚙️ Settings"}]
],
"resize_keyboard": true,
"is_persistent": true,
"input_field_placeholder": "Type a city or tap a button"
}Reply-keyboard buttons send their label as a message — so handlers must route through stable i18n keys, never hardcoded display text.
Seven fully worked scenarios (text-only edit · rebuilding an overloaded keyboard
· adding the three styles · icon + color together · unknown custom_emoji_id ·
SDK without 9.4 support · when not to add color) live in
references/examples-before-after.md.
- Preserve
callback_data, URLs, deep links, payload schemes, and handler routing. Visual redesign ≠ handler rewrite. If a payload must change, update every consumer in the same change and say so. - Old clients ignore unknown fields: a button with
style/icon_custom_emoji_idrenders as a normal button with the same label and action. Design for that baseline, then enhance. - Never rely on color or emoji alone — the label carries the meaning.
- Verify SDK support before using new fields. Detection recipes per SDK are
in
references/sdk-compatibility.md; a strict, typed SDK may silently drop unknown fields, which looks like working code that never colors anything. - If the SDK lags: upgrade it, or send the markup as a raw Bot API payload behind a single adapter — never claim a field shipped when it didn't.
- Localization is part of compatibility: keep catalog keys, update every locale, and keep layout tolerant of the longest translation.
- Degrade a button by dropping
style/icon, never by removing the button.
- Audited the project and produced a screen/state map before editing.
- Fixed words → structure → navigation → rendering → color, in order.
- Every inline button has exactly one action field;
callback_data≤64 bytes and unchanged. -
styleonlyprimary/success/danger, assigned by meaning; ≤1primary;dangerbehind a confirmation. - No invented
custom_emoji_id; eligibility route (Fragment / owner Premium / neither) established and reported. - Labels understandable with no color and no emoji.
- Every tap acknowledged; long operations show honest, throttled progress; streamed drafts finalized with a real message even on failure.
- Localization keys intact; longest-translation layout verified.
- SDK support verified, or a documented raw-payload/fallback path used.
- Keyboards validated (
scripts/validate_keyboard.py), project checks green. - Report says what changed, what was deliberately left alone, and what still needs the bot owner (emoji IDs, Premium status, copy decisions).
telegram-bot-ui/
├── SKILL.md # compact operational guide (loaded on activation)
├── README.md · LICENSE
├── references/ # progressive-disclosure detail (read on demand)
│ ├── microcopy-and-labels.md # texts, titles, captions, labels, emoji
│ ├── buttons-and-styles.md # style colors, emphasis, layout
│ ├── inline-keyboards.md # callbacks, pagination, editing
│ ├── reply-keyboards.md # persistence, request_*, ForceReply
│ ├── navigation-and-flows.md # screens, states, commands, deep links
│ ├── custom-emoji.md # icon_custom_emoji_id, IDs, eligibility
│ ├── dynamic-feedback-and-streaming.md# drafts, thinking, progress, loaders
│ ├── personalization-and-links.md # user data, links, previews
│ ├── consistency-and-review.md # cross-screen consistency + checklist
│ ├── ui-changelog.md # UI-relevant Bot API history & gates
│ ├── examples-before-after.md # seven worked scenarios
│ ├── tables.md # structured data as real tables
│ ├── telegram-capabilities.md # Rich Messages + capability matrix
│ ├── regular-formatting.md # HTML/MarkdownV2/entities
│ ├── channels-and-broadcasting.md
│ ├── format-selection.md
│ ├── project-adaptation.md
│ ├── security-and-escaping.md
│ ├── limits-and-splitting.md
│ ├── rich-media.md
│ ├── streaming-and-editing.md
│ ├── localization-and-rtl.md
│ ├── errors-and-fallbacks.md
│ ├── sdk-compatibility.md
│ ├── testing-playbook.md
│ └── official-sources.md # sources + last-verified dates
├── scripts/ # stdlib-only validators (no token, no network)
│ ├── validate_skill.py
│ ├── validate_references.py
│ └── validate_keyboard.py
├── evals/
│ └── activation-evals.md # positive/negative trigger scenarios
└── agents/
└── openai.yaml # optional Codex-specific metadata
Claude Code — user level (all projects):
git clone https://github.com/hlibsuslov/telegram-bot-ui.git \
~/.claude/skills/telegram-bot-uiClaude Code — repository level (one project): clone into
<repo>/.claude/skills/telegram-bot-ui.
Codex (OpenAI): clone into ~/.agents/skills/telegram-bot-ui (user) or
<repo>/.agents/skills/telegram-bot-ui (project).
On Windows the Claude Code user path is
%USERPROFILE%\.claude\skills\telegram-bot-ui.
The skill directory name must match the name in SKILL.md
(telegram-bot-ui). The repository was renamed from telegram-rich-messages
on 2026-08-05; GitHub redirects the old URL, so existing clones keep working —
run git remote set-url origin https://github.com/hlibsuslov/telegram-bot-ui.git
to update one.
- Automatically: the
descriptioninSKILL.mdlets the host select the skill when a request matches — e.g. "improve this bot's menu", "make the confirm button stand out", "add colored buttons", "rewrite this bot's error message", "the keyboard is a mess", "stream the AI answer", "fix Telegram can't parse entities". - Explicitly: ask for it by name — "use the telegram-bot-ui skill to audit
our bot's screens" (Codex:
$telegram-bot-ui).
python scripts/validate_skill.py # front matter, sections, secrets
python scripts/validate_references.py # links, orphans, empty files, secrets
python scripts/validate_keyboard.py keyboard.json # keyboard payload rulesAll three are standard-library only, read local files only, and never use a bot
token or the network. On Windows use py -3 in place of python. Then walk
evals/activation-evals.md to confirm trigger
behaviour.
- Read project instructions; detect language, framework, SDK, and versions.
- Determine the supported Bot API version — this decides whether
styleandicon_custom_emoji_idare reachable at all. - Build a screen/state map: every screen, keyboard,
callback_data, and transition. - Identify real UX defects; fix words → structure → navigation → rendering → color.
- Implement in the project's own style, preserve payloads and localization, add a fallback and tests, run its checks.
Source-of-truth order: current official Telegram docs → Bot API changelog → installed SDK docs → SDK source → SDK issues → third-party articles (context only). The skill re-verifies official docs for version-dependent work and is not frozen at any Bot API version.
- Bot API · changelog · Bot Features · Bots FAQ
InlineKeyboardButton·KeyboardButton·ReplyKeyboardMarkup·ForceReply- Bot API 9.4 — 9 Feb 2026
(
style,icon_custom_emoji_id) · Bot API 6.2 — 12 Aug 2022 (custom emoji foundation) getCustomEmojiStickers·getStickerSet·Sticker·MessageEntityanswerCallbackQuery·sendChatAction·editMessageReplyMarkup·setMyCommands·setChatMenuButtonsendMessageDraft·sendRichMessageDraft·InputRichBlockThinking·LinkPreviewOptions- Formatting options · Mini Apps
Telegram's UI surface evolves. Before version-dependent work, re-check the
Bot API changelog, update
references/ui-changelog.md with any entry that
changes the interface, and record the new date in
references/official-sources.md. Content here
was verified against Bot API 10.2 (released 2026-07-14) on 2026-08-05.
Issues and PRs welcome. Please: keep SKILL.md compact (detail belongs in
references/), cite official Telegram docs for any capability claim, mark
unverified numbers as such, run all three validators, and keep the skill
project-agnostic (no assumptions about a specific SDK or repo).
MIT.
Community project; not affiliated with or endorsed by Telegram. Verify current behaviour against the official Bot API before relying on any capability.