| name | sdk-patterns |
| description | This skill should be used when the user asks about "grammY Bot class", "grammY Context object", "ctx.reply", "ctx.api", "send a message in grammY", "use grammY in code", or needs idiomatic grammY code patterns for everyday bot operations. |
grammY — SDK Patterns
Idiomatic code patterns for the everyday surface of grammY: creating a Bot, handling updates via the Context object, calling the Bot API, and typing context flavors.
Create a Bot
import { Bot } from "grammy";
const bot = new Bot(process.env.BOT_TOKEN!);
The single positional argument is your BotFather token. A second optional argument lets you pass botInfo, client options (timeouts, base URL), or a custom ContextConstructor.
The Context object (ctx)
Every middleware receives one argument — ctx — which represents the current update being processed. It bundles:
| Property | What it is |
|---|
ctx.update | The full Telegram Update object |
ctx.message, ctx.editedMessage, ctx.channelPost … | Aliases for the matching update type |
ctx.from | The sender's User |
ctx.chat | The current Chat |
ctx.callbackQuery | Present for inline-button presses |
ctx.api | Direct Bot API client — ctx.api.sendMessage(chatId, text) |
ctx.me | Info about the bot itself (filled by bot.init()) |
Reply aliases — always prefer these
Instead of ctx.api.sendMessage(ctx.chat.id, "hi"), write ctx.reply("hi"). grammY provides one alias per Bot API send method, automatically using the current chat id:
await ctx.reply("text");
await ctx.replyWithPhoto("https://example.com/img.png", { caption: "Look" });
await ctx.replyWithDocument(new InputFile("./report.pdf"));
await ctx.replyWithAudio("file_id_from_earlier");
await ctx.replyWithVideo(new InputFile("./demo.mp4"));
await ctx.replyWithSticker("CAACAgIA...");
await ctx.replyWithChatAction("typing");
await ctx.editMessageText("updated text");
await ctx.deleteMessage();
await ctx.answerCallbackQuery({ text: "noted" });
await ctx.answerInlineQuery([...]);
Use ctx.api for other chats
ctx.reply only targets the chat the update came from. To send a message to a different chat (broadcast, admin notification, scheduled post):
await ctx.api.sendMessage(adminChatId, "alert: user reported abuse");
Register handlers
bot.command("start", (ctx) => ctx.reply("Welcome"));
bot.command(["help", "h"], (ctx) => ctx.reply("Help text"));
bot.hears("ping", (ctx) => ctx.reply("pong"));
bot.hears(/^echo (.+)$/i, (ctx) => ctx.reply(ctx.match[1]));
bot.on("message:text", (ctx) => { });
bot.on("callback_query:data", (ctx) => { });
bot.on(":file", async (ctx) => {
const file = await ctx.getFile();
console.log(file.file_path);
});
For the full filter-query DSL, open filter-queries.
TypeScript context flavors
The base Context type is augmented by plugins. Whenever you use a plugin that adds methods or properties to ctx, combine its flavor with Context and pass it as a generic to Bot<C>:
import { Bot, Context, SessionFlavor, session } from "grammy";
import { ConversationFlavor, conversations } from "@grammyjs/conversations";
import { HydrateFlavor, hydrate } from "@grammyjs/hydrate";
interface SessionData { counter: number }
type MyContext = HydrateFlavor<
ConversationFlavor<Context & SessionFlavor<SessionData>>
>;
const bot = new Bot<MyContext>(process.env.BOT_TOKEN!);
bot.use(session({ initial: () => ({ counter: 0 }) }));
bot.use(conversations());
bot.use(hydrate());
Order of flavors matches order of bot.use() installation. Forgetting a flavor is the #1 source of Property 'session' does not exist on type 'Context' errors.
API options object
Send methods accept an optional other object with all the Bot API parameters that aren't chat_id / text / file:
await ctx.reply("Bold *italic* `code`", {
parse_mode: "MarkdownV2",
disable_notification: true,
reply_parameters: { message_id: ctx.msg.message_id },
link_preview_options: { is_disabled: true },
});
Initialization (optional, advanced)
By default bot.start() calls bot.init() internally. Call it manually only when:
- You need
ctx.me available before the first update (e.g. serverless cold starts — see deployment-hosting).
- You're constructing several
Bot instances and want to skip duplicate getMe calls by passing { botInfo } from a cache.
await bot.init();
console.log(bot.botInfo);
Graceful shutdown
process.once("SIGINT", () => bot.stop());
process.once("SIGTERM", () => bot.stop());
bot.stop() waits for in-flight updates to finish before exiting.
Bot.api — call any Bot API method
bot.api (and ctx.api) is a typed proxy for every Bot API method, plus options:
await bot.api.setMyCommands([
{ command: "start", description: "Begin" },
{ command: "help", description: "Show help" },
]);
await bot.api.setMyDescription("This bot helps you …");
await bot.api.getMe();
await bot.api.deleteWebhook({ drop_pending_updates: true });
The full method list is curated in the api-reference skill (manual-trigger).