Skip to main content

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

  1. Drag a menu.show node onto your canvas
  2. Connect it to a trigger (like /start command)

Step 2: Configure the Menu

SettingValue
Menu IDmain_menu
TitleWelcome! 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!

Result
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]
info

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

  1. Add a menu.paginated_list node
  2. Configure:
SettingValue
Menu IDproducts
Title📦 Products (Page {{page}}/{{total_pages}})
Page Size5
Item Template{{emoji}} {{name}} - ${{price}}
Callback Templateproduct_{{id}}
  1. 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]
ButtonAction
⬅️Previous page (disabled on page 1)
📄 1/4Page 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 DataMatchesOutputParams
product_42ShowProduct{id: "42"}
category_electronicsShowCategory{name: "electronics"}
random_stuffFallback

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

TypeWhat User SeesWhat You Get
textText input promptString value
numberNumber input promptNumeric value
selectButtons to choose fromSelected option key
confirmYes/No buttonsBoolean (true/false)
photoPhoto upload promptFile 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
}

Perfect For

  • Product search
  • User lookup
  • Document finder
  • Any large dataset

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

  1. Prompt → User types search query
  2. Filter → Items matching query shown as buttons
  3. Select → User clicks result
  4. Output → Selected item returned

Configuration

SettingDescription
search_fieldsWhich fields to search (empty = all)
min_query_lengthMinimum characters to search
max_resultsMaximum results to show
no_results_textMessage when no matches

Outputs

OutputDescription
SelectedUser picked a result
SelectedItemThe full item object
QueryWhat the user searched
CancelledUser 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

OutputWhen
YesUser confirmed
NoUser 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

tip

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

tip

Button text should be obvious without context.

✅ "📧 Contact Support"
❌ "CS"

3. Always Provide a Way Out

tip

Every menu should have a back button or cancel option.

4. Handle All Cases

tip

Use the Fallback output from routers to catch unexpected callbacks.

5. Use Pagination for Lists

tip

More than 6 items? Use menu.paginated_list instead of cramming everything into one screen.


Common Patterns

Search → Details Flow

Registration Wizard


Node Reference

NodePurpose
menu.showDisplay menu with items
menu.paginated_listPaginated menu for large lists
menu.routerRoute callbacks by pattern
menu.from_dataTransform data for menus
menu.wizardMulti-step forms
menu.searchIn-menu search
menu.confirmYes/No dialogs
menu.edit_currentEdit current menu message
menu.navigate_backProgrammatic back navigation
menu.resetClear navigation stack

Next Steps