\n\n\n\n My Telegram Bot Setup: Urban Gardening Use Case - AI7Bot \n

My Telegram Bot Setup: Urban Gardening Use Case

📖 12 min read2,300 wordsUpdated Apr 7, 2026

Hey everyone, Marcus here from ai7bot.com. Hope you’re all having a productive week. I’ve been buried in a new project lately, trying to get a particular Telegram bot up and running for a community I’m part of. It’s for a local urban gardening group – we needed a way to quickly share updates about seed swaps, watering schedules for our community plot, and volunteer sign-ups without flooding a main chat. And let me tell you, working with the Telegram Bot API is always an adventure, but a rewarding one.

Today, I want to dive into something I’ve seen a lot of people struggle with, and something I’ve definitely tripped over myself more times than I care to admit: Handling State and Context in Telegram Bots with Python. It sounds a bit abstract, but if you’ve ever built a bot that needs to remember what a user said a few messages ago, or guide them through a multi-step conversation, you know exactly what I’m talking about. We’re going beyond just responding to keywords here; we’re building bots that can actually hold a conversation, even a simple one.

Why State Management is a Headache (and a Necessity)

Think about a typical interaction with a human. If I ask you, “What’s your favorite color?” and you say “Blue,” then I ask, “Why blue?” you instantly know I’m still talking about your favorite color. The context is maintained. Bots, by their nature, are stateless. Each message they receive is usually treated as a brand new interaction, completely independent of previous ones. This is fine for a bot that just echoes text or provides a simple lookup. But what if you want to:

  • Guide a user through a registration process?
  • Ask for multiple pieces of information (e.g., name, age, city)?
  • Allow a user to configure preferences?
  • Implement a simple game where turns matter?

Without remembering the “state” of the conversation – what the user was doing, what question was just asked – your bot becomes incredibly frustrating to use. It’s like talking to someone who has short-term memory loss every five seconds. Not ideal for user experience.

My gardening bot, for example, needs to let users sign up for a volunteer slot. I can’t just ask “What day?” because I first need to know *what task* they want to volunteer for. If they say “Watering,” then I ask “What day?”, the bot needs to remember they’re still in the “Watering volunteer” context. That’s state management in a nutshell.

Enter Python-Telegram-Bot’s ConversationHandler

For Python developers, the python-telegram-bot library is usually the go-to for building Telegram bots. And thankfully, it provides a fantastic tool for managing state: the ConversationHandler. This thing is a lifesaver. It allows you to define a sequence of states (stages of a conversation) and transitions between them, all while storing temporary data specific to that user.

Let’s break down how ConversationHandler works with a simple example. We’ll build a bot that asks for a user’s name and favorite fruit.

Setting Up the Basics

First, you’ll need python-telegram-bot installed. If you don’t have it:

pip install python-telegram-bot --pre

Note the --pre because I usually work with the latest pre-release versions for the newest features, and that’s what I’m using for my examples today. If you prefer stable, just omit it.

Every bot needs a BotFather token. Get yours, and let’s get a basic structure going:

from telegram import Update
from telegram.ext import Application, CommandHandler, MessageHandler, filters, ConversationHandler

# Define conversation states
NAME, FRUIT = range(2)

async def start(update: Update, context):
 """Starts the conversation and asks for the user's name."""
 await update.message.reply_text("Hi there! What's your name?")
 return NAME # Move to the NAME state

async def get_name(update: Update, context):
 """Stores the user's name and asks for their favorite fruit."""
 user_name = update.message.text
 context.user_data['name'] = user_name # Store the name in user_data
 await update.message.reply_text(f"Nice to meet you, {user_name}! What's your favorite fruit?")
 return FRUIT # Move to the FRUIT state

async def get_fruit(update: Update, context):
 """Stores the favorite fruit and ends the conversation."""
 favorite_fruit = update.message.text
 user_name = context.user_data.get('name', 'there') # Retrieve the name
 await update.message.reply_text(f"Got it, {user_name}! Your favorite fruit is {favorite_fruit}. Thanks for sharing!")
 # Clear user_data for this conversation if needed, or just let it persist
 # context.user_data.clear() # Optional: Clear data specific to this convo
 return ConversationHandler.END # End the conversation

async def cancel(update: Update, context):
 """Cancels and ends the conversation."""
 await update.message.reply_text("Okay, cancelled. Maybe next time!")
 return ConversationHandler.END

def main():
 application = Application.builder().token("YOUR_BOT_TOKEN").build()

 conv_handler = ConversationHandler(
 entry_points=[CommandHandler("start", start)],
 states={
 NAME: [MessageHandler(filters.TEXT & ~filters.COMMAND, get_name)],
 FRUIT: [MessageHandler(filters.TEXT & ~filters.COMMAND, get_fruit)],
 },
 fallbacks=[CommandHandler("cancel", cancel)],
 )

 application.add_handler(conv_handler)
 application.run_polling(allowed_updates=Update.ALL_TYPES)

