Skip to main content
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:
  1. Talk to @BotFather
  2. Send /mybots and select your bot
  3. Go to Bot SettingsInline Mode
  4. 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",
});

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
  });
});

Pagination

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

Build docs developers (and LLMs) love