Interactive Menus
Build professional multi-level menus, paginated lists, search, and multi-step forms — all with just a few nodes.
Why Use the Menu System?
The Problem
Building interactive menus manually is tedious:
A simple 3-level menu requires 15+ nodes with manual state tracking!
The Solution
The Menu System handles all the complexity:
- ✅ Automatic back buttons — no manual tracking
- ✅ Navigation history — users can go back naturally
- ✅ Pagination built-in — handle 100+ items easily
- ✅ Multi-step forms — registration flows in one node
- ✅ Search — filter long lists instantly
Result: The same 3-level menu needs just 3 nodes.
Quick Start: Your First Menu
Let's create a simple main menu:
Step 1: Add a Menu Node
- Drag a menu.show node onto your canvas
- Connect it to a trigger (like
/startcommand)
Step 2: Configure the Menu
| Setting | Value |
|---|---|
| Menu ID | main_menu |
| Title | Welcome! Choose an option: |
Step 3: Add Buttons
Connect an array of items to the Items input:
[
{"text": "📊 My Orders", "callback_data": "orders"},
{"text": "👤 Profile", "callback_data": "profile"},
{"text": "⚙️ Settings", "callback_data": "settings"},
{"text": "❓ Help", "callback_data": "help"}
]
Step 4: Handle Selections
The menu outputs:
- Selected → User clicked a button
- Back → User clicked back (empty stack)
- SelectedItem → The item that was clicked
- SelectedIndex → Which button (0, 1, 2...)
Connect these to your next actions!
Welcome! Choose an option:
[📊 My Orders] [👤 Profile]
[⚙️ Settings] [❓ Help]
Nested Menus (Sub-menus)
How It Works
When you show a new menu, it pushes onto the navigation stack:
User clicks "Settings"
→ Stack: [Main Menu, Settings Menu]
→ Back button appears automatically!
User clicks Back
→ Stack: [Main Menu]
→ Returns to main menu
Creating Sub-menus
Simply connect the Selected output to another menu.show:
Automatic Back Buttons
When your menu is a sub-menu (stack depth > 0), a back button appears automatically:
⚙️ Settings
[🔔 Notifications]
[🌙 Dark Mode]
[🔐 Privacy]
[⬅️ Back]
The back button text is configurable via back_button_text setting.
Pagination (Large Lists)
When You Need Pagination
Got a long list? Use menu.paginated_list instead of menu.show:
- Product catalogs
- Order history
- User lists
- Search results
Creating a Paginated Menu
- Add a menu.paginated_list node
- Configure:
| Setting | Value |
|---|---|
| Menu ID | products |
| Title | 📦 Products (Page {{page}}/{{total_pages}}) |
| Page Size | 5 |
| Item Template | {{emoji}} {{name}} - ${{price}} |
| Callback Template | product_{{id}} |
- Connect your data array to Items input
What Users See
📦 Products (Page 1/4)
[🍎 Apple - $1.99]
[🍊 Orange - $2.49]
[🍋 Lemon - $0.99]
[🥝 Kiwi - $3.49]
[🍇 Grapes - $4.99]
[⬅️] [📄 1 / 4] [➡️]
[⬅️ Back]
Navigation Controls
| Button | Action |
|---|---|
| ⬅️ | Previous page (disabled on page 1) |
| 📄 1/4 | Page indicator (not clickable) |
| ➡️ | Next page (disabled on last page) |
Handling Selection
The SelectedIndex output gives you the global index (not page index):
User on Page 2, clicks item 3
→ SelectedIndex = 8 (not 3!)
→ SelectedItem = items[8]
Callback Router
The Problem
After a menu click, you need to figure out what was clicked:
callback_data: "product_42"
callback_data: "category_electronics"
callback_data: "action_delete_15"
The Solution: menu.router
The menu.router node routes callbacks using patterns:
{
"routes": [
{
"pattern": "^product_(\\d+)$",
"output": "ShowProduct",
"params": ["id"]
},
{
"pattern": "^category_(\\w+)$",
"output": "ShowCategory",
"params": ["name"]
}
]
}
What Happens
| Callback Data | Matches | Output | Params |
|---|---|---|---|
product_42 | ✅ | ShowProduct | {id: "42"} |
category_electronics | ✅ | ShowCategory | {name: "electronics"} |
random_stuff | ❌ | Fallback | — |
Using Extracted Parameters
The Params output gives you the extracted values:
Params.id = "42"
Use these to fetch the right product, category, etc.!
Multi-Step Forms (Wizard)
Perfect For
- User registration
- Order checkout
- Profile setup
- Surveys
Creating a Wizard
Use the menu.wizard node:
{
"wizard_id": "registration",
"steps": [
{
"id": "name",
"title": "👤 What's your name?",
"type": "text",
"required": true
},
{
"id": "email",
"title": "📧 Enter your email:",
"type": "text",
"validation": "contains(value, '@')",
"error_message": "Please enter a valid email"
},
{
"id": "plan",
"title": "📦 Choose your plan:",
"type": "select",
"options": [
{"key": "free", "label": "🆓 Free"},
{"key": "pro", "label": "⭐ Pro - $9/mo"}
]
},
{
"id": "confirm",
"title": "✅ Confirm registration?",
"type": "confirm"
}
]
}
Step Types
| Type | What User Sees | What You Get |
|---|---|---|
| text | Text input prompt | String value |
| number | Number input prompt | Numeric value |
| select | Buttons to choose from | Selected option key |
| confirm | Yes/No buttons | Boolean (true/false) |
| photo | Photo upload prompt | File ID |
Progress Indicator
Users see where they are:
📝 Step 2/4
📧 Enter your email:
[⬅️ Back] [❌ Cancel]
Validation
Add validation expressions:
{
"validation": "len(value) >= 2",
"error_message": "Name must be at least 2 characters"
}
If validation fails, the error message shows and the user must retry.
Getting Results
When the wizard completes:
- Completed output fires
- FormData contains all answers:
{
"name": "John",
"email": "john@example.com",
"plan": "pro",
"confirm": true
}
In-Menu Search
Perfect For
- Product search
- User lookup
- Document finder
- Any large dataset
Creating Search
Use the menu.search node:
{
"menu_id": "product_search",
"title": "🔍 Search for a product:",
"search_fields": ["name", "description"],
"max_results": 5,
"item_template": "{{name}} - ${{price}}"
}
How It Works
- Prompt → User types search query
- Filter → Items matching query shown as buttons
- Select → User clicks result
- Output → Selected item returned
Configuration
| Setting | Description |
|---|---|
| search_fields | Which fields to search (empty = all) |
| min_query_length | Minimum characters to search |
| max_results | Maximum results to show |
| no_results_text | Message when no matches |
Outputs
| Output | Description |
|---|---|
| Selected | User picked a result |
| SelectedItem | The full item object |
| Query | What the user searched |
| Cancelled | User canceled the search |
Confirmation Dialogs
Quick Confirmations
Use menu.confirm for Yes/No questions:
{
"title": "🗑️ Delete this item?",
"yes_text": "Yes, Delete",
"no_text": "Cancel",
"destructive": true
}
Outputs
| Output | When |
|---|---|
| Yes | User confirmed |
| No | User declined |
Destructive Mode
Set destructive: true for dangerous actions — shows a warning style:
⚠️ Delete this item?
This cannot be undone!
[Yes, Delete] [Cancel]
Best Practices
1. Keep Menus Shallow
Aim for 3 levels max. Deeper menus frustrate users.
Main Menu → Category → Item Details ✅
Main Menu → Category → Sub-Category → Item → Details ❌
2. Use Clear Labels
Button text should be obvious without context.
✅ "📧 Contact Support"
❌ "CS"
3. Always Provide a Way Out
Every menu should have a back button or cancel option.
4. Handle All Cases
Use the Fallback output from routers to catch unexpected callbacks.
5. Use Pagination for Lists
More than 6 items? Use menu.paginated_list instead of cramming everything into one screen.
Common Patterns
Main Menu with Sub-menus
Search → Details Flow
Registration Wizard
Node Reference
| Node | Purpose |
|---|---|
| menu.show | Display menu with items |
| menu.paginated_list | Paginated menu for large lists |
| menu.router | Route callbacks by pattern |
| menu.from_data | Transform data for menus |
| menu.wizard | Multi-step forms |
| menu.search | In-menu search |
| menu.confirm | Yes/No dialogs |
| menu.edit_current | Edit current menu message |
| menu.navigate_back | Programmatic back navigation |
| menu.reset | Clear navigation stack |
Next Steps
- Keyboards & Buttons → — Low-level button customization
- Tutorial: E-commerce Bot → — See menus in action
- Variables → — Store menu selections