Skip to content

Cheat Sheet

Quick reference for the most common GramIO patterns. Click any heading to go to the full docs.

On this page: Setup · Commands · Hears · Any update · Derive & Decorate · Guards · Inline Keyboard · Callback Query · Reply Keyboard · Formatting · Files · Session · Scenes · I18n · Error Handling · Hooks · Plugins · Write Plugin · Inline Query · Autoload · Testing · Webhook


Setup

bash
npm create gramio my-bot
ts
import { Bot } from "gramio";

const bot = new Bot(process.env.BOT_TOKEN as string);

bot.start();

Commands

ts
bot.command("start", (ctx) => ctx.send("Hello!"));

// With arguments: /say hello world → ctx.args = "hello world"
bot.command("say", (ctx) => ctx.send(ctx.args ?? "nothing"));

Hears

ts
// Exact string
bot.hears("hi", (ctx) => ctx.send("Hey!"));

// Regex — ctx.args holds the match array
bot.hears(/^hello (.+)/i, (ctx) => ctx.send(`Hi, ${ctx.args?.[1]}!`));

// Function predicate
bot.hears(
    (ctx) => ctx.text?.startsWith("?"),
    (ctx) => ctx.send("A question!"),
);

Listen to any update

ts
bot.on("message", (ctx) => ctx.send("Got your message!"));

bot.on(["message", "edited_message"], (ctx) => ctx.send("New or edited!"));

Derive & Decorate

derive runs per-request; decorate runs once at startup.

ts
// Per-request: enriches ctx with fresh data each time
const bot = new Bot(token)
    .derive(async (ctx) => ({
        user: await db.getUser(ctx.from?.id),
    }))
    .on("message", (ctx) => {
        ctx.user; // fully typed ✅
    });
ts
// Per-update-type: only for specific events
const bot = new Bot(token)
    .derive("message", async (ctx) => ({
        isAdmin: await db.isAdmin(ctx.from.id),
    }))
    .on("message", (ctx) => {
        ctx.isAdmin; // ✅ — only typed on "message" handlers
    });
ts
// Startup-time: no per-request overhead
const bot = new Bot(token)
    .decorate({ db, redis, config })
    .on("message", (ctx) => {
        ctx.db.query("..."); // ✅ — same instance every time
    });

Guards

Stop the middleware chain when a condition isn't met. Downstream handlers only run if the guard passes.

ts
// Reusable admin guard
const adminOnly = bot.guard(
    (ctx) => ctx.from?.id === ADMIN_ID,
    // optional rejection handler:
    (ctx) => ctx.send("Admins only."),
);

adminOnly.command("ban", (ctx) => ctx.send("User banned."));
ts
// Narrow update type — filter for messages with text
const textOnly = bot.guard((ctx) => ctx.is("message") && !!ctx.text);

textOnly.on("message", (ctx) => {
    ctx.text; // string — narrowed by the guard ✅
});

Inline Keyboard

ts
import { InlineKeyboard, CallbackData } from "gramio";

// Simple text buttons
const keyboard = new InlineKeyboard()
    .text("Yes ✅", "yes")
    .text("No ❌", "no")
    .row()
    .url("GitHub", "https://github.com/gramiojs/gramio");

ctx.send("Choose:", { reply_markup: keyboard });
ts
// Type-safe callback data
const actionData = new CallbackData("action").number("id");

ctx.send("Pick one:", {
    reply_markup: new InlineKeyboard()
        .text("Item 1", actionData.pack({ id: 1 }))
        .text("Item 2", actionData.pack({ id: 2 })),
});

Callback Query

ts
// String match
bot.callbackQuery("yes", (ctx) => ctx.editText("You said yes!"));

// Type-safe CallbackData
const actionData = new CallbackData("action").number("id");

bot.callbackQuery(actionData, (ctx) => {
    ctx.send(`You picked ID: ${ctx.queryData.id}`);
    //                                      ^? number
});

Reply Keyboard

ts
import { Keyboard } from "gramio";

const keyboard = new Keyboard()
    .text("Option A")
    .text("Option B")
    .row()
    .requestLocation("Share location 📍")
    .resized();

ctx.send("Choose:", { reply_markup: keyboard });

Remove Keyboard

ts
import { RemoveKeyboard } from "gramio";

ctx.send("Keyboard removed.", { reply_markup: new RemoveKeyboard() });

Format Messages

ts
import { format, bold, italic, link, code, pre, spoiler } from "gramio";

ctx.send(format`
    ${bold`Hello!`} Welcome to ${link("GramIO", "https://gramio.dev")}.

    Here is some ${italic`styled`} text and a ${spoiler`surprise`}.

    ${code("inline code")} or a block:
    ${pre("const x = 1;", "typescript")}
`);

Send Files

ts
import { MediaUpload } from "@gramio/files";

// From disk
ctx.sendDocument(await MediaUpload.path("./report.pdf"));

// From URL
ctx.sendPhoto(await MediaUpload.url("https://example.com/cat.png"));

// From Buffer
ctx.sendDocument(await MediaUpload.buffer(buffer, "file.pdf"));

// By file_id (already uploaded to Telegram)
ctx.sendPhoto("AgACAgIAAxk...");

Session

ts
import { session } from "@gramio/session";

