Pagination & Limits
Long lists need paging, and busy actions need rate limits. The cleanest approach uses a parameterized menu whose page param carries the current page across every press — no extra state needed.
Menu-based pagination
A page: Number param slices the list and provides prev/next navigation. The param is remembered across presses, so the next/prev buttons always know where they are.
shared products: Array<{ id: String, name: String, price: Number }> = []
menu productList(page: Number) {
text: `📦 Products (page ${page + 1})`
for p in array.slice(shared.products, page * 5, page * 5 + 5) {
text `${p.name} — $${p.price}` -> viewProduct(pid: p.id)
row
}
text "⬅ Prev" -> goPage(page: page - 1)
text "Next ➡" -> goPage(page: page + 1)
}
flow shop on command "/shop" {
send.text("Products:", reply_markup: productList(0))
}
subflow goPage(page: Number) {
edit.text(message: callback.message, text: "Products:", reply_markup: productList(page))
}
subflow viewProduct(pid: String) {
set { flow.product = array.find(shared.products, "id", pid) }
when types.isNil(flow.product) {
send.answer_callback(text: "No longer available.")
} else {
send.text(`${flow.product.name}\n$${flow.product.price}`)
}
}
Hiding unavailable navigation buttons
Conditionally omit the prev/next buttons by reading the page and list length. Since menus re-render on every press, the buttons reflect the current state automatically:
shared products: Array<{ id: String, name: String, price: Number }> = []
menu productList(page: Number) {
text: `📦 Products (page ${page + 1})`
for p in array.slice(shared.products, page * 5, page * 5 + 5) {
text `${p.name} — $${p.price}` -> viewProduct(pid: p.id)
row
}
// Static null-drop: buttons whose expr evaluates to null are dropped at render.
// Build the row conditionally in the handler instead.
text "⬅ Prev" -> goPage(page: page - 1)
text "Next ➡" -> goPage(page: page + 1)
}
For precise hide/show logic, compute the slice and boundary checks in the handler then rebuild the menu reference:
subflow goPage(page: Number) {
set { flow.clamped = math.max(page, 0) }
set { flow.maxPage = math.floor((util.len(shared.products) - 1) / 5) }
set { flow.safe = math.min(flow.clamped, flow.maxPage) }
edit.text(message: callback.message, text: "Products:", reply_markup: productList(flow.safe))
}
Multi-param pagination (filter + page)
Combine a category filter and a page number into a two-param menu:
menu catalog(cat: String, page: Number) {
text: `🗂 ${cat} (p. ${page + 1})`
for p in array.slice(array.where(shared.products, "category", cat), page * 4, page * 4 + 4) {
text p.name -> viewProduct(pid: p.id)
row
}
text "⬅" -> turnPage(cat: cat, page: page - 1)
text "➡" -> turnPage(cat: cat, page: page + 1)
}
subflow turnPage(cat: String, page: Number) {
edit.text(message: callback.message, text: "Catalog:", reply_markup: catalog(cat, page))
}
Both cat and page travel through every button press automatically.
flow.paginate recipe
For a simpler single-list paginator with built-in selection handling, use the flow.paginate recipe:
flow browse on command "/browse" {
flow.paginate(items: "shared.products", page_size: 5, header: "🛒 Products", doneLabel: "Done", timeout: "300s") {
on_select {
send.text(`You picked: ${selected.name}`)
}
on_done {
send.text("Closed the catalog.")
}
}
}
| Param | Notes |
|---|---|
items! | Name of the array to paginate |
page_size | Items per page |
header | Title shown above the list |
doneLabel | Label of the done button |
timeout | How long to wait for interaction |
Handlers: on_select, on_done. See Recipes.
Rate limiting
Allow an action only when a per-user counter is under a threshold:
flow claim on command "/claim" {
flow.rateLimit(key: `claims:${user.id}`, max: 5) {
body {
set { user.rewards += 1 }
send.text("🎁 Reward claimed!")
}
on_limited {
send.text("⏳ You've hit today's claim limit. Come back later.")
}
}
}
| Param | Notes |
|---|---|
key! | The counter key (often per-user) |
max! | Maximum allowed before limiting |
Handlers: body, on_limited.