Bot API 9.5 Lands, Rate Limiter Debuts, Composer Gets Superpowers
February 23 – March 2, 2026
A massive week for the GramIO ecosystem. Telegram Bot API 9.5 is fully supported — member tags, date_time entities, and can_manage_tags admin rights. A brand-new @gramio/rate-limit plugin arrives with macro-based per-handler throttling. @gramio/format learns to parse HTML directly into Telegram entities. @gramio/composer ships EventContextOf, ContextOf, defineComposerMethods, and a full Elysia-inspired macro system. Plus @gramio/views gains sticker/voice/video_note support, create-gramio generates CLAUDE.md context files for AI tools, and @gramio/scenes gets cross-chain deduplication.
Bot API 9.5 — Member Tags & date_time Entities
Telegram Bot API 9.5 is now fully supported across the entire ecosystem.
Member tags: set, clear, and verify text labels per user
Bots can now assign plain text tags (up to 16 characters, no emoji) to group and supergroup members. Tags require the can_manage_tags administrator right. GramIO exposes this via the new setChatMemberTag method and context shorthand:
// Set a tag using the context shorthand
bot.command("tag", async (ctx) => {
if (!ctx.replyToMessage) return ctx.send("Reply to a user to tag them");
await ctx.replyToMessage.setMemberTag("VIP");
await ctx.reply("Tag set!");
});
// Remove a tag by passing undefined (or empty string)
await ctx.setMemberTag(undefined);The ChatMember type gains new fields:
| Field | Type | Available on |
|---|---|---|
tag | string | ChatMemberMember, ChatMemberRestricted |
canEditTag | boolean | ChatMemberRestricted, ChatPermissions |
canManageTags | boolean | ChatAdministratorRights, ChatMemberAdministrator |
senderTag | string | undefined | Message |
To allow other admins to set tags, pass can_manage_tags: true to promoteChatMember.
New date_time MessageEntity type
Telegram now marks timestamps in messages with date_time entities. GramIO surfaces the new fields on MessageEntity:
bot.on("message", (ctx) => {
const dateEntities = ctx.entities?.filter((e) => e.type === "date_time");
for (const entity of dateEntities ?? []) {
console.log(entity.unixTime); // Unix timestamp
console.log(entity.dateTimeFormat); // Telegram's format string
}
});Updated packages: @gramio/types v9.5.0, gramio v0.7.0, @gramio/contexts v0.5.0, @gramio/keyboards v1.3.1
@gramio/rate-limit v0.0.1 — Rate Limiting via Macros
Brand-new rate limiting plugin with per-handler throttling
@gramio/rate-limit is a new official plugin that protects your bot handlers from abuse using sliding-window rate limiting. The key design choice: it uses GramIO's macro system for per-handler options instead of imperative if (!await ctx.rateLimit(...)) return checks.
import { Bot } from "gramio";
import { rateLimit } from "@gramio/rate-limit";
const bot = new Bot(process.env.BOT_TOKEN!)
.extend(
rateLimit({
// Optional: plug in Redis, SQLite, Cloudflare KV…
// storage: redisStorage(redis),
onLimitExceeded: async (ctx) => {
if (ctx.is("message")) await ctx.reply("Too many requests, slow down!");
},
}),
);
// Throttle per handler — no if-checks needed in handler body
bot.command("pay", (ctx) => {
// process payment
}, { rateLimit: { limit: 3, window: 60 } });
bot.command("help", (ctx) => ctx.reply("Help text"), {
rateLimit: {
id: "help",
limit: 20,
window: 60,
onLimitExceeded: (ctx) => ctx.reply("Too many /help requests!"),
},
});
await bot.start();The plugin ships with in-memory storage out of the box. Swap in Redis, SQLite, or Cloudflare KV by passing a storage option from @gramio/storages.
Note on the export name: The plugin function was renamed from
rateLimitPlugintorateLimitin the same release. UserateLimit— the old name is gone.
@gramio/format v0.5.0 — Parse HTML into Telegram Entities
htmlToFormattable(): send HTML content without parse_mode
@gramio/format v0.5.0 adds htmlToFormattable() — a new sub-module that converts HTML markup into GramIO's FormattableString format. This is a perfect complement to markdownToFormattable() for when your content source produces HTML (CMS outputs, TipTap, ProseMirror, LLM-generated HTML, etc.).
The approach is the same as with Markdown: parse locally into entities, send without any parse_mode. Invalid or partial HTML degrades gracefully to plain text instead of erroring.
Install the peer dependency first:
npm install node-html-parseryarn add node-html-parserpnpm add node-html-parserbun add node-html-parserThen import from the @gramio/format/html sub-path:
import { htmlToFormattable } from "@gramio/format/html";
import { Bot } from "gramio";
const bot = new Bot(process.env.BOT_TOKEN!);
bot.command("start", (ctx) => {
const content = `<h1>Hello!</h1><p><strong>Bold</strong> and <em>italic</em></p>
<ul><li>item one</li><li>item two</li></ul>
<p>Visit <a href="https://gramio.dev">gramio.dev</a></p>`;
ctx.send(htmlToFormattable(content));
});
await bot.start();Supported HTML elements:
| HTML | Telegram entity |
|---|---|
<b>, <strong> | bold |
<i>, <em> | italic |
<u> | underline |
<s>, <del>, <strike> | strikethrough |
<code> | code |
<pre><code class="language-js"> | pre (with language) |
<blockquote> | blockquote |
<a href="..."> | text_link |
<h1>–<h6> | bold |
<ul>, <ol>, <li> | plain text with bullet/number |
<br> | newline |
Note: This API may change in the future as it stabilizes.
join() now accepts FormattableString arrays directly
The join() helper gains a new overload: you can pass an array of FormattableString objects directly without a mapping function:
import { join, bold, italic } from "@gramio/format";
const items: FormattableString[] = [bold("one"), italic("two"), "three"];
// New: pass the array directly
const result = join(items, "\n");
// Before you needed: join(items, (x) => x, "\n")@gramio/composer v0.3.3 — EventContextOf, ContextOf, Macro System
EventContextOf<T, E>: per-event derive visibility in custom methods
EventContextOf<TComposer, TEvent> is a new utility type that extracts the full context for a specific event from a composer instance — including both global and per-event derives. This is what you need when writing custom methods that use .derive(['event1']) to scope enrichment:
import type { EventContextOf } from "@gramio/composer";
// Per-event derive: only visible in 'message' handlers
composer.derive(['message'], () => ({ messageData: "..." }));
// EventContextOf extracts both global + per-event derives for 'message'
type MessageCtx = EventContextOf<typeof composer, 'message'>;
// MessageCtx includes 'messageData'ContextOf<T> and defineComposerMethods(): type-safe custom methods with derives
Writing custom methods that receive accumulated derives used to require complex generic gymnastics. Now there are two clean tools:
ContextOf<T> — extracts TOut (the fully accumulated context type) from a composer instance:
import type { ContextOf } from "@gramio/composer";
type Ctx = ContextOf<typeof myComposer>; // infers accumulated contextdefineComposerMethods() — required when your custom methods have generic signatures, because TypeScript can't infer generics through nested types:
import { defineComposerMethods, createComposer } from "@gramio/composer";
import type { ComposerLike, ContextOf, Middleware } from "@gramio/composer";
const methods = defineComposerMethods({
command<TThis extends ComposerLike<TThis>>(
this: TThis,
name: string,
handler: Middleware<MessageCtx & ContextOf<TThis>>,
): TThis {
return this.on("message", (ctx, next) => {
if (ctx.text === `/${name}`) return handler(ctx, next);
return next();
});
},
});
const { Composer } = createComposer<BaseCtx, EventMap, typeof methods>({
discriminator: (ctx) => ctx.updateType,
methods,
});
// Derives flow into the handler automatically — zero annotation:
new Composer()
.derive(() => ({ user: { id: 1 } }))
.command("start", (ctx) => {
ctx.user.id; // ✅ typed
});Macro system: Elysia-inspired per-handler options
macro() lets you register reusable behaviors that handlers activate declaratively via an options object — no manual if (!await ctx.check()) return patterns:
bot.macro("adminOnly", {
preHandler: async (ctx, next) => {
if (ctx.from?.id !== ADMIN_ID) return ctx.reply("Admins only");
return next();
},
});
// Activate in any handler by passing options:
bot.command("ban", handler, { adminOnly: true });
bot.command("kick", handler, { adminOnly: true });The macro runs before the handler body. Multiple macros compose in registration order.
WeakMap-backed property isolation fix
A critical bug affecting GramIO context properties is fixed. Previously, group() and extend() used Object.create(ctx) for isolation — which broke WeakMap-backed private fields (like GramIO's lazy-cached getters ctx.text, ctx.from) because prototype chain lookups don't work with WeakMaps.
The fix replaces Object.create(ctx) with a snapshot/restore strategy: capture keys before execution, delete new keys after, restore original values. GramIO context properties in isolation groups now work correctly.
@gramio/views v0.1.1 — Sticker, Voice, Video Note Support
Views now send stickers, voice messages, and video notes
@gramio/views v0.1.1 expands the supported media types to include sticker, voice, and video_note. Each has a different edit behavior since Telegram doesn't allow changing these media files after sending:
| Media type | Send | Edit behavior |
|---|---|---|
sticker | ✅ | editReplyMarkup only (keyboard update) |
voice | ✅ | editCaption + keyboard |
video_note | ✅ | editReplyMarkup only (keyboard update) |
import { defineView } from "@gramio/views";
const stickerView = defineView().render(function (fileId: string) {
this.media({ type: "sticker", media: fileId });
this.keyboard(/* ... */);
});Render methods (renderWithContext, performSend, performEdit) now return proper typed values (RenderSendResult, RenderResult) instead of void.
@gramio/scenes — Cross-Chain Deduplication & EventComposer extend
Plugins extended at bot level are no longer re-applied in scenes
If you extend a plugin at the bot level, entering a scene used to re-apply it — causing double middleware execution. The scene engine now checks the bot's main composer for already-applied plugins and skips them:
const authPlugin = new Plugin("auth").derive(() => ({ user: getUser() }));
const bot = new Bot(token)
.extend(authPlugin) // ← applied here
.extend(scenes); // scenes skip authPlugin inside scene chainsScenes can now extend() with EventComposers
scene.extend() now accepts EventComposer instances in addition to Plugin objects, enabling type-safe per-event derives inside scenes:
const echoScene = new Scene("echo")
.extend(myEventComposer) // ← new: EventComposer accepted
.on("message", (ctx) => ctx.send(ctx.text!));create-gramio v2.0.0–v2.0.3 — AI Tools, Broadcast, CLI Presets
CLAUDE.md generation for AI coding agents
npm create gramio@latest now generates a CLAUDE.md file in your project root — a context file for AI coding agents (Claude Code, Cursor, etc.) explaining your bot's tech stack, architecture, key commands, and which plugins are enabled.
AI Skills opt-in for GramIO knowledge
New projects can opt in to installing GramIO's AI Skills (bunx skills add) during scaffolding — enabled by default in recommended and full presets. Skills give your AI assistant deep GramIO knowledge with examples and plugin references.
Broadcast plugin support
The scaffolder now includes @gramio/broadcast as an optional plugin choice (with Redis and graceful shutdown wired up automatically).
CLI argument parsing and presets
Full CLI argument parsing landed: npm create gramio@latest ./bot --preset=recommended --orm=drizzle --linter=biome — no interactive prompts needed for CI or scripted setups. Three presets: minimal, recommended, full.
@gramio/files v0.3.2 — Buffer → Uint8Array Fix
Buffer objects are now properly converted to Uint8Array before being passed to the File constructor, fixing an incompatibility with environments that don't extend Uint8Array with Buffer (e.g. some Bun builds). undefined values in form data are now skipped instead of serialized.
Documentation Site — Major Content Push
The docs site received a massive content update this cycle — new pages, rewrites, and guides across the board.
Homepage rewrite: type-safety first, with live code tabs
The homepage has a completely new structure. The hero messaging shifts from "create bots with convenience" to "build bots the right way" with type-safety as the central theme. A new "See it in action" section adds five interactive code tabs covering Commands & Formatting, Keyboards & Callbacks, I18n, Scenes, and Composer — each showing real GramIO patterns with derive(), CallbackData, format, and guard().
New Introduction page with framework comparison
A new /introduction page explains GramIO's design philosophy with concrete examples of type propagation, formatting, and plugin composition. Includes a comparison table vs grammY and Telegraf covering type propagation, formatting approach, multi-runtime support, test utilities, and scenes.
Get Started guide rewrite
The get-started guide is rebuilt from scratch with a streamlined onboarding flow, comprehensive code examples (commands, formatting, keyboards, derive, middleware), and a production pattern section explaining the shared plugin Composer architecture used in real projects.
New Composer guide: modular bot architecture
A new /guides/composer page documents how to structure multi-file bots using the Composer as a module system — file-per-feature pattern, sharing context via derive()/decorate() with .as("scoped"), ContextOf typing for extracted handlers, static dependencies, and a suggested file structure. Includes a Composer vs Plugin comparison table.
Four migration guides added
Step-by-step migration guides with side-by-side code comparisons for developers switching from other frameworks:
- From grammY — context shortcuts, middleware, keyboards, sessions
- From Telegraf —
Telegraf→Bot,.action()→.callbackQuery(),ctx.telegram→ctx.api - From puregram — handler style, command registration, reply methods
- From node-telegram-bot-api — callback-based → async/await, full TypeScript patterns
Cheat Sheet expanded with derive, guards, scenes, i18n, testing
The Cheat Sheet gets a full expansion: per-request and per-update-type derive() patterns, decorate() for static values, adminOnly/textOnly guard examples with type narrowing, multi-step scenes with ask(), i18n pluralization, and TelegramTestEnvironment testing patterns. Quick navigation anchors added to all sections.
Guides index restructured as a learning path
The guides index is rebuilt as a structured learning path: a beginner series table, topic sections (Bot Setup, Payments, Filtering, AI & Tooling, Migration), and a quick references section with links to all key pages.
Filters guide + @gramio/schema-parser ecosystem page
A new filters guide covers filter-only .on() with auto-discovery via CompatibleEvents, inline filters, and standalone predicates (reply, isBot, isPremium, forwardOrigin, senderChat). A new @gramio/schema-parser ecosystem page documents the native TypeScript Telegram API schema parser.
allow_paid_broadcast documented in rate-limits guide
The rate-limits guide gains a new section on allow_paid_broadcast: enables up to 1,000 messages/second at 0.1 Stars per message, with a warning about checking bot balance via @BotFather before large campaigns.
cache_time tip added to inline query guide
The inline query guide now documents that context.answer() accepts all answerInlineQuery parameters as a second argument, with cache_time (default 300 s) as the most useful. Recommends cache_time: 0 for dynamic results and during development.