Skip to content

Bot API 10.0 Lands Ecosystem-Wide & Scenes Become Composers

May 8 – 31, 2026

Two headline stories this cycle. First, Telegram Bot API 10.0 rolls out across the entire stack — @gramio/types v10, @gramio/contexts v0.7, @gramio/files v0.5, @gramio/format v0.8, and gramio v0.10 — bringing live photos, guest messages, poll media, message-reaction permissions, and bot access settings. Second, @gramio/scenes v0.7 ships the long-promised scene-as-composer redesign: every Scene is now a full EventComposer, each step is its own sub-composer with .enter/.exit/.fallback, scenes compose into reusable step modules, and there's a new onExit lifecycle hook. Plus @gramio/onboarding v0.2 makes ctx.onboarding flow through bot.extend() with zero ceremony.

Bot API 10.0 — Live Photos, Guest Messages, Poll Media

@gramio/types v10.0.0 regenerates the full type surface for Telegram Bot API 10.0, and @gramio/contexts v0.7.0 lights it up with first-class context getters and mixins.

New attachment: live photos

A new LivePhotoAttachment plus Message.livePhoto, ExternalReplyInfo.livePhoto, and a sendLivePhoto mixin:

ts
bot.on("message", (ctx) => {
    if (ctx.livePhoto) return ctx.send("Nice live photo!");
});

await bot.api.sendLivePhoto({ chat_id, live_photo: "/path/to/live.mov" });

Guest messages

Bot API 10.0 introduces guest messages — a new guest_message update where users interact with your bot without a regular chat. GramIO maps it to MessageContext, exposes Message.guestQueryId / guestBotCallerUser / guestBotCallerChat, adds MessageContext.answerGuestQuery(), and User.supportsGuestQueries(). The framework-level bot.guestQuery(...) shorthand lands in gramio v0.10 (below).

Polls gain media

Polls and their options can now carry media. Poll gains media, explanationMedia, membersOnly, and countryCodes; PollOption gains media. sendPoll accepts per-option media, and correctOptionId is now correctOptionIds (an array) per the API change.

Reactions you can delete, and react-permissions

  • NodeMixin gains deleteReaction and deleteAllReactions.
  • ChatPermissions.canReactToMessages and ChatMember.canReactToMessages() expose the new react-permission bit.
  • ChatMemberControlMixin adds getUserPersonalChatMessages, getManagedBotAccessSettings, and setManagedBotAccessSettings; new BotAccessSettings / SentGuestMessage structures back them.

@gramio/files v0.5.0 & @gramio/format v0.8.0 regenerated

@gramio/files switched its generator to @gramio/schema-parser (fetching the live schema instead of a vendored JSON) and regenerated MEDIA_METHODS — picking up sendLivePhoto, sendPoll, setMyProfilePhoto, plus extra cover/photo/live-photo fields on sendVideo, sendMediaGroup, sendPaidMedia, and editMessageMedia. @gramio/format regenerated its mutator for sendLivePhoto, answerGuestQuery, and the new explanation_media / per-option media formattables. Both bumped their @gramio/types peer to ^10.0.0.

gramio v0.10.0 — Guest Queries, CallbackData Inline Results, Updates Fix

Bot API 10.0 support

gramio v0.10.0 wires up the Bot API 10.0 dependency line (@gramio/types ^10, @gramio/contexts ^0.7, @gramio/files ^0.5, @gramio/format ^0.8, @gramio/test ^0.7) and adds guest_message to the allowed_updates filter.

bot.guestQuery() — handle guest messages

A new shorthand mirroring inlineQuery: it accepts a string / RegExp / predicate trigger (or none, to match any guest message), captures args, and supports macro-options. Reply with ctx.answerGuestQuery(result), where result is a single InlineQueryResult:

ts
import { InlineQueryResult, InputMessageContent } from "gramio";

bot.guestQuery(async (ctx) => {
    // a user reached the bot via a guest message
    await ctx.answerGuestQuery(
        InlineQueryResult.article("1", "Hi!", InputMessageContent.text("Hello, guest!")),
    );
});

// or filter on the guest query text
bot.guestQuery(/^help/i, (ctx) =>
    ctx.answerGuestQuery(
        InlineQueryResult.article("help", "Help", InputMessageContent.text("How can I help?")),
    ),
);

It's deliberately separate from command/hears/startParameter — guest messages have different reply semantics (answerGuestQuery, not ctx.send/reply). See the new guestQuery trigger page.

chosenInlineResult accepts a CallbackData schema

bot.chosenInlineResult(schema, handler) now filters on result_id and unpacks into a typed ctx.queryData, exactly like callbackQuery(schema, …):

ts
import { CallbackData } from "gramio";

const card = new CallbackData("card").number("id");

bot.inlineQuery(/cards/, (ctx) =>
    ctx.answer([
        InlineQueryResult.article(card.pack({ id: 42 }), "Card #42", /* … */),
    ]),
);

bot.chosenInlineResult(card, (ctx) => {
    ctx.queryData.id; // ✅ typed as number
});

String / RegExp / predicate triggers still match against query as before.

No-trigger bot.inlineQuery(handler) overload

bot.inlineQuery(handler) (no trigger) now matches any inline query — handy for the auth-redirect pattern where you answer with an empty result set plus a login button.

Plugin authors: subclass-overlay helpers re-exported

WithDerives, WithEventDerive, WithDecorate, WithExtend, and DeriveHandler are now re-exported from gramio directly, so plugin authors building Composer-derived classes (like Scene) can import them without reaching into @gramio/composer.

Fix: no lost updates when stopping mid-batch

When bot.stop() flipped isStarted to false while a getUpdates batch was in flight, the offset was advanced locally (confirming the batch on Telegram's side) while the batch was dropped — silently losing those updates. v0.10 abandons such a batch without advancing the offset, so Telegram re-delivers it on the next start.

Fix: typed bots assignable to webhookHandler

Bots with derives/plugins/macros couldn't be passed to webhookHandler without as any because their generics sat in contravariant positions. The parameter widened to AnyBot, so the cast is gone.

@gramio/scenes v0.7 — Scenes Become Composers

The biggest scenes release ever. Scene now extends EventComposer, so the full bot-level DSL — .use/.on/.derive/.decorate/.guard/.command/.callbackQuery/.hears/… — is available directly on every scene. On top of that foundation come three big DX wins.

Builder steps — each step is its own sub-composer

The new recommended way to define a step: pass a builder callback that receives a per-step composer with .enter / .exit / .fallback / .message lifecycle hooks plus the full event surface (.on / .command / .callbackQuery / .hears):

ts
import { Scene } from "@gramio/scenes";

const checkout = new Scene("checkout")
    .step("ask-name", (c) =>
        c
            .message("What's your name?") // sugar for .enter(ctx => ctx.send(...))
            .on("message", (ctx) => ctx.scene.update({ name: ctx.text })),
    )
    .step("confirm", (c) =>
        c
            .enter((ctx) => ctx.send(`${ctx.scene.state.name}, confirm? (yes/no)`))
            .hears("yes", (ctx) => ctx.scene.exit())
            .fallback((ctx) => ctx.send("Please answer yes or no")),
    );

State auto-inferred from ctx.scene.update()

Builder steps thread the shape you pass to ctx.scene.update({...}) into ctx.scene.state for every later step — no .state<T>() declaration needed:

ts
new Scene("signup")
    .step("ask", (c) =>
        c.on("message", (ctx) => ctx.scene.update({ name: ctx.text! })),
    )
    .step("greet", (c) =>
        c.enter((ctx) => {
            ctx.scene.state.name; // ✅ inferred as string
        }),
    );

Reusable step modules — scene.extend(otherScene)

Define a nameless Scene as a reusable block of steps and .extend() it into any named scene. Named-step collisions throw; numeric steps are renumbered automatically:

ts
// A reusable confirmation module — cannot be entered directly
const confirm = new Scene().step("confirm", (c) =>
    c
        .enter((ctx) => ctx.send("Are you sure?"))
        .callbackQuery("yes", (ctx) => ctx.scene.step.next())
        .callbackQuery("no", (ctx) => ctx.scene.exit()),
);

const order = new Scene("order")
    .step("review", (c) => c.enter((ctx) => ctx.send("Review your order")))
    .extend(confirm) // ← pulls in the "confirm" step
    .step("done", (c) => c.enter((ctx) => ctx.send("Order placed!")));

onExit lifecycle hook + derive visible in onEnter

Symmetric to onEnter, the new onExit fires when a user leaves a scene (via exit(), exitSub(), or reenter()) before storage is torn down — perfect for cleanup or a "thanks for completing" message. And scene-level .derive() results are now visible inside onEnter, so you can load data and react to it on entry in one chain:

ts
const checkout = new Scene("checkout")
    .derive(async (ctx) => ({ user: await db.users.find(ctx.from!.id) }))
    .onEnter((ctx) => analytics.track("checkout_start", { user: ctx.user }))
    .onExit((ctx) => ctx.send("Thanks for stopping by!"))
    .step("review", (c) => c.message("Looks good?").on("message", confirm));

The classic event-filter step form — .step("message", handler) and .step(["message", "callback_query"], handler) — is fully supported and works alongside builder steps in the same scene. Reach for builder steps in new code for the cleaner per-step lifecycle and automatic state inference. See the scenes guide for both forms side by side.

v0.7.1 — onEnter-consumed derives run exactly once again

A quick follow-up patch: 0.7.0 could run a scene-level .derive() twice on the entry update when its result was consumed inside onEnter (once so onEnter could see it, once in the dispatch chain). @gramio/scenes 0.7.1 restores exactly-once-per-update execution — important if your derive has side effects (counters, spans, DB writes). Upgrade straight to 0.7.1.

@gramio/onboarding v0.2.0 — Typed build()

createOnboarding({ id: "welcome" }).…​.build() previously returned a plain Plugin, erasing the derive shape — so ctx.onboarding.welcome needed module augmentation or a cast at every call site. v0.2 threads the flow Id through the whole builder, so bot.extend(...) now widens ctx.onboarding.welcome automatically:

ts
import { Bot } from "gramio";
import { createOnboarding } from "@gramio/onboarding";

const bot = new Bot(process.env.BOT_TOKEN!).extend(
    createOnboarding({ id: "welcome" })
        .step("hi", { text: "Hi!" })
        .step("done", { text: "All set!" })
        .build(),
);

bot.command("start", (ctx) => {
    ctx.onboarding.welcome.start(); // ✅ typed, no augmentation needed
    return ctx.send("Let's go!");
});

No runtime changes — type-level only — but consumers that relied on the old plain-Plugin return will pick up stricter inference.

Documentation & Skills

  • New guestQuery trigger page documenting the Bot API 10 guest-message flow.
  • Scenes docs restructured to lead with the builder-step API while keeping the event-filter form documented as an equal alternative.
  • New deep-links reference in the AI skills — a single source of truth for every t.me/<bot>?… link family (?start=, ?startapp=, ?startgroup=, payload encoding, admin= tokens) so downstream agents route each link correctly.
  • Telegram method/type reference pages updated to Bot API 10.0, plus a twoslash + Vite build pass across ~200 snippets.