Challenge: Telegram bots can only register ONE webhook URL globally. Need: Handle multiple workflow types (approvals, commands, status queries, button callbacks). Solution: Master router pattern with cascading IF nodes.
❌ NOT ALLOWED:
Telegram Bot
├─ Webhook 1: /approve → Approval Workflow
├─ Webhook 2: /submit_job → Job Submission Workflow
├─ Webhook 3: /status → Status Query Workflow
└─ Webhook 4: callback_query → Button Handler Workflow
⚠️ Telegram API only allows ONE webhook URL per bot.
// Telegram API method
setWebhook({
url: 'https://domain.com/webhook/telegram-1' // ✅ Works
});
setWebhook({
url: 'https://domain.com/webhook/telegram-2' // ❌ Overwrites previous webhook
});
// Result: Only ONE webhook can be active at a time.
Single Telegram Webhook
↓
Master Router Workflow (n8n)
↓
Pattern Matching (IF nodes)
↓
┌───────┴───────┬─────────┬─────────┬──────────┐
│ │ │ │ │
Approval Submit Status Button Help
Workflow Job Query Handler Message
Workflow Workflow Workflow
/**
* Master Router: Receives ALL Telegram updates
* Routes to appropriate sub-workflow based on message content
*/
async function masterRouter(telegramUpdate) {
const update = telegramUpdate.body;
// Route 1: Text Commands
if (update.message && update.message.text) {
const text = update.message.text;
const chatId = update.message.chat.id;
// Pattern matching with cascading IF logic
if (text.startsWith('/approve')) {
// Extract job ID from command: "/approve abc123"
const jobId = text.split(' ')[1];
return triggerWorkflow('proposal-approval', { jobId, chatId });
}
else if (text.startsWith('/reject')) {
const jobId = text.split(' ')[1];
return triggerWorkflow('proposal-rejection', { jobId, chatId });
}
else if (text.startsWith('/submit_job')) {
// Extract URL from command: "/submit_job https://upwork.com/jobs/~123"
const jobUrl = text.split(' ')[1];
return triggerWorkflow('job-manual-submit', { jobUrl, chatId });
}
else if (text.startsWith('/status')) {
return triggerWorkflow('status-query', { chatId });
}
else if (text.startsWith('/interview')) {
const taskId = text.split(' ')[1];
return triggerWorkflow('interview-prep', { taskId, chatId });
}
else if (text === '/start' || text === '/help') {
return sendHelpMessage(chatId);
}
else {
// Unknown command
return sendMessage(chatId, 'Unknown command. Use /help for available commands.');
}
}
// Route 2: Inline Button Callbacks
else if (update.callback_query) {
const callbackData = update.callback_query.data; // e.g., "approve_abc123"
const chatId = update.callback_query.message.chat.id;
const messageId = update.callback_query.message.message_id;
// Parse callback data pattern: "action_id"
const [action, jobId] = callbackData.split('_');
switch (action) {
case 'approve':
return triggerWorkflow('button-approve', { jobId, chatId, messageId });
case 'reject':
return triggerWorkflow('button-reject', { jobId, chatId, messageId });
case 'edit':
return triggerWorkflow('button-edit', { jobId, chatId, messageId });
default:
return answerCallbackQuery(update.callback_query.id, 'Unknown action');
}
}
// Route 3: Other Update Types (future expansion)
else if (update.edited_message) {
// Handle edited messages (if needed)
console.log('Message edited, ignoring');
}
else {
// Unknown update type
console.log('Unknown update type:', Object.keys(update));
}
}
/**
* Helper: Trigger sub-workflow via internal webhook
*/
async function triggerWorkflow(workflowName, data) {
// n8n workflows can trigger each other via webhooks
await fetch(`https://n8n.domain.com/webhook/${workflowName}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
}
/**
* Helper: Send help message
*/
async function sendHelpMessage(chatId) {
const helpText = `
*Available Commands:*
/submit_job <url> - Submit Upwork job URL for processing
/status - Show active jobs and proposals
/approve <id> - Approve proposal
/reject <id> - Reject proposal
/interview <id> - Generate interview prep
/help - Show this message
*Inline Buttons:*
Tap buttons in messages for quick actions (Approve/Reject/Edit)
`;
await telegram.sendMessage({
chat_id: chatId,
text: helpText,
parse_mode: 'Markdown'
});
}
[Webhook Trigger: /webhook/telegram]
↓
[Parse Telegram Update]
↓
[IF: update.message?]
├─ Yes → [Extract text]
│ ↓
│ [IF: text starts with '/approve'?]
│ ├─ Yes → [Trigger Approval Workflow]
│ └─ No → [Next IF node]
│ ↓
│ [IF: text starts with '/submit_job'?]
│ ├─ Yes → [Trigger Job Submit Workflow]
│ └─ No → [Next IF node]
│ ↓
│ [IF: text starts with '/status'?]
│ ├─ Yes → [Trigger Status Workflow]
│ └─ No → [Send Unknown Command Message]
└─ No → [IF: update.callback_query?]
├─ Yes → [Parse callback_data]
│ ↓
│ [IF: action === 'approve'?]
│ ├─ Yes → [Trigger Button Approve Workflow]
│ └─ No → [Next IF node]
│ ↓
│ [IF: action === 'reject'?]
│ ├─ Yes → [Trigger Button Reject Workflow]
│ └─ No → [Unknown Action]
└─ No → [Ignore/Log]
/approve, /submit_job, etc.callback_data for action and parametersRequirement: Add /report command to generate weekly report.
Implementation:
// In master router, add new IF branch:
else if (text.startsWith('/report')) {
return triggerWorkflow('weekly-report', { chatId });
}
That’s it! No changes to:
| Approach | Pros | Cons |
|---|---|---|
| Master Router (n8n) | Visual, easy to debug, all logic in one place | IF nodes can be verbose for many routes |
| External Router Service | Code-based routing (if/switch), centralized | Additional infrastructure, another service to manage |
| Long Polling (no webhooks) | No webhook limitation | Higher latency, less efficient, harder to scale |
| Multiple Bots | Each bot has own webhook | Confusing UX (which bot to message?), more API keys |
Chosen: Master Router in n8n Why: Visual debugging, no additional infrastructure, sufficient for 12+ routes
What’s NOT included (proprietary):
What IS demonstrated:
state-management.js for handling stateful conversationserror-handling.js for retry logic and alertsThis is a portfolio demonstration. The concept applies to any platform with single webhook limitations (Telegram, Slack, Discord). The pattern is reusable across different automation platforms (n8n, Zapier, custom code).