Inline queries allow users to interact with your bot from any chat by typing @your_bot query in the message input field. This is perfect for creating search bots, content aggregators, and interactive utilities.
Enabling Inline Mode
First, enable inline mode for your bot:
Talk to @BotFather
Send /mybots and select your bot
Go to Bot Settings → Inline Mode
Enable inline mode and optionally set a placeholder
Basic Usage
import { Bot , InlineKeyboard } from "grammy" ;
import { InlineQueryResultBuilder } from "grammy" ;
const bot = new Bot ( "YOUR_BOT_TOKEN" );
bot . on ( "inline_query" , async ( ctx ) => {
const query = ctx . inlineQuery . query ;
const results = [
InlineQueryResultBuilder . article (
"id-1" ,
"Article Title" ,
"Article description"
). text ( `You searched for: ${ query } ` ),
InlineQueryResultBuilder . photo (
"id-2" ,
"https://example.com/photo.jpg" ,
"https://example.com/thumb.jpg"
),
];
await ctx . answerInlineQuery ( results );
});
Result Types
grammY supports all Telegram inline result types:
InlineQueryResultBuilder . article (
"unique-id" ,
"Article Title" ,
"Description shown in the list"
). text ( "Message content when selected" , {
parse_mode: "Markdown" ,
});
InlineQueryResultBuilder . photo (
"unique-id" ,
"https://example.com/image.jpg" , // photo URL
"https://example.com/thumb.jpg" // thumbnail URL
). caption ( "Photo caption" );
InlineQueryResultBuilder . video (
"unique-id" ,
"https://example.com/video.mp4" ,
"video/mp4" ,
"https://example.com/thumb.jpg" ,
"Video Title"
);
InlineQueryResultBuilder . gif (
"unique-id" ,
"https://example.com/animation.gif" ,
"https://example.com/thumb.jpg"
);
Adding Keyboards
Attach inline keyboards to inline results:
const keyboard = new InlineKeyboard ()
. text ( "Button 1" , "callback-1" )
. url ( "Visit Site" , "https://grammy.dev" );
const result = InlineQueryResultBuilder . article (
"id" ,
"Title" ,
"Description"
)
. text ( "Message content" )
. keyboard ( keyboard );
await ctx . answerInlineQuery ([ result ]);
Search Example
interface SearchItem {
id : string ;
title : string ;
description : string ;
url : string ;
}
const database : SearchItem [] = [
{
id: "1" ,
title: "Getting Started" ,
description: "Learn how to create your first bot" ,
url: "https://grammy.dev/quickstart" ,
},
// ... more items
];
bot . on ( "inline_query" , async ( ctx ) => {
const query = ctx . inlineQuery . query . toLowerCase ();
// Search the database
const matches = database . filter (
( item ) =>
item . title . toLowerCase (). includes ( query ) ||
item . description . toLowerCase (). includes ( query )
);
// Build results
const results = matches . slice ( 0 , 50 ). map (( item ) =>
InlineQueryResultBuilder . article ( item . id , item . title , item . description )
. text (
` ${ item . title } \n\n ${ item . description } \n\n ${ item . url } ` ,
{ parse_mode: "Markdown" }
)
. keyboard (
new InlineKeyboard (). url ( "Learn More" , item . url )
)
);
await ctx . answerInlineQuery ( results , {
cache_time: 300 , // Cache results for 5 minutes
});
});
Handle large result sets with pagination:
bot . on ( "inline_query" , async ( ctx ) => {
const offset = parseInt ( ctx . inlineQuery . offset ) || 0 ;
const limit = 50 ;
const results = getAllResults ()
. slice ( offset , offset + limit )
. map (( item , index ) =>
InlineQueryResultBuilder . article (
` ${ offset + index } ` ,
item . title ,
item . description
). text ( item . content )
);
await ctx . answerInlineQuery ( results , {
next_offset: results . length === limit ? String ( offset + limit ) : "" ,
});
});
Chosen Inline Results
Track which results users actually choose:
bot . on ( "chosen_inline_result" , async ( ctx ) => {
const resultId = ctx . chosenInlineResult . result_id ;
const query = ctx . chosenInlineResult . query ;
console . log ( `User chose result ${ resultId } for query: ${ query } ` );
// Update analytics, track usage, etc.
});
You must enable chosen inline result feedback in BotFather for this to work. Send /setinlinefeedback to @BotFather.
Answer Options
await ctx . answerInlineQuery ( results , {
cache_time: 300 , // Cache results on Telegram servers (seconds)
is_personal: true , // Results are specific to this user
next_offset: "50" , // Offset for pagination
button: { // Show a button above results
text: "Open in Bot" ,
start_parameter: "inline" ,
},
});
Best Practices
Quick Responses Answer inline queries within 1 second. Cache results when possible.
Limit Results Return at most 50 results per query. Use pagination for more.
Cache Intelligently Set appropriate cache_time based on how often your content changes.
Handle Empty Queries Provide useful default results when the query is empty.
Advanced Example: Image Search Bot
import { Bot } from "grammy" ;
const bot = new Bot ( "YOUR_BOT_TOKEN" );
bot . on ( "inline_query" , async ( ctx ) => {
const query = ctx . inlineQuery . query || "cats" ;
// Fetch images from an API
const images = await searchImages ( query );
const results = images . map (( img , index ) =>
InlineQueryResultBuilder . photo (
` ${ query } - ${ index } ` ,
img . url ,
img . thumbnail
)
. caption ( ` ${ img . title } - ${ query } ` )
. keyboard (
new InlineKeyboard ()
. url ( "View Full Size" , img . fullUrl )
. switchInlineCurrent ( "Search Again" , query )
)
);
await ctx . answerInlineQuery ( results , {
cache_time: 600 ,
is_personal: false ,
});
});
bot . on ( "chosen_inline_result" , async ( ctx ) => {
// Track which images users select
console . log ( "Chosen:" , ctx . chosenInlineResult );
});
bot . start ();
Common Issues
Make sure:
Inline mode is enabled in BotFather
You’re answering the query within 30 seconds
Result IDs are unique
Required fields are provided for each result type
Keyboards on inline results only appear when the user sends the result. They don’t show in the inline query list.
Cache API responses
Use pagination to limit results
Consider using answerInlineQuery options to cache results on Telegram’s servers
Optimize database queries
See Also