Create your own
Lesson illustration

Command Handling in Bots

Hello! Welcome back to our course on building Telegram bots.

In our last session, we made your bot significantly more resilient by implementing graceful shutdown. It now handles termination signals properly, ensuring it can stop cleanly without abrupt disconnections. With these foundational aspects of reliability and error handling in place, your bot is now a robust platform ready for us to build upon.

This lesson marks our shift from infrastructure to features. We'll start with the most fundamental way users interact with a bot: commands. You will learn how to register and respond to specific commands like /start and /help, which form the backbone of most bot interactions, particularly the CLI-style bots you're interested in building. We will also see how to make these commands easily discoverable for the user by creating a command menu within the Telegram interface.

What Are Bot Commands?

In Telegram, commands are special messages that begin with a forward slash (/), such as /start or /settings. They are a standardized way for users to send explicit instructions to a bot. When configured correctly, these commands appear in a user's chat interface via a dedicated "Menu" button, providing a clear and accessible entry point for your bot's functionality.

This image shows how registered commands appear to a user in the Telegram chat interface. Tapping the "Menu" button reveals the list of available commands and their descriptions.

This command-based interaction model provides a structured and predictable user experience, much like a command-line interface. For the user, it's a simple way to navigate the bot's features; for the developer, it's a clear way to structure the bot's logic.

Handling Commands with grammY

The grammY framework provides a simple and elegant way to listen for and react to specific commands using the bot.command() method. This allows you to attach a handler function to one or more command strings.

The official grammY documentation explains this feature clearly.

Commands - grammY

This guide covers the essentials of handling commands in grammY.

Please read the initial section, focusing on the Usage part, which introduces the bot.command() method. Also, pay close attention to the code snippet under Suggest Commands to Users, as we will be implementing this shortly.

As the documentation shows, bot.command() is a specialized listener, much like the bot.on("message:text") we used before. However, instead of firing on every text message, it fires only when a message starts with the specified command.

Let's modify our index.ts file to use this. We will replace our generic message echoer with dedicated handlers for the two most common built-in commands: /start and /help.

  1. Remove the old handler: Delete or comment out the bot.on("message:text", ...) block.
  2. Add command handlers: Add new listeners for start and help.

Here's how your index.ts might look after these changes:

// index.ts
import { Bot, GrammyError, HttpError } from "grammy";
import "dotenv/config";

const token = process.env.TELEGRAM_BOT_TOKEN;
if (!token) throw new Error("TELEGRAM_BOT_TOKEN is not set!");

const bot = new Bot(token);

// Error handler
bot.catch((err) => {
  // ... (error handling logic from previous lesson)
});

// --- NEW COMMAND HANDLERS ---

// Handle the /start command.
bot.command("start", (ctx) => {
  ctx.reply("Welcome! I am your new bot. Send /help to see what I can do.");
});

// Handle the /help command.
bot.command("help", (ctx) => {
  ctx.reply("This is a help message. Here are the available commands:\n/start - Start the bot\n/help - Show this help message");
});

// --- REMOVE OR COMMENT OUT THE OLD HANDLER ---
/*
bot.on("message:text", (ctx) => {
  console.log(`Received message from ${ctx.from.first_name}: "${ctx.message.text}"`);
  ctx.reply(`Echo: ${ctx.message.text}`);
});
*/

// Graceful shutdown listeners
process.once("SIGINT", () => bot.stop());
process.once("SIGTERM", () => bot.stop());

// Start the bot
bot.start();

console.log("Bot started with command handlers.");

If you run the bot now (bun run index.ts) and send /start, you'll receive the welcome message. If you send any other text, the bot will remain silent because we removed the catch-all bot.on("message:text") handler. This is a key step in building more intentional bot logic: you handle only the inputs you expect.

Advertising Your Commands

While users can type commands manually, a much better user experience is to present them with a list of available commands. This is the purpose of the "Menu" button shown earlier.

We can programmatically set this list using bot.api.setMyCommands(). This method takes an array of objects, where each object defines a command and a short description. This call only needs to be made once when the bot starts up.

Let's add this to our index.ts file, just before the bot.start() call. This pattern is also demonstrated in the tutorial from Dynamic.xyz, which you can see in the "Main Bot File" section.

// index.ts
import { Bot, GrammyError, HttpError } from "grammy";
// ...

const bot = new Bot(token);

// ... (error handler and command handlers) ...

// --- SET THE COMMANDS MENU ---
// This should be done before starting the bot.
await bot.api.setMyCommands([
  { command: "start", description: "Start the bot and get a welcome message" },
  { command: "help", description: "Display help information" },
]);


// Graceful shutdown listeners
// ...

// Start the bot
await bot.start(); // Note: bot.start() is async, so using await is best practice.

console.log("Bot started with command handlers and menu.");

Note: Since bot.api.setMyCommands() returns a Promise, we should await it. This requires making the surrounding scope async. The easiest way to do this in a modern TypeScript/JavaScript module is to use top-level await. We should also await bot.start() for consistency.

Let's update our index.ts to be a self-contained async scope.

Here is the complete, final version of your index.ts for this lesson:

// index.ts
import { Bot, GrammyError, HttpError } from "grammy";
import "dotenv/config";

async function main() {
    const token = process.env.TELEGRAM_BOT_TOKEN;
    if (!token) throw new Error("TELEGRAM_BOT_TOKEN is not set!");

    const bot = new Bot(token);

    // Error handler
    bot.catch((err) => {
        const ctx = err.ctx;
        console.error(`Error while handling update ${ctx.update.update_id}:`);
        const e = err.error;

        if (e instanceof GrammyError) {
            console.error("Error in request:", e.description);
        } else if (e instanceof HttpError) {
            console.error("Could not contact Telegram:", e);
        } else {
            console.error("Unknown error:", e);
        }
    });

    // Register command handlers
    bot.command("start", (ctx) => ctx.reply("Welcome! Up and running."));
    bot.command("help", (ctx) => ctx.reply("Send /start to get a greeting."));

    // Set command menu
    await bot.api.setMyCommands([
        { command: "start", description: "Start the bot" },
        { command: "help", description: "Show help text" },
    ]);

    // Graceful shutdown listeners
    process.once("SIGINT", () => bot.stop());
    process.once("SIGTERM", () => bot.stop());

    // Start the bot
    await bot.start();
    console.log("Bot has been started.");
}

main().catch(err => {
    console.error(err);
    process.exit(1);
});

I've wrapped the logic in an async function main() to cleanly use top-level await. Now, run your bot again.

Open your Telegram chat with the bot. You should now see the "Menu" button in the input field area. Clicking it will show /start and /help with the descriptions you provided. You can now trigger your handlers by either typing the commands or selecting them from the menu.

This image shows a more advanced example of a command (`/forecast5`) being sent, with the bot providing a structured reply. It also shows other suggested commands available to the user.

Conclusion

You have just implemented one of the most essential features of any Telegram bot. By moving from a generic message handler to specific command handlers, you've laid the groundwork for a structured, feature-rich bot.

Key Takeaways:

  • Commands are user instructions that start with /.
  • You can register a listener for a specific command using bot.command("command_name", handler).
  • To improve user experience, you can create a command menu using bot.api.setMyCommands(). This makes your bot's features discoverable.
  • Structuring your bot's logic around commands is a robust pattern for building functionality.

In our next lesson, we will dive deeper into what you can do inside these handlers. We'll explore the Context object (ctx) in more detail to inspect message properties, and crucially, you'll learn how to parse arguments and parameters that users send along with commands (e.g., /search <query>).

Can't find a good explanation? Sign up and we'll make it for you

Sign up