fe4c19c7c6
Phase 8: Inline Keyboard Infrastructure - Standard stack identified (HTTP Request + Telegram API) - Architecture patterns documented (5 core patterns) - Pitfalls catalogued (5 critical issues) - n8n-specific workarounds for dynamic keyboards
605 lines
23 KiB
Markdown
605 lines
23 KiB
Markdown
# Phase 8: Inline Keyboard Infrastructure - Research
|
|
|
|
**Researched:** 2026-02-03
|
|
**Domain:** Telegram Bot inline keyboards in n8n workflows
|
|
**Confidence:** MEDIUM
|
|
|
|
## Summary
|
|
|
|
This phase implements inline keyboard buttons for the Telegram bot to enable visual, tap-based container control. The implementation must work within n8n's workflow architecture, using HTTP Request nodes for dynamic keyboard generation (native Telegram node has limitations) and callback query handling through Telegram Trigger + Switch routing.
|
|
|
|
**Key constraint:** n8n's native Telegram node does not support dynamic inline keyboards via expressions. The solution requires direct Telegram API calls via HTTP Request nodes to send messages with `reply_markup` JSON.
|
|
|
|
**State management challenge:** n8n workflows are stateless by default. Callback query routing and confirmation timeouts require careful workflow design to maintain context between user interactions.
|
|
|
|
**Primary recommendation:** Use HTTP Request nodes for all inline keyboard operations (send, edit), leverage callback_data for compact state encoding (64-byte limit), and route callbacks via Switch node checking `$json.callback_query.data` patterns.
|
|
|
|
## Standard Stack
|
|
|
|
### Core Technologies
|
|
|
|
| Technology | Version | Purpose | Why Standard |
|
|
|------------|---------|---------|--------------|
|
|
| Telegram Bot API | 7.0+ | Inline keyboard support | Official API for bot features; API 7.0 sets 100-button/64-byte limits |
|
|
| n8n HTTP Request node | Built-in | Dynamic keyboard generation | Native Telegram node lacks dynamic keyboard support; HTTP direct access required |
|
|
| n8n Telegram Trigger | Built-in | Receive callback queries | Configured with `updates: ["message", "callback_query"]` to handle button presses |
|
|
| n8n Switch node | Built-in | Route callbacks by data | Conditional routing based on `callback_query.data` patterns |
|
|
| n8n Code node | Built-in | Build keyboard JSON | JavaScript for generating reply_markup structures dynamically |
|
|
|
|
### Supporting Tools
|
|
|
|
| Tool | Purpose | When to Use |
|
|
|------|---------|-------------|
|
|
| answerCallbackQuery | Acknowledge button presses | **ALWAYS** - Must call within timeout or Telegram shows loading indefinitely |
|
|
| editMessageText | Update message + keyboard | Confirmations, progress updates, navigation between menus |
|
|
| editMessageReplyMarkup | Update keyboard only | When text stays same but buttons change |
|
|
|
|
### n8n-Specific Requirements
|
|
|
|
**Workflow configuration:**
|
|
```json
|
|
{
|
|
"Telegram Trigger": {
|
|
"parameters": {
|
|
"updates": ["message", "callback_query"]
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**Existing workflow already has:**
|
|
- Telegram Trigger with callback_query support (line 8 in workflow)
|
|
- Switch node routing messages vs callbacks (line 82-90)
|
|
- Authentication checks for both paths (line 114, 146)
|
|
|
|
## Architecture Patterns
|
|
|
|
### Pattern 1: HTTP Request for Dynamic Keyboards
|
|
|
|
**What:** Use HTTP Request node instead of native Telegram node to send messages with inline keyboards.
|
|
|
|
**Why:** n8n's Telegram node interprets arrays as strings and rejects expression-based keyboard construction. Community consensus: bypass the native node entirely.
|
|
|
|
**Structure:**
|
|
```javascript
|
|
// In Code node: Build keyboard structure
|
|
const keyboard = {
|
|
inline_keyboard: [
|
|
[
|
|
{ text: "▶️ Start", callback_data: "action:start:plex" },
|
|
{ text: "⏹️ Stop", callback_data: "action:stop:plex" }
|
|
],
|
|
[
|
|
{ text: "🔄 Restart", callback_data: "action:restart:plex" }
|
|
]
|
|
]
|
|
};
|
|
|
|
return {
|
|
json: {
|
|
chatId: chat_id,
|
|
text: "Container: plex",
|
|
reply_markup: keyboard
|
|
}
|
|
};
|
|
```
|
|
|
|
**HTTP Request node configuration:**
|
|
```
|
|
Method: POST
|
|
URL: https://api.telegram.org/bot{{ $credentials.telegramApi.token }}/sendMessage
|
|
Body:
|
|
{
|
|
"chat_id": "={{ $json.chatId }}",
|
|
"text": "={{ $json.text }}",
|
|
"parse_mode": "HTML",
|
|
"reply_markup": {{ JSON.stringify($json.reply_markup) }}
|
|
}
|
|
```
|
|
|
|
**Confidence:** HIGH (verified via n8n community, multiple sources)
|
|
|
|
### Pattern 2: Callback Query Routing with Switch Node
|
|
|
|
**What:** Route callback queries based on callback_data patterns using Switch node.
|
|
|
|
**Structure:**
|
|
```
|
|
Telegram Trigger (callback_query)
|
|
→ Route Update Type (existing Switch: line 82)
|
|
→ IF Callback Authenticated (line 146)
|
|
→ Parse Callback Data (new Code node)
|
|
→ Switch on Action Type (new Switch node)
|
|
├─ "action:list" → Show container list
|
|
├─ "action:select:*" → Show container submenu
|
|
├─ "action:start:*" → Execute start
|
|
├─ "action:stop:*" → Show confirmation
|
|
└─ "confirm:*" / "cancel:*" → Handle confirmation
|
|
```
|
|
|
|
**Data access in Switch node:**
|
|
- Callback query ID: `{{ $json.callback_query.id }}`
|
|
- Callback data: `{{ $json.callback_query.data }}`
|
|
- Chat ID: `{{ $json.callback_query.message.chat.id }}`
|
|
- Message ID: `{{ $json.callback_query.message.message_id }}`
|
|
|
|
**Confidence:** HIGH (verified in existing workflow + official n8n docs)
|
|
|
|
### Pattern 3: Message Editing for In-Place Updates
|
|
|
|
**What:** Edit existing messages to update keyboards and text without creating new messages.
|
|
|
|
**Use cases:**
|
|
1. **Navigation:** Container list → container submenu → action result
|
|
2. **Confirmations:** "Stop container?" with Yes/No buttons
|
|
3. **Progress:** "Stopping plex..." → "Plex stopped ✅"
|
|
|
|
**HTTP Request configuration (editMessageText):**
|
|
```
|
|
Method: POST
|
|
URL: https://api.telegram.org/bot{{ $credentials.telegramApi.token }}/editMessageText
|
|
Body:
|
|
{
|
|
"chat_id": "={{ $json.callback_query.message.chat.id }}",
|
|
"message_id": {{ $json.callback_query.message.message_id }},
|
|
"text": "={{ $json.newText }}",
|
|
"parse_mode": "HTML",
|
|
"reply_markup": {{ JSON.stringify($json.reply_markup) }}
|
|
}
|
|
```
|
|
|
|
**Confidence:** HIGH (Telegram API official)
|
|
|
|
### Pattern 4: Answer Callback Query (Critical)
|
|
|
|
**What:** **ALWAYS** call answerCallbackQuery after receiving a callback, even with no visible notification.
|
|
|
|
**Why:** Telegram clients show loading indicator until bot answers. Failure to answer causes 502 timeout error and poor UX.
|
|
|
|
**HTTP Request configuration:**
|
|
```
|
|
Method: POST
|
|
URL: https://api.telegram.org/bot{{ $credentials.telegramApi.token }}/answerCallbackQuery
|
|
Body:
|
|
{
|
|
"callback_query_id": "={{ $json.callback_query.id }}",
|
|
"text": "={{ $json.notificationText || '' }}",
|
|
"show_alert": false
|
|
}
|
|
```
|
|
|
|
**Best practice:** Call answerCallbackQuery FIRST in callback handling flow, before any other processing.
|
|
|
|
**Confidence:** HIGH (official Telegram docs + python-telegram-bot docs)
|
|
|
|
### Pattern 5: Pagination for Container Lists
|
|
|
|
**What:** Show 5-8 containers per page with Previous/Next navigation buttons.
|
|
|
|
**Recommended structure:**
|
|
```javascript
|
|
// Calculate pagination
|
|
const containersPerPage = 6;
|
|
const currentPage = parseInt(page) || 0;
|
|
const totalPages = Math.ceil(containers.length / containersPerPage);
|
|
const start = currentPage * containersPerPage;
|
|
const pageContainers = containers.slice(start, start + containersPerPage);
|
|
|
|
// Build keyboard
|
|
const keyboard = [];
|
|
|
|
// Container rows (1 per container)
|
|
pageContainers.forEach(container => {
|
|
keyboard.push([{
|
|
text: `${container.name} — ${container.state}`,
|
|
callback_data: `action:select:${container.name}`
|
|
}]);
|
|
});
|
|
|
|
// Navigation row
|
|
const navRow = [];
|
|
if (currentPage > 0) {
|
|
navRow.push({ text: "◀️ Previous", callback_data: `action:list:${currentPage - 1}` });
|
|
}
|
|
if (currentPage < totalPages - 1) {
|
|
navRow.push({ text: "Next ▶️", callback_data: `action:list:${currentPage + 1}` });
|
|
}
|
|
if (navRow.length > 0) keyboard.push(navRow);
|
|
|
|
return {
|
|
json: {
|
|
text: `<b>Containers</b> (${start + 1}-${Math.min(start + containersPerPage, containers.length)} of ${containers.length})`,
|
|
reply_markup: { inline_keyboard: keyboard }
|
|
}
|
|
};
|
|
```
|
|
|
|
**Confidence:** MEDIUM (based on best practices, not n8n-specific verification)
|
|
|
|
### Anti-Patterns to Avoid
|
|
|
|
**1. Using Native Telegram Node for Dynamic Keyboards**
|
|
- **Why bad:** n8n interprets arrays as strings; expressions fail
|
|
- **Instead:** Use HTTP Request node with manually constructed JSON
|
|
|
|
**2. Not Answering Callback Queries**
|
|
- **Why bad:** Telegram client shows loading forever, 502 timeout errors
|
|
- **Instead:** Always call answerCallbackQuery, even with empty parameters
|
|
|
|
**3. Embedding Full State in callback_data**
|
|
- **Why bad:** 64-byte limit enforced by API 7.0; error 400 BUTTON_DATA_INVALID
|
|
- **Instead:** Use compact identifiers (e.g., `action:stop:plex` not `{"action":"stop","container":"plex","user":"admin"}`)
|
|
|
|
**4. Creating New Messages for Updates**
|
|
- **Why bad:** Chat gets cluttered, poor UX
|
|
- **Instead:** Use editMessageText to update in place
|
|
|
|
## Don't Hand-Roll
|
|
|
|
| Problem | Don't Build | Use Instead | Why |
|
|
|---------|-------------|-------------|-----|
|
|
| Callback query timeout management | Custom timeout tracking system | Accept 30-second hard limit, revert via editMessageText | Telegram's timeout is enforced server-side; cannot extend |
|
|
| Button state storage | Redis/database for button state | Encode state in callback_data (64 bytes) | n8n workflows are stateless; external storage adds complexity |
|
|
| Keyboard layouts | Custom positioning logic | 2D array = rows, inner arrays = columns | Telegram's inline_keyboard structure is straightforward |
|
|
| Update detection | Poll Unraid API for updates | Defer to future phase (not in scope) | Context explicitly defers this |
|
|
|
|
## Common Pitfalls
|
|
|
|
### Pitfall 1: callback_data Size Limit Violations
|
|
|
|
**What goes wrong:** Buttons fail with error 400 BUTTON_DATA_INVALID when callback_data exceeds 64 bytes.
|
|
|
|
**Why it happens:** UTF-8 encoding; emojis cost 4 bytes each. `{"action":"restart","container":"linuxserver-plex","user":"admin"}` = 68 bytes.
|
|
|
|
**How to avoid:**
|
|
- Use compact encoding: `action:restart:plex` (22 bytes)
|
|
- Hash long container names if necessary: `a:r:sha1hash`
|
|
- Test byte length: `Buffer.byteLength(callback_data, 'utf8')`
|
|
|
|
**Warning signs:** 400 errors from sendMessage/editMessageText API calls
|
|
|
|
**Confidence:** HIGH (Telegram API 7.0 spec)
|
|
|
|
### Pitfall 2: Switch Node Expression Mismatch
|
|
|
|
**What goes wrong:** Switch node fails to route callbacks; flow doesn't match expected conditions.
|
|
|
|
**Why it happens:** Incorrect data path reference (e.g., `$json.data` instead of `$json.callback_query.data`)
|
|
|
|
**How to avoid:**
|
|
- Use existing workflow pattern: Check `$json.callback_query?.id` for presence (line 63)
|
|
- Access callback data via: `$json.callback_query.data`
|
|
- Use string operations: `.startsWith("action:stop:")` for prefix matching
|
|
- Test with n8n's expression editor to verify data structure
|
|
|
|
**Warning signs:** Switch node always goes to fallback output
|
|
|
|
**Confidence:** MEDIUM (community reports issue, but no clear resolution found)
|
|
|
|
### Pitfall 3: Confirmation Timeout Without Fallback
|
|
|
|
**What goes wrong:** User doesn't respond to "Stop container? Yes/No" within 30 seconds; buttons remain clickable but outdated.
|
|
|
|
**Why it happens:** No timeout handling implemented; old callback_data still works.
|
|
|
|
**How to avoid:**
|
|
- Include timestamp in callback_data: `confirm:stop:plex:1738595200`
|
|
- In callback handler, check if timestamp is within 30 seconds
|
|
- If expired, call editMessageText to remove buttons and show "Confirmation expired"
|
|
|
|
**Warning signs:** Users report clicking old buttons and triggering unexpected actions
|
|
|
|
**Confidence:** MEDIUM (timeout behavior is known; implementation pattern is extrapolated)
|
|
|
|
### Pitfall 4: Race Conditions with editMessageText
|
|
|
|
**What goes wrong:** Multiple editMessageText calls in rapid succession; only last one takes effect, or API returns 429 rate limit errors.
|
|
|
|
**Why it happens:** Telegram API has rate limits; editing same message multiple times quickly violates limits.
|
|
|
|
**How to avoid:**
|
|
- For progress updates, only show final state for quick actions (context decision: "show final result only")
|
|
- For longer operations (updates), use single progress message edit: "Updating..." → "Updated ✅"
|
|
- Don't edit every second; minimum 2-3 second intervals if progress needed
|
|
|
|
**Warning signs:** 429 Too Many Requests errors, messages not updating
|
|
|
|
**Confidence:** MEDIUM (based on API rate limit documentation)
|
|
|
|
### Pitfall 5: Persistent Menu Button Without Implementation
|
|
|
|
**What goes wrong:** Context mentions "persistent menu button" but Telegram Bot API doesn't support this directly.
|
|
|
|
**Why it happens:** Confusion between inline keyboards (message-attached) and reply keyboards (persistent in input field).
|
|
|
|
**How to avoid:**
|
|
- If "persistent" means reply keyboard: Use `ReplyKeyboardMarkup` with `/status` command button (different from inline)
|
|
- If "persistent" means always-visible inline keyboard: Send pinned message with keyboard
|
|
- Clarify requirement: likely means reply keyboard with "🗂 Containers" button that sends `/status`
|
|
|
|
**Warning signs:** Searching for "persistent inline keyboard" yields no results
|
|
|
|
**Confidence:** LOW (ambiguous requirement; needs clarification)
|
|
|
|
## Code Examples
|
|
|
|
### Example 1: Send Container List with Inline Keyboard
|
|
|
|
**Source:** Synthesized from Telegram API docs + n8n HTTP Request pattern
|
|
|
|
```javascript
|
|
// Code node: Build Container List Keyboard
|
|
const containers = $input.all().map(item => item.json);
|
|
const chatId = $('Route Update Type').item.json.message.chat.id;
|
|
|
|
// Group running vs stopped
|
|
const running = containers.filter(c => c.State === 'running');
|
|
const stopped = containers.filter(c => c.State === 'exited');
|
|
|
|
// Build keyboard (max 6 containers for readability)
|
|
const keyboard = [];
|
|
|
|
running.slice(0, 6).forEach(container => {
|
|
const name = container.Names[0].replace(/^\//, '').replace(/^linuxserver-/, '');
|
|
keyboard.push([{
|
|
text: `${name} — Running`,
|
|
callback_data: `select:${name}`
|
|
}]);
|
|
});
|
|
|
|
if (running.length > 6) {
|
|
keyboard.push([{
|
|
text: `View all (${running.length} total)`,
|
|
callback_data: 'list:all:0'
|
|
}]);
|
|
}
|
|
|
|
return {
|
|
json: {
|
|
chatId: chatId,
|
|
text: '<b>🗂 Containers</b>\n\nTap a container to manage it:',
|
|
reply_markup: { inline_keyboard: keyboard }
|
|
}
|
|
};
|
|
```
|
|
|
|
**HTTP Request node:**
|
|
```
|
|
POST https://api.telegram.org/bot{{ $credentials.telegramApi.token }}/sendMessage
|
|
|
|
{
|
|
"chat_id": "={{ $json.chatId }}",
|
|
"text": "={{ $json.text }}",
|
|
"parse_mode": "HTML",
|
|
"reply_markup": {{ JSON.stringify($json.reply_markup) }}
|
|
}
|
|
```
|
|
|
|
### Example 2: Container Submenu with Action Buttons
|
|
|
|
```javascript
|
|
// Code node: Build Container Actions Keyboard
|
|
const containerName = $json.selectedContainer;
|
|
const container = $json.containerDetails; // from Docker API
|
|
const chatId = $json.callback_query.message.chat.id;
|
|
|
|
const keyboard = [];
|
|
|
|
// Action row 1: Start/Stop based on state
|
|
if (container.State === 'running') {
|
|
keyboard.push([
|
|
{ text: '⏹️ Stop', callback_data: `stop:${containerName}` },
|
|
{ text: '🔄 Restart', callback_data: `restart:${containerName}` }
|
|
]);
|
|
} else {
|
|
keyboard.push([
|
|
{ text: '▶️ Start', callback_data: `start:${containerName}` }
|
|
]);
|
|
}
|
|
|
|
// Action row 2: Other actions
|
|
keyboard.push([
|
|
{ text: '📋 Logs', callback_data: `logs:${containerName}` },
|
|
{ text: '⬆️ Update', callback_data: `update:${containerName}` }
|
|
]);
|
|
|
|
// Navigation row
|
|
keyboard.push([
|
|
{ text: '◀️ Back to List', callback_data: 'list:all:0' }
|
|
]);
|
|
|
|
const stateIndicator = container.State === 'running' ? '🟢' : '⚪';
|
|
|
|
return {
|
|
json: {
|
|
text: `${stateIndicator} <b>${containerName}</b>\n\n` +
|
|
`State: ${container.State}\n` +
|
|
`Uptime: ${container.Status}\n` +
|
|
`Image: ${container.Image}`,
|
|
reply_markup: { inline_keyboard: keyboard }
|
|
}
|
|
};
|
|
```
|
|
|
|
### Example 3: Confirmation Flow with Timeout
|
|
|
|
```javascript
|
|
// Code node: Build Stop Confirmation
|
|
const containerName = $json.containerName;
|
|
const timestamp = Math.floor(Date.now() / 1000); // Unix timestamp
|
|
|
|
const keyboard = {
|
|
inline_keyboard: [
|
|
[
|
|
{ text: '✅ Yes, Stop', callback_data: `confirm:stop:${containerName}:${timestamp}` },
|
|
{ text: '❌ No, Cancel', callback_data: `cancel:stop:${containerName}` }
|
|
]
|
|
]
|
|
};
|
|
|
|
return {
|
|
json: {
|
|
text: `⚠️ Stop <b>${containerName}</b>?\n\nThis will stop the container immediately.`,
|
|
reply_markup: keyboard,
|
|
confirmTimestamp: timestamp
|
|
}
|
|
};
|
|
```
|
|
|
|
**Validation in callback handler:**
|
|
```javascript
|
|
// Code node: Validate Confirmation Timeout
|
|
const callbackData = $json.callback_query.data; // "confirm:stop:plex:1738595200"
|
|
const parts = callbackData.split(':');
|
|
const action = parts[0]; // "confirm"
|
|
const operation = parts[1]; // "stop"
|
|
const containerName = parts[2]; // "plex"
|
|
const timestamp = parseInt(parts[3]); // 1738595200
|
|
|
|
const currentTime = Math.floor(Date.now() / 1000);
|
|
const elapsed = currentTime - timestamp;
|
|
|
|
if (elapsed > 30) {
|
|
// Timeout expired
|
|
return {
|
|
json: {
|
|
expired: true,
|
|
text: '⏱️ Confirmation expired. Please try again.',
|
|
removeKeyboard: true
|
|
}
|
|
};
|
|
}
|
|
|
|
// Still valid
|
|
return {
|
|
json: {
|
|
expired: false,
|
|
operation: operation,
|
|
containerName: containerName
|
|
}
|
|
};
|
|
```
|
|
|
|
### Example 4: Answer Callback Query (Always Required)
|
|
|
|
```javascript
|
|
// Code node: Prepare Callback Answer
|
|
const callbackQueryId = $json.callback_query.id;
|
|
|
|
return {
|
|
json: {
|
|
callback_query_id: callbackQueryId,
|
|
text: '', // Empty for silent acknowledgment
|
|
show_alert: false
|
|
}
|
|
};
|
|
```
|
|
|
|
**HTTP Request node (place FIRST in callback flow):**
|
|
```
|
|
POST https://api.telegram.org/bot{{ $credentials.telegramApi.token }}/answerCallbackQuery
|
|
|
|
{
|
|
"callback_query_id": "={{ $json.callback_query_id }}"
|
|
}
|
|
```
|
|
|
|
## State of the Art
|
|
|
|
| Old Approach | Current Approach (2026) | When Changed | Impact |
|
|
|--------------|-------------------------|--------------|--------|
|
|
| Native Telegram node with inline keyboards | HTTP Request node + JSON construction | n8n PR #17258 (pending since Oct 2025) | Native node still lacks dynamic keyboard support; HTTP workaround required |
|
|
| Separate reply keyboard for menus | Inline keyboards for all interactions | Bot API 2.0 (2016) | Inline keyboards don't send messages to chat; cleaner UX |
|
|
| Store button state in database | Encode state in callback_data | Callback queries introduced API 2.0 | Simpler for stateless workflows; 64-byte limit requires compact encoding |
|
|
| 200-button limit for edits | 100-button limit enforced | Bot API 7.0 (2023) | Must use pagination for large lists |
|
|
|
|
**Deprecated/outdated:**
|
|
- **ReplyKeyboardMarkup for action buttons:** Use inline keyboards instead; reply keyboards send text messages, inline keyboards work silently
|
|
- **Long-polling for updates:** Telegram Trigger node handles webhooks automatically in n8n
|
|
- **Custom keyboard builder libraries:** Not applicable in n8n; use Code node with plain JavaScript
|
|
|
|
## Open Questions
|
|
|
|
### 1. Persistent Menu Button Implementation
|
|
|
|
**What we know:** Context mentions "persistent menu button ('Containers')" but doesn't specify type.
|
|
|
|
**What's unclear:**
|
|
- Is this a ReplyKeyboardMarkup (persistent in input field)?
|
|
- Or a pinned inline keyboard message?
|
|
- Or the existing `/start` command menu?
|
|
|
|
**Recommendation:** Implement as ReplyKeyboardMarkup with single button "🗂 Containers" that sends `/status` command. This provides persistent access without cluttering chat.
|
|
|
|
**Confidence:** LOW (ambiguous requirement)
|
|
|
|
### 2. Unraid Update Detection API
|
|
|
|
**What we know:** Context defers "cached update status checking" but mentions "use Unraid's native update detection if accessible."
|
|
|
|
**What's unclear:** Whether Unraid API exposes update availability via Docker socket proxy.
|
|
|
|
**Recommendation:** Research in separate task during implementation. If available, add update indicator emoji (⬆️) to container name in list. If not, defer entirely.
|
|
|
|
**Confidence:** LOW (Unraid API not researched)
|
|
|
|
### 3. Direct Container Access with Inline Response
|
|
|
|
**What we know:** `/status plex` should "jump straight to that container's submenu."
|
|
|
|
**What's unclear:** Should this respond with inline keyboard immediately, or maintain current text-only response?
|
|
|
|
**Recommendation:** Respond with inline keyboard matching the submenu structure (status details + action buttons). Maintains consistency with button-based flow.
|
|
|
|
**Confidence:** MEDIUM (logical extension of requirements)
|
|
|
|
## Sources
|
|
|
|
### Primary (HIGH confidence)
|
|
|
|
- [Telegram Bot API - Official Documentation](https://core.telegram.org/bots/api)
|
|
- sendMessage, editMessageText, answerCallbackQuery methods
|
|
- InlineKeyboardMarkup structure and callback_data constraints
|
|
- [n8n Docs - Telegram Node](https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.telegram/)
|
|
- Native node capabilities and limitations
|
|
- [n8n Docs - Telegram Trigger Node](https://docs.n8n.io/integrations/builtin/trigger-nodes/n8n-nodes-base.telegramtrigger/)
|
|
- Callback query configuration
|
|
- [n8n Docs - Switch Node](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.switch/)
|
|
- Conditional routing patterns
|
|
- [python-telegram-bot v22.5 Docs - CallbackQuery](https://docs.python-telegram-bot.org/en/stable/telegram.callbackquery.html)
|
|
- answerCallbackQuery best practices and timeout behavior
|
|
|
|
### Secondary (MEDIUM confidence)
|
|
|
|
- [n8n Community - Dynamic Inline Keyboard for Telegram Bot](https://community.n8n.io/t/dynamic-inline-keyboard-for-telegram-bot/86568)
|
|
- HTTP Request workaround for dynamic keyboards (verified by community consensus)
|
|
- [GitHub PR #17258 - n8n Telegram JSON Keyboard Support](https://github.com/n8n-io/n8n/pull/17258)
|
|
- Pending feature for native dynamic keyboard support (not yet merged as of Jan 2026)
|
|
- [Telegram Inline Keyboard UX Design Guide](https://wyu-telegram.com/blogs/444/)
|
|
- Best practices for button layout, pagination, performance (Bot API 7.0 constraints)
|
|
- [n8n Workflow Template #7664 - Telegram Inline Keyboard with Dynamic Menus](https://n8n.io/workflows/7664-telegram-bot-inline-keyboard-with-dynamic-menus-and-rating-system/)
|
|
- Reference implementation (couldn't access full workflow JSON)
|
|
|
|
### Tertiary (LOW confidence)
|
|
|
|
- [n8n Community - Telegram Inline Keyboard Callback Query Workflow Example](https://community.n8n.io/t/n8n-telegram-inline-keyboard-callback-query-workflow-example/112588)
|
|
- Community member seeking help; no resolution provided (flags common issue with Switch routing)
|
|
- [n8n Community - Telegram Node Flexible Reply Markup](https://community.n8n.io/t/telegram-node-possible-to-send-flexible-data-for-reply-markup-inline-keyboard/3835)
|
|
- Additional confirmation of native node limitations
|
|
|
|
## Metadata
|
|
|
|
**Confidence breakdown:**
|
|
- Standard stack: **MEDIUM** - HTTP Request workaround verified by community, but no official n8n documentation on this pattern
|
|
- Architecture patterns: **MEDIUM** - Telegram API patterns are HIGH confidence; n8n-specific implementation is based on community consensus and existing workflow analysis
|
|
- Pitfalls: **MEDIUM** - API constraints are well-documented (HIGH); n8n-specific issues are community-reported (MEDIUM); timeout handling is extrapolated (LOW)
|
|
|
|
**Research date:** 2026-02-03
|
|
**Valid until:** ~30 days (stable domain; n8n PR #17258 may change recommendations if merged)
|
|
|
|
**Key research limitations:**
|
|
1. Could not access complete n8n workflow template #7664 (template page showed only CSS, not workflow JSON)
|
|
2. n8n's official docs don't document the HTTP Request workaround for dynamic keyboards (community knowledge only)
|
|
3. No official n8n examples for Telegram inline keyboard + callback query patterns found
|
|
4. "Persistent menu button" requirement is ambiguous (needs clarification during planning)
|