if __name__ == "__main__":
 main()

Dissecting the ConversationHandler

Let’s break down what’s happening in that code, because this is where the magic lies for state management:

  1. NAME, FRUIT = range(2): These are our conversation states. They are just integers, but giving them meaningful names makes the code much more readable. You can have as many as you need to represent different stages of your interaction.

  2. entry_points=[CommandHandler("start", start)]: This tells the ConversationHandler how a user can *start* this particular conversation. In our case, typing /start will trigger the start function.

  3. states={...}: This dictionary is the core of your conversation flow.

    • Keys: These are the state identifiers (NAME, FRUIT).
    • Values: These are lists of handlers that are active *when the conversation is in that specific state*.
      • NAME: [MessageHandler(filters.TEXT & ~filters.COMMAND, get_name)]: When the conversation is in the NAME state, any text message (that isn’t a command) will be handled by the get_name function.
      • FRUIT: [MessageHandler(filters.TEXT & ~filters.COMMAND, get_fruit)]: Similarly, when in the FRUIT state, text messages go to get_fruit.
  4. fallbacks=[CommandHandler("cancel", cancel)]: These are handlers that can be triggered *at any point* in the conversation to break out of it. If a user types /cancel, our cancel function runs, and the conversation ends.

  5. Returning State Identifiers: This is crucial. When a handler function (like start, get_name, get_fruit) is called as part of a ConversationHandler, its return value tells the handler what state to transition to next.

    • return NAME: Moves the conversation to the NAME state.
    • return FRUIT: Moves the conversation to the FRUIT state.
    • return ConversationHandler.END: Terminates the conversation. The bot forgets the user’s current state within this specific conversation.
  6. context.user_data: This is where you store data specific to the user *during* the conversation. It’s a dictionary that persists across different states for that particular user. My bot uses it to remember the user’s name between the NAME and FRUIT states. This is temporary storage for the duration of the conversation or until you explicitly clear it.

So, the flow is: User types /start -> start function runs -> returns NAME -> Bot asks for name. User types “Marcus” -> get_name function runs (because we’re in NAME state) -> stores “Marcus” in user_data -> returns FRUIT -> Bot asks for fruit. User types “Apple” -> get_fruit runs (because we’re in FRUIT state) -> retrieves “Marcus” from user_data, uses “Apple” -> returns ConversationHandler.END. Conversation complete!

A More Complex Scenario: Inline Keyboards and Multiple Paths

My gardening bot, for example, doesn’t just ask for text inputs. It presents choices with inline keyboards. Let’s adapt our example to use an inline keyboard for fruit selection, and also allow for a “skip” option.

Updated Code with Inline Keyboard and Choices

from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import Application, CommandHandler, MessageHandler, filters, ConversationHandler, CallbackQueryHandler

# Define conversation states
NAME, FRUIT_CHOICE, FRUIT_INPUT = range(3)

async def start(update: Update, context):
 """Starts the conversation and asks for the user's name."""
 await update.message.reply_text("Hi there! What's your name?")
 return NAME

async def get_name(update: Update, context):
 """Stores the user's name and presents fruit options."""
 user_name = update.message.text
 context.user_data['name'] = user_name

 keyboard = [
 [InlineKeyboardButton("Apple", callback_data='fruit_apple')],
 [InlineKeyboardButton("Banana", callback_data='fruit_banana')],
 [InlineKeyboardButton("Something Else", callback_data='fruit_other')],
 [InlineKeyboardButton("Skip", callback_data='fruit_skip')],
 ]
 reply_markup = InlineKeyboardMarkup(keyboard)

 await update.message.reply_text(
 f"Nice to meet you, {user_name}! What's your favorite fruit?",
 reply_markup=reply_markup
 )
 return FRUIT_CHOICE # Move to the FRUIT_CHOICE state

async def handle_fruit_choice(update: Update, context):
 """Handles the fruit choice from the inline keyboard."""
 query = update.callback_query
 await query.answer() # Acknowledge the callback query

 choice = query.data
 user_name = context.user_data.get('name', 'there')

 if choice == 'fruit_apple':
 context.user_data['fruit'] = 'Apple'
 await query.edit_message_text(f"Got it, {user_name}! Your favorite fruit is Apple. Thanks for sharing!")
 return ConversationHandler.END
 elif choice == 'fruit_banana':
 context.user_data['fruit'] = 'Banana'
 await query.edit_message_text(f"Got it, {user_name}! Your favorite fruit is Banana. Thanks for sharing!")
 return ConversationHandler.END
 elif choice == 'fruit_other':
 await query.edit_message_text("Okay, what's your favorite fruit then? Type it out.")
 return FRUIT_INPUT # Move to a new state to get text input
 elif choice == 'fruit_skip':
 await query.edit_message_text(f"No problem, {user_name}! We'll skip the fruit for now. Thanks anyway!")
 return ConversationHandler.END

