| name | filter-queries |
| description | This skill should be used when the user asks about "grammY filter query", "bot.on syntax", "listen for specific update types", "message:text", "filter photos", "filter URLs", or needs to understand grammY's filter-query DSL — the framework's signature feature for routing updates with type narrowing. |
grammY — Filter Queries
The filter-query DSL is grammY's signature feature: a compact string syntax for bot.on() that picks the exact update types you want and narrows the TypeScript context type at the same time.
The mental model
Telegram updates form a tree:
update
├── message
│ ├── text
│ ├── photo
│ ├── caption
│ ├── entities[]
│ └── forward_origin
├── edited_message (same shape)
├── channel_post (same shape)
├── edited_channel_post (same shape)
├── callback_query
│ └── data
├── inline_query
└── …
A filter query is a colon-separated path into this tree. bot.on("a:b:c") fires when update.a.b.c exists.
The basics
bot.on("message", (ctx) => { });
bot.on("message:text", (ctx) => { });
bot.on(":text", (ctx) => { });
bot.on("message:photo", (ctx) => { });
bot.on(":file", async (ctx) => {
const file = await ctx.getFile();
});
bot.on("message:sticker", (ctx) => { });
Entities and captions — the :: form
message::url means URL entity in either the text OR the caption. The double colon is a shortcut for "in entities or caption_entities":
bot.on("message:entities:url", (ctx) => { });
bot.on("message:caption_entities:url", (ctx) => { });
bot.on("message::url", (ctx) => { });
bot.on("message:entities", (ctx) => { });
All standard entity types are valid: mention, hashtag, cashtag, bot_command, url, email, phone_number, bold, italic, underline, strikethrough, spoiler, code, pre, text_link, text_mention, custom_emoji.
Combining queries
Array — logical OR
bot.on(
["message:entities:url", "edited_message:entities:url"],
(ctx) => { },
);
Chaining — logical AND
bot.on("::url").on(":forward_origin", (ctx) => {
});
Combined with bot.command / bot.hears
bot
.filter((ctx) => ctx.chat?.type === "private")
.on("message:photo:caption", (ctx) => { });
Type narrowing
Filter queries are not just runtime predicates — they refine the TypeScript type of ctx:
bot.on("message", async (ctx) => {
const text: string | undefined = ctx.msg.text;
});
bot.on("message:text", async (ctx) => {
const text: string = ctx.msg.text;
});
bot.on("callback_query:data", async (ctx) => {
const data: string = ctx.callbackQuery.data;
});
This is why filter queries should always be preferred over if (ctx.msg.text) runtime guards.
Generic .filter() for non-tree predicates
When a check is not expressible as a tree path (random sampling, custom permission, runtime flag), use bot.filter():
bot
.filter((ctx) => ctx.update.update_id % 2 === 0)
.on("message", (ctx) => { });
function isFromAdmin(ctx: Context): ctx is Context & { from: { id: number } } {
return ctx.from?.id === ADMIN_ID;
}
bot.filter(isFromAdmin, async (ctx) => {
ctx.from.id;
});
Cheat sheet
| Query | Fires on |
|---|
message | Any direct message (text, photo, doc, …) |
message:text | Text message |
message:photo | Photo |
message:document | File |
message:voice | Voice note |
message:video_note | Round video |
message:location | Location share |
message:contact | Contact share |
message:new_chat_members | User joined |
message:left_chat_member | User left |
:text | Text in message OR channel post |
:file | Any media attachment |
::url | URL in text or caption |
::bot_command | /something in text or caption |
edited_message | User edited a message |
callback_query:data | Inline-button press |
inline_query | User typed @bot in any chat |
chosen_inline_result | User picked an inline result |
chat_member | Bot's chat-member status changed |
my_chat_member | Bot was added/removed/promoted |
pre_checkout_query | Payment confirm step (10s deadline) |
successful_payment | Payment completed |
business_connection | Telegram Business connected/disconnected |
business_message | Message in a Business-managed chat |
For payments and business specifics, open payments-business-games.