- 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
```typescript
// `message` means ONLY `update.message` — not edits, not channel posts
bot.on("message", (ctx) => { /* … */ });
// Shortcuts for the first level:
bot.on("msg", (ctx) => { /* message + channel_post */ });
bot.on("edit", (ctx) => { /* edited_message + edited_channel_post */ });
// All four kinds — pass an array (logical OR)
bot.on(["message", "edited_message", "channel_post", "edited_channel_post"], (ctx) => { /* … */ });
// Only direct messages with text
bot.on("message:text", (ctx) => { /* … */ });
// Only text — for both messages AND channel posts (leading colon is a wildcard for the L1 type)
bot.on(":text", (ctx) => { /* … */ });
// Photos
bot.on("message:photo", (ctx) => { /* … */ });
// Any file payload (photo | live_photo | video | animation | document | audio | voice | video_note | sticker)
bot.on(":file", async (ctx) => {
const file = await ctx.getFile();
});
// Visual media only (photo | live_photo | video)
bot.on(":media", (ctx) => { /* … */ });
// Edited messages and channel posts — the shortcuts combine with any second level
bot.on("edit:text", (ctx) => { /* … */ });
bot.on("msg:text", (ctx) => { /* message or channel post with text */ });
// Stickers
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":
```typescript
// Messages whose text contains a URL
bot.on("message:entities:url", (ctx) => { /* … */ });
// Messages whose caption contains a URL
bot.on("message:caption_entities:url", (ctx) => { /* … */ });
// Either — most common form
bot.on("message::url", (ctx) => { /* … */ });
// Entities of any type (mention, hashtag, bot_command, url, email, …)
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
```typescript
// Matches text messages OR edited text messages with a URL
bot.on(
["message:entities:url", "edited_message:entities:url"],
(ctx) => { /* … */ },
);
```
### Chaining — logical AND
```typescript
// Messages or channel posts that contain a URL AND are forwards
bot.on("::url").on(":forward_origin", (ctx) => {
// both conditions hold
});
```
### Combined with bot.command / bot.hears
```typescript
// Photo messages in private chats that also carry a caption.
// `photo` has no third level — chain two queries instead of "message:photo:caption".
bot
.filter((ctx) => ctx.chat?.type === "private")
.on("message:photo")
.on("message:caption", (ctx) => { /* … */ });
```
## Type narrowing
Filter queries are not just runtime predicates — they refine the TypeScript type of `ctx`:
```typescript
bot.on("message", async (ctx) => {
const text: string | undefined = ctx.msg.text; // could be photo/video/etc.
});
bot.on("message:text", async (ctx) => {
const text: string = ctx.msg.text; // guaranteed present
});
bot.on("callback_query:data", async (ctx) => {
const data: string = ctx.callbackQuery.data; // guaranteed
});
```
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()`:
```typescript
// Only process every second update
bot
.filter((ctx) => ctx.update.update_id % 2 === 0)
.on("message", (ctx) => { /* … */ });
// Type-predicate variant — narrows ctx too
function isFromAdmin(ctx: Context): ctx is Context & { from: { id: number } } {
return ctx.from?.id === ADMIN_ID;
}
bot.filter(isFromAdmin, async (ctx) => {
ctx.from.id; // narrowed to number, not number | undefined
});
```
## Cheat sheet
| Query | Fires on |
|---|---|
| `message` | Any `message` update (text, photo, doc, …) — not edits or channel posts |
| `msg` | `message` or `channel_post` |
| `edit` | `edited_message` or `edited_channel_post` |
| `msg:text` / `edit:text` | Text in a message or channel post / in an edit of one |
| `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 |
| `message:chat_owner_changed` | Chat ownership transferred (the key was misspelled before grammY 1.42) |
| `:text` | Text in message OR channel post |
| `:media` | Photo, live photo or video |
| `:file` | Any file attachment (photo, live photo, video, animation, document, audio, voice, video note, sticker) |
| `::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` | Another member's status changed (needs `allowed_updates` and admin rights) |
| `my_chat_member` | The bot's own status changed — added, removed, promoted, blocked |
| `pre_checkout_query` | Payment confirm step (10s deadline) |
| `message:successful_payment` | Payment completed (`:successful_payment` also matches channel posts) |
| `business_connection` | Telegram Business connected/disconnected |
| `business_message` | Message in a Business-managed chat |
For payments and business specifics, open `payments-business-games`.
View on GitHub