async def get_custom_fruit(update: Update, context):
 """Stores the custom fruit input and ends the conversation."""
 custom_fruit = update.message.text
 user_name = context.user_data.get('name', 'there')
 context.user_data['fruit'] = custom_fruit
 await update.message.reply_text(f"Got it, {user_name}! Your favorite fruit is {custom_fruit}. Thanks for sharing!")
 return ConversationHandler.END

async def cancel(update: Update, context):
 """Cancels and ends the conversation."""
 await update.message.reply_text("Okay, cancelled. Maybe next time!")
 return ConversationHandler.END

def main():
 application = Application.builder().token("YOUR_BOT_TOKEN").build()

 conv_handler = ConversationHandler(
 entry_points=[CommandHandler("start", start)],
 states={
 NAME: [MessageHandler(filters.TEXT & ~filters.COMMAND, get_name)],
 FRUIT_CHOICE: [CallbackQueryHandler(handle_fruit_choice)], # Listen for button presses
 FRUIT_INPUT: [MessageHandler(filters.TEXT & ~filters.COMMAND, get_custom_fruit)], # Listen for text input if 'other' was chosen
 },
 fallbacks=[CommandHandler("cancel", cancel)],
 )

 application.add_handler(conv_handler)
 application.run_polling(allowed_updates=Update.ALL_TYPES)

if __name__ == "__main__":
 main()

Key Changes and Observations

  • New State FRUIT_INPUT: We introduced a third state to specifically handle text input *after* a user chooses “Something Else” from the inline keyboard. This shows how states can branch.
  • CallbackQueryHandler: For inline keyboard buttons, you use a CallbackQueryHandler instead of a MessageHandler. It listens for callback_data strings.
  • query.answer() and query.edit_message_text(): When handling inline keyboard callbacks, it’s good practice to answer() the query (to remove the loading spinner for the user) and often to edit_message_text() to update the previous message, making the conversation flow cleanly.
  • Conditional State Transitions: Inside handle_fruit_choice, depending on the button pressed, we either end the conversation (for Apple/Banana/Skip) or transition to the FRUIT_INPUT state (for “Something Else”). This is powerful for building complex, dynamic conversational flows.

This setup is far more robust. The bot now remembers the user’s name, presents choices, and can even ask for more information if a specific choice is made. This is exactly the kind of state management my gardening bot uses to guide volunteers through signing up for tasks like “Watering,” then asking “Which day?”, and finally confirming.

Actionable Takeaways for Your Next Bot

State management might seem like a hurdle, but once you get the hang of ConversationHandler, it opens up a whole new world of possibilities for your Telegram bots. Here’s what I want you to remember:

  1. Map Your Conversation Flow: Before you even write a line of code, draw out your conversation. What are the steps? What questions do you ask? What are the possible answers, and where do they lead? This will directly translate into your states and transitions.

  2. Use Meaningful State Names: While range(N) is fine, giving your states descriptive names (REGISTRATION_START, ASK_EMAIL, CONFIRM_DETAILS) makes your code much easier to read and maintain, especially as your bot grows.

  3. Leverage context.user_data: This is your temporary memory bank for the user within that specific conversation. Don’t be shy about storing necessary bits of information here. Just remember it’s scoped to the conversation handler instance for that user.

  4. Plan Your Fallbacks: Always include a cancel command or a general fallback for unexpected input. Users will inevitably type things your bot doesn’t expect, and you want to give them an graceful exit or a way to restart.

  5. Test Each State Independently: As you build, test each state transition. Does pressing “Apple” end the conversation? Does “Something Else” correctly move to the input state? Catching these bugs early saves a lot of headaches.

  6. Consider Persistence for Long-Term Data: context.user_data is great for temporary conversation state. If you need to remember user preferences or settings across multiple conversations (e.g., their registered name for all future interactions), you’ll need to save that data to a database (like SQLite, PostgreSQL, or even a simple JSON file). python-telegram-bot has built-in persistence for ConversationHandler, which can save and restore states, but that’s a topic for another day!

Building conversational bots is a bit like designing a choose-your-own-adventure story. You need to anticipate the user’s choices and guide them through the narrative. The ConversationHandler is your trusty compass for doing just that in Telegram. Give it a shot on your next project – you’ll find your bots become much more interactive and user-friendly. Happy bot building!

🕒 Published:

💬
Written by Jake Chen

Bot developer who has built 50+ chatbots across Discord, Telegram, Slack, and WhatsApp. Specializes in conversational AI and NLP.

Learn more →
Browse Topics: Best Practices | Bot Building | Bot Development | Business | Operations
Scroll to Top