const bot = new Bot(process.env.BOT_TOKEN as string).extend(
    session({
        key: "session",
        initial: () => ({ count: 0 }),
    }),
);

bot.command("count", (ctx) => {
    ctx.session.count++;
    //    ^? { count: number }
    ctx.send(`Count: ${ctx.session.count}`);
});

Scenes (conversations)

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

const loginScene = new Scene("login")
    .step("message", (ctx) => {
        if (ctx.scene.step.firstTime) return ctx.send("Enter your email:");
        return ctx.scene.update({ email: ctx.text });
    })
    .step("message", (ctx) => ctx.send(`Registered: ${ctx.scene.state.email}`));

const bot = new Bot(process.env.BOT_TOKEN as string)
    .extend(session())
    .extend(scenes([loginScene]));

bot.command("login", (ctx) => ctx.scene.enter(loginScene));

I18n

ts
import {
    defineI18n,
    type LanguageMap,
    type ShouldFollowLanguage,
} from "@gramio/i18n";
import { format, bold } from "gramio";

const en = {
    welcome: (name: string) => format`Hello, ${bold(name)}!`,
    items: (count: number) => `You have ${count} item${count === 1 ? "" : "s"}`,
} satisfies LanguageMap;

const ru = {
    welcome: (name: string) => format`Привет, ${bold(name)}!`,
    items: (count: number) =>
        `У вас ${count} предмет${count === 1 ? "" : "ов"}`,
} satisfies ShouldFollowLanguage<typeof en>; // must match en keys/signatures

const i18n = defineI18n({ primaryLanguage: "en", languages: { en, ru } });

const bot = new Bot(token).derive((ctx) => ({
    t: i18n.buildT(ctx.from?.language_code ?? "en"),
}));

bot.command("start", (ctx) =>
    ctx.send(ctx.t("welcome", ctx.from?.firstName ?? "stranger")),
);

Error Handling

ts
// Catch all errors
bot.onError(({ context, kind, error }) => {
    console.error(kind, error.message);
    if (context.is("message")) context.send("Something went wrong.");
});

// Only for specific update types
bot.onError("message", ({ context, kind, error }) => {
    context.send(`${kind}: ${error.message}`);
});

// Custom typed errors
class NoRights extends Error {
    constructor(public role: "admin" | "moderator") {
        super();
    }
}

const bot = new Bot(process.env.BOT_TOKEN as string)
    .error("NO_RIGHTS", NoRights)
    .onError(({ kind, error, context }) => {
        if (kind === "NO_RIGHTS" && context.is("message"))
            context.send(`You need the «${error.role}» role.`);
    });

Hooks

ts
bot.onStart((info) => console.log(`@${info.username} is running!`));

bot.onStop(() => console.log("Shutting down..."));

// Intercept every API request
bot.preRequest((ctx) => {
    console.log("Calling", ctx.method);
    return ctx;
});

// Inspect every response
bot.onResponse((ctx) => {
    console.log(ctx.method, "→", ctx.response);
    return ctx;
});

Use a Plugin

ts
import { autoAnswerCallbackQuery } from "@gramio/auto-answer-callback-query";

bot.extend(autoAnswerCallbackQuery());

Write a Plugin

ts
import { Plugin } from "gramio";

const myPlugin = new Plugin("my-plugin").derive("message", (ctx) => ({
    isAdmin: ctx.from?.id === 123456789,
}));

bot.extend(myPlugin);

bot.on("message", (ctx) => {
    if (ctx.isAdmin) ctx.send("Hi boss!");
    //     ^? boolean
});

Inline Query

ts
bot.inlineQuery("cats", async (ctx) => {
    await ctx.answer(
        [
            ctx.buildInlineQueryResult.article({
                id: "1",
                title: "Cat fact",
                input_message_content: { message_text: "Cats purr at 25Hz." },
            }),
        ],
        { cache_time: 30 },
    );
});

Autoload handlers

ts
import { autoload } from "@gramio/autoload";

// Loads all files from ./src/commands/**/*.ts automatically
const bot = new Bot(process.env.BOT_TOKEN as string).extend(autoload());
ts
// src/commands/start.ts
import type { Bot } from "gramio";

export default (bot: Bot) => bot.command("start", (ctx) => ctx.send("Hi!"));

Testing

ts
import { describe, expect, it } from "bun:test";
import { Bot } from "gramio";
import { TelegramTestEnvironment } from "@gramio/test";

const bot = new Bot("test");
bot.command("start", (ctx) => ctx.send("Hello!"));

const env = new TelegramTestEnvironment(bot);
const user = env.createUser({ first_name: "Alice" });

// Simulate a /start command
await user.sendMessage("/start");

// Assert the response
expect(env.apiCalls[0].method).toBe("sendMessage");
expect(env.apiCalls[0].params.text).toBe("Hello!");

Webhook

GramIO has no built-in HTTP server — bring your own framework and use webhookHandler:

ts
import { Bot, webhookHandler } from "gramio";
import Fastify from "fastify";

const bot = new Bot(process.env.BOT_TOKEN as string);
const fastify = Fastify();

fastify.post("/webhook", webhookHandler(bot, "fastify"));
fastify.listen({ port: 3000, host: "::" });

bot.start({
    webhook: { url: "https://example.com/webhook" },
});

See all supported frameworks → (Hono, Express, Elysia, Koa, Bun.serve, Deno.serve, node:http)