GramIO
GramIO is a modern, type-safe Telegram Bot API framework for TypeScript. It runs on Node.js, Bun, and Deno with full Bot API coverage, a composable plugin system, and first-class TypeScript support.
When to Use This Skill
- Creating or modifying Telegram bots
- Setting up bot commands, callbacks, inline queries, or reactions
- Building keyboards (reply, inline, remove, force reply)
- Formatting messages with entities (bold, italic, code, links, mentions)
- Uploading/downloading files and media
- Managing user sessions or multi-step conversation flows (scenes)
- Writing custom plugins
- Configuring webhooks or long polling
- Handling payments with Telegram Stars
- Broadcasting messages with rate limit handling
- Building Telegram Mini Apps (TMA) with backend auth
- Containerizing bots with Docker
- Using standalone
@gramio/typesfor custom Bot API wrappers - Writing and publishing custom plugins
- Migrating bots from puregram, grammY, Telegraf, or node-telegram-bot-api to GramIO
Quick Start
npm create gramio bot-name
cd bot-name
npm run dev
Basic Pattern
import { Bot } from "gramio";
const bot = new Bot(process.env.BOT_TOKEN as string)
.command("start", (context) => context.send("Hello!"))
.onStart(({ info }) => console.log(`@${info.username} started`))
.onError(({ context, kind, error }) => console.error(`[${kind}]`, error));
bot.start();
Introspection Tools
This skill ships four Node.js scripts under tools/ that parse the installed @gramio/* and gramio packages on disk. Prefer these over URL-fetching or loading the telegram-api-index — each call returns one focused, version-accurate signature instead of a wall of docs.
Run from the user's project root (where node_modules/ lives). Tools print to stdout; errors and auto-correct hints go to stderr.
| Tool | Use it when |
|---|---|
tools/get-bot-api-method.mjs <name> |
You need the signature of a Bot API method (e.g. sendMessage, createChatInviteLink) — returns JSDoc + params/return type from @gramio/types. --list shows all methods, --search <term> filters by name/description. |
tools/get-bot-api-type.mjs <name> |
You need a Telegram type definition (e.g. Message, ChatInviteLink). Accepts short (Message) or full (TelegramMessage) names. Same --list / --search flags. |
tools/get-context-getter.mjs <ClassName> |
You need to know what getters/methods a context exposes (MessageContext, CallbackQueryContext, User, Chat). Add --deep to pull in mixins + merged interfaces recursively. --search <name> finds every class that exposes a given getter/method (e.g. firstName lives on User, Chat, Contact, SharedUser). |
tools/get-plugin.mjs <name> |
You need a plugin's entry function signature + what it derives onto context (e.g. session, scenes, i18n). --list shows every installed @gramio/* package. |
node tools/get-bot-api-method.mjs sendMessage
node tools/get-bot-api-type.mjs InlineKeyboardMarkup
node tools/get-context-getter.mjs MessageContext --deep
node tools/get-context-getter.mjs --search chatId
node tools/get-plugin.mjs session
The scripts fuzzy-match (sendMesage → sendMessage) and suggest alternatives on miss. They require the relevant package to be installed — if not, they print the exact npm install hint.
Critical Concepts
Callback routing — NEVER parse callback data manually.
CallbackData.pack()produces a 6-character hash prefix (sha1-base64url of the schema name) + serialized payload — NOT a literal prefix like"nav:". Checks likectx.data.startsWith("nav:")always fail at runtime. Use one of these four patterns, picked by shape of data:// Fixed string → exact string match bot.callbackQuery("refresh", (ctx) => ctx.answer("Refreshed")); // Pattern / variable slug → RegExp with capture groups bot.callbackQuery(/^user_(\d+)$/, (ctx) => { const [, id] = ctx.match!; ctx.answer(`User ${id}`); }); // Structured data → CallbackData schema (preferred for multi-field payloads) import { CallbackData } from "gramio"; const nav = new CallbackData("nav").enum("to", ["home", "history", "admin"]); bot.callbackQuery(nav, (ctx) => { ctx.queryData.to; // "home" | "history" | "admin" — fully typed }); // Stale-safe unpack (when inline keyboard may outlive a schema change) const result = nav.safeUnpack(ctx.data!); if (!result.success) return ctx.answer("Button expired"); result.data.to; // typed// ❌ NEVER — hashed prefix means string compare won't match, // and you lose full type safety. if (ctx.data?.startsWith("nav:")) { const [, to] = ctx.data.slice(4).split(":"); // ... }See callback-data and middleware-routing for overlapping-handler behavior across plugins.
Method chaining — handlers, hooks, and plugins chain via
.command(),.on(),.extend(), etc. Order matters: when two handlers match the same update, the first-registered one wins unless it callsnext(). See middleware-routing.Type-safe context — context is automatically typed based on the update type. Use
context.is("message")for type narrowing in generic handlers. After.derive()/.decorate()/.extend(plugin), new fields appear on the inferred context type automatically — never cast withctx as unknown as { myField }. Export the bot's context type and reuse it (see context → "Context typing after derive").Context getters — always camelCase; never touch
ctx.payloadorctx.update.*— every Telegram field is exposed as a camelCase getter (ctx.from,ctx.firstName,ctx.chatId,ctx.messageId,ctx.text,ctx.data,ctx.queryData). Bothctx.payloadANDctx.updateare raw snake_case internal objects — treat them as off-limits in handler code.Plugin system —
new Plugin("name").derive(() => ({ ... }))adds typed properties to context. Register viabot.extend(plugin).Hooks lifecycle — onStart → (updates with onError) → onStop. API calls: preRequest → call → onResponse/onResponseError.
Error suppression —
bot.api.method({ suppress: true })returns error instead of throwing.Lazy plugins — async plugins (without
await) load atbot.start(). Useawaitfor immediate loading.Derive vs Decorate —
.derive()runs per-update (computed),.decorate()injects static values once.Formatting — four critical rules (read formatting before writing any message text):
- Never use
parse_mode—formatproducesMessageEntityarrays, not HTML/Markdown strings. Addingparse_mode: "HTML"or"MarkdownV2"will break the message. GramIO passes entities automatically. - Never use native
.join()on arrays of formatted values — it calls.toString()on eachFormattable, silently destroying all styling. Always use thejoinhelper:join(items, (x) => bold(x), "\n"). - Always wrap styled content in
format\`` when composing or reusing — embedding aFormattablein a plain template literal (`${bold`x`}`) strips all entities. Useformat\${bold`x`}`` instead. - Never call
.toString()on aFormattableString— pass it directly as thetext:/caption:param tosend,editMessageText,editMessageCaption, etc. Calling.toString()strips all entities. This is the #1 reason magic-links and formatted entities "stop working" after an edit.
- Never use
Scenes — step semantics and update-type filtering —
context.scene.step.go(N)andcontext.scene.step.next()run the scene's middleware chain immediately, but each.step(updateName, handler)filters bycontext.is(updateName). If your current context iscallback_queryand the next step is.step("message", …), it will not fire — you must either send the UI directly beforereturn step.next(), use.step(["message", "callback_query"], …), or render the prompt inonEnter/ the current step's callback handler. Prefer.ask("field", zodSchema, "prompt")for single-value validated input. See scenes.InlineQueryResult builders — use
InlineQueryResult.article(id, title, InputMessageContent.text(...))and similar builder methods for inline results.bot.inlineQuery(/regex/, handler)routes inline queries. See triggers.Composing screens — prefer
@gramio/viewsover inlinectx.send/ctx.editText— for any bot with more than a couple of screens, use the@gramio/viewsplugin to define reusable view templates and render them withcontext.render(view, params). It auto-detects send-vs-edit based on update type, centralises text/keyboards/media (incl. JSON-driven templates with{{param}}/{{$global}}interpolation and i18n via adapter factory), and eliminates the common failure mode of duplicating message bodies acrossctx.send(...)andctx.editText(...)call sites. The plain render-function pattern is fine for trivial menu-only bots (2-3 screens, no media, no i18n) — anything more graduates to@gramio/views.@gramio/viewsis marked work-in-progress (helper/adapter details may evolve), but the core shape (initViewsBuilder,.render(fn),context.render(...)) is stable and is the recommended direction for view composition in GramIO. See views.@gramio/promptis in-memory only — NOT persistent. Use Scenes +.ask()for any multi-step / connect / onboarding flow. The Promise returned bycontext.prompt(...)/context.wait(...)lives in the current Node.js process. The moment the process restarts (deploy, crash, container reschedule, dyno cycling), the Promise is gone: the user's next message is not treated as an answer, no error is thrown, the bot silently "forgets" where the user was. This is fatal for OAuth-connect flows (Spotify/Last.fm/etc.), onboarding wizards, payment collection, any question-sequence that must survive a deploy. Scenes, by contrast, persist step index and collected answers in the configured storage (Redis / Cloudflare / custom) and rehydrate on the next update. Rule:@gramio/promptis acceptable only for single-question, in-process prompts where losing the flow on restart is acceptable (e.g. throwaway confirmations, debug helpers). Every question-sequence that matters — and every "ask → validate → ask again" connect flow — belongs in a Scene with.ask("field", zodSchema, "prompt message")..ask()kills the samefirstTimebranching and validation-retry boilerplate asprompt, plus it persists. Do not recommend@gramio/promptas a simpler alternative to scenes for multi-step flows; that is the recommendation trap this concept exists to block. See scenes and prompt.Subagent delegation — skills do not auto-activate inside subagent sessions. When spawning a subagent that will write bot code, explicitly pass the relevant reference-file paths (e.g.
skills/references/callback-data.md,skills/plugins/scenes.md,skills/references/formatting.md,skills/references/middleware-routing.md) in the agent prompt, or include the key rules inline.No
anyanywhere in examples — never writectx: any,as any,<any>, or implicit-any parameters in any file underskills/(examples, markdown code blocks, plugin docs). Skill examples are templates that AI copies verbatim into user bots; everyanyhere multiplies into every downstream bot. Derive the proper type fromContextType<typeof bot, "update_name">,CallbackQueryShorthandContext<typeof bot, typeof schema>, or export aBotContext = typeof bot['_']['context']alias. If a value is genuinely unknown at a system boundary, useunknown+ narrowing. No exceptions, even in "what-not-to-do" snippets — use@ts-expect-erroron the specific line with a comment instead of a broadany.Button-first UX — users tap, they don't type. Navigation belongs to inline keyboards, not slash commands.
/startshould be a short hero (bold title + blockquote description) with an inline keyboard of primary actions — not a wall of text listing/help,/settings,/delete. Nested menus need breadcrumbs in the title (⚙️ Settings · home › settings), a◀ Backbutton on every non-home screen, and🏠 Homeanywhere deeper than two levels. Navigation clicks edit the current message (ctx.editText), never send new ones — new sends are for events (results, notifications), not navigation. Toggle buttons carry their state in the label (✅ Notifications/⬜ Notifications) and one handler flips the session field + rerenders. Destructive actions always go through a confirm screen with the safe default on the left. Every callback handler starts withctx.answer()so the spinner stops immediately — empty is fine for navigation, short text for toast feedback,{ show_alert: true }only for errors the user must acknowledge. Commands exist for discovery (register withsetMyCommandsso they appear in Telegram's menu button), not as the primary UI. See ux-patterns for the full playbook andexamples/ux-menu.tsfor a worked example covering hero/start, nested menu, toggles, and destructive confirm.Run
bun run check:skillsbefore finishing any skill edit — any change toskills/**/*.tsor TypeScript code blocks inskills/**/*.mdmust typecheck cleanly against the currently installed gramio versions. Thecheck:skillsscript runstsc --noEmitoverskills/examples/*.tswith strict mode. If it reports errors, fix them — don't ship. If a pre-existing example breaks because gramio's API evolved, update the example to match the current API (checknode_modules/gramio/dist/index.d.tsandnode_modules/@gramio/*/dist/index.d.tsfor current signatures).Rich Messages are a separate send surface — use
rich/markdownTablefor the Markdown lane,blocks/tablefor native structured blocks, or@gramio/jsx/richfor rich JSX. Rich values route tosendRichMessage, notsendMessage.streamRichMessageis Markdown-chunk based; send native blocks directly withctx.send/ctx.sendRichMessage. Uploads work recursively inrich_message.blocks/.media, but direct uploads are forbidden insendRichMessageDraft. Handle user cancellation withbot.on("stopped_message_generation", ...). See rich-messages.
Official Plugins
| Plugin | Package | Purpose |
|---|---|---|
| Session | @gramio/session |
Persistent per-user data storage |
| Scenes | @gramio/scenes |
Multi-step conversation flows |
| Onboarding | @gramio/onboarding |
Decorate-only — declarative tutorials / feature intros. Multi-flow concurrency, refusal ladder, scope-aware rendering. NOT a forms/wizard tool — use Scenes for that |
| I18n | @gramio/i18n |
Internationalization (TS-native or Fluent) |
| Autoload | @gramio/autoload |
File-based handler loading |
| Prompt | @gramio/prompt |
Single-question prompts — in-memory only, not persistent. Use Scenes .ask() for anything that must survive restarts |
| Views | @gramio/views |
Recommended for screen composition — reusable templates (programmatic + JSON), auto send/edit, keyboards, media, i18n |
| JSX | @gramio/jsx |
JSX syntax for formatting + keyboards (no React) |
| Pagination | @gramio/pagination |
Paginated inline-keyboard menus with fluent builder |
| Auto Retry | @gramio/auto-retry |
Retry on 429 rate limits |
| Media Cache | @gramio/media-cache |
Cache file_ids |
| Media Group | @gramio/media-group |
Handle album messages |
| Split | @gramio/split |
Split long messages |
| Auto Answer CB | @gramio/auto-answer-callback-query |
Auto-answer callbacks |
| PostHog | @gramio/posthog |
Analytics + feature flags |
| OpenTelemetry | @gramio/opentelemetry |
Distributed tracing and spans |
| Sentry | @gramio/sentry |
Error tracking + performance monitoring |
Telegram Bot API Reference Pages
GramIO docs include a dedicated reference page for every Telegram Bot API method and type:
- Methods:
https://gramio.dev/telegram/methods/{methodName}— e.g.sendMessage,createChatInviteLink,answerCallbackQuery - Types:
https://gramio.dev/telegram/types/{typeName}— e.g.Message,ChatInviteLink,InlineKeyboard
Each page contains: GramIO TypeScript examples, parameter details, error table with causes and fixes, tips & gotchas, and related links. When a user asks about a specific Telegram API method or type, you can fetch or reference the corresponding page for accurate GramIO-specific usage.
Tip for LLMs: Any GramIO docs page can be fetched as clean Markdown by appending
.mdto the URL:https://gramio.dev/telegram/methods/sendMessage.md— clean Markdown instead of HTML. This works for all sections of the docs, not just API pages.
These pages are not included in this skill by default — fetch them on demand when the user asks about a specific method/type.
To quickly find which methods exist — use the pre-built index: telegram-api-index. It lists all 165+ Bot API methods with short descriptions in one file. Load it when you need to discover a method name or confirm one exists before fetching a full page.
References
Core
| Topic | Description | Reference |
|---|---|---|
| Bot Configuration | Constructor, API options, proxy, test DC, debugging | bot-configuration |
| Bot API | Calling methods, suppress, withRetries, type helpers | bot-api |
| Context & Updates | derive, decorate, middleware, start/stop, type narrowing | context |
| Triggers | command, hears, callbackQuery, inlineQuery, reaction | triggers |
| Filters | type-safe context narrowing — filter-only .on(), filters.* predicates, guard() |
filters |
| Middleware Routing | handler priority, next(), overlapping CallbackData, centralized routing |
middleware-routing |
| Scene ↔ Composer inheritance | share named .as("scoped") composer derives between bot-level handlers and Scene steps; file split to avoid circular imports |
scene-composer-inheritance |
| Hooks | onStart, onStop, onError, preRequest, onResponse | hooks |
| Updates & Lifecycle | start/stop options, graceful shutdown (SIGINT/SIGTERM) | updates |
| Troubleshooting | symptom → cause → fix: 409 conflict, broken formatting, spinner hangs, missing opt-in updates, scenes-without-session, snake_case access, un-awaited MediaUpload | troubleshooting |
Features
| Topic | Description | Reference |
|---|---|---|
| Keyboards | Keyboard, InlineKeyboard, layout helpers, styling | keyboards |
| Formatting | entity helpers, join (never native .join()!), variable composition, no parse_mode |
formatting |
| Rich Messages | Markdown/structured block helpers, rich JSX, recursive uploads, drafts, stop-generation updates | rich-messages |
| UX Patterns | button-first nav, /start anatomy, nested menus, toggles, destructive confirm, empty states, formatting hierarchy, command discovery |
ux-patterns |
| Deep Links | every t.me/<bot>?... link family — ?start=, ?startgroup=, ?startchannel=, ?startapp=, ?startattach=, ?game= — with the handler / context field each one lands in, payload encoding rules, admin= permission tokens, footguns |
deep-links |
| Files | MediaUpload, MediaInput, download, Bun.file() | files |
| CallbackData | Type-safe callback data schemas | callback-data |
| Storage | In-memory, Redis, Cloudflare adapters | storage |
| Telegram Stars | Payments, invoices, subscriptions, inline invoices, refunds, test mode | telegram-stars |
| Types | @gramio/types, type helpers, Proxy wrapper, declaration merging | types |
Infrastructure
| Topic | Description | Reference |
|---|---|---|
| Webhook | Framework integration, tunneling, custom handlers | webhook |
| Rate Limits | withRetries, broadcasting, queues | rate-limits |
| Docker | Dockerfile, multi-stage build, Docker Compose | docker |
| Local Bot API Server | self-host telegram-bot-api, cloud→local migration (logOut + api.baseURL), 2 GB uploads, token-less file downloads |
local-bot-api |
| tdlight (@gramio/tdlight) | userbot/user mode, extra methods (searchMessages/getChats/votePoll), scheduling, proxies, extra object fields; method-name traps; Docker | tdlight |
| TMA | Mini Apps, mkcert HTTPS, @gramio/init-data auth | tma |
| Testing | Event-driven bot testing, user actors, API mocking | testing |
Migrations
Load when the user wants to migrate an existing bot to GramIO.
| From | Description | Reference |
|---|---|---|
| grammY | Flavor generics → derive/extend, named API params, conversations → scenes, checklist | migration-from-grammy |
| puregram | Symbol mapping, API comparisons, checklist for puregram → GramIO refactor | migration-from-puregram |
| Telegraf | Symbol mapping, context typing, Scenes/WizardScene, webhook differences, checklist | migration-from-telegraf |
| node-telegram-bot-api | Symbol mapping, middleware concepts, keyboard builders, session, checklist | migration-from-ntba |
Upgrading an existing GramIO project across
gramio/@gramio/*versions (not migrating from another framework) → use the standalonegramio-upgradeskill (skills/gramio-upgrade/SKILL.md). It detects installed versions, looks up the breaking changes / deprecations / new features between current and target in its version-by-version ledger (skills/gramio-upgrade/MIGRATIONS.md), and applies the edits.
Plugins
| Plugin | Description | Reference |
|---|---|---|
| Session | Per-user data, Redis support | session |
| Scenes | Multi-step flows, state, navigation | scenes |
| Onboarding | Decorative tutorials with multi-flow concurrency — pair with Scenes for forms | onboarding |
| I18n | TS-native and Fluent internationalization | i18n |
| Autoload | File-based handler discovery | autoload |
| Prompt | Send + wait for response — in-memory only, lost on restart; use Scenes .ask() for persistent question sequences |
prompt |
| Views | Recommended pattern — @gramio/views plugin (templates, JSON, i18n, auto send/edit); plain render-fn fallback for trivial bots |
views |
| JSX | JSX syntax for formatting + keyboards (no React runtime) | jsx |
| Pagination | Fluent paginated inline keyboards (prev/next/first/last, page info) | pagination |
| OpenTelemetry | Distributed tracing, spans, instrumentation | opentelemetry |
| Sentry | Error tracking, performance monitoring | sentry |
| Others | auto-retry, media-cache, media-group, split, posthog | other |
| Plugin Development | Writing custom plugins, derive/decorate/error, lazy loading | plugin-development |
Examples
| Example | Description | File |
|---|---|---|
| Basic bot | Commands, hooks, error handling | basic.ts |
| Keyboards | Reply, inline, columns, conditional | keyboards.ts |
| UX menu | Hero /start, nested menu with breadcrumbs + Back, toggle buttons, destructive confirm step |
ux-menu.ts |
| CallbackData | Type-safe callback schemas | callback-data.ts |
| Formatting | Entity types, join helper, variable composition, parse_mode anti-pattern | formatting.ts |
| File upload | Path, URL, buffer, media groups | file-upload.ts |
| Error handling | Custom errors, suppress, scoped | error-handling.ts |
| Webhook | Framework integration | webhook.ts |
| Session | Counters, settings, Redis | session.ts |
| Scenes | Registration flow with steps | scenes.ts |
| Wizard scene | Callback-driven scene, mixed callback+message steps, global exit | wizard-scene.ts |
| Scene composer inheritance | 3-file package: named scoped composer + Scene.extend + file split for circular-import-safe layout | scene-composer-inheritance/ |
| Callback routing | Centralized router, shared nav CallbackData across features | callback-routing.ts |
| Telegram Stars | Payments, invoices, refunds | telegram-stars.ts |
| TMA | Elysia server, init-data auth, webhook | tma.ts |
| Docker | Graceful shutdown, webhook/polling toggle | docker.ts |
| Testing | User simulation, API mocking, error testing | testing.ts |
| Deep links | /start payload routing (ref / order / opaque token / inline-mode redirect) + my_chat_member admin-rights verification for ?startchannel/?startgroup links |
deep-links.ts |