Skip to content

Telegram Service Handlers

Service Handlers are a family of 4 N8N sub-workflows invoked by the Telegram Orchestrator once a user intent has been detected. Each handler encapsulates a functional domain (general chat, Odoo, Docker, N8N admin) and exposes a stable interface to the Orchestrator: it receives {service, action, params, chatId, userId, isAdmin} and returns {chatId, text, parseMode, keyboard} ready to send back to Telegram.

WorkflowIDNodesRole
Service Handler - GeneraleCayuEQg4e8OhW6z10Help (help) + free chat with Claude (chat)
Service Handler - OdooQFvNDnHjV6MIBB7n33Contacts, invoices, quotes, opportunities, projects, tasks, dashboard
Service Handler - DockerlBrSuWWM2WpN1v9I5Dispatcher to Docker Actions + Telegram formatting
Service Handler - N8N (parent)lwY2qomOWjrx0EP86Admin gate + delegation to sub-workflow
SH N8N Action Executor (sub)HpILpcfY8Z6OJq9b37Workflows, executions, datatables, triggers (extracted in #279)

Business sub-workflows

Service Handlers

general

odoo / projet

docker

n8n

Telegram Trigger

Telegram Orchestrator

IA Router · intent detection

Route by Service

SH General · 10n

SH Odoo · 33n

SH Docker · 5n

SH N8N · 6n

CLI Ollama · /api/generate

13 native Odoo nodes

Docker Actions

SH N8N Action Executor · 37n

Send Telegram Message


ProblemWithout dedicated handlersWith Service Handlers
Orchestrator/business couplingOrchestrator grows on every new actionRouting stays ~5 outputs; business lives elsewhere
Scattered permissionsisAdmin check repeated everywhereCentralized gate in each sensitive handler
Fragile testingTesting Odoo requires simulating the whole Telegram pipelineSub-workflow callable in isolation with a payload
ScalabilityAdding /projet or N8N actions bloats an already critical workflowThe relevant handler absorbs growth

The Service Handler - N8N initially ran at 40 nodes: admin gate + 15 actions (list_workflows, toggle, executions, datatables, triggers…). Three symptoms triggered the refactoring into a thin parent + heavy sub-workflow:

SymptomCause
Full re-test on each changeModifying an action forced re-testing the admin gate
Pagination + callback everywherePagination logic was duplicated across list flows
Hard to read40 nodes in one canvas hid the action Switch

The split produces 6 parent nodes (Trigger → Check Admin → Is Admin? → Restore Input → Execute Sub → Return) and 37 nodes inside SH N8N Action Executor orchestrating the 15 N8N actions.

Adding /projet commands pushed SH Odoo from 27 to 33 nodes: 3 new Switch outputs (list_projects, create_project, project_status), 5 additional native Odoo nodes, and a Code aggregator for the project dashboard. This is the upper bound before a split similar to #279 becomes necessary; see “What if?”.

All handlers honor the same invariants:

InvariantImplementation
Single triggerExecute Workflow Trigger with inputSource: passthrough
Send TypingTelegram typing indicator while waiting
Format ResponseStandard output {chatId, text, parseMode, keyboard.pattern}
Long messagesSplit / paginate if > 4096 chars
onError: continueRegularOutputOn external nodes (Odoo, HTTP) to format errors cleanly

WorkflowIDNodesRole
Service Handler - GeneraleCayuEQg4e8OhW6z10Static help + Claude chat via CLI Ollama
Service Handler - OdooQFvNDnHjV6MIBB7n3313 Odoo actions (contacts, invoices, projects, dashboard)
Service Handler - DockerlBrSuWWM2WpN1v9I5Dispatch to Docker Actions + contextual keyboards
Service Handler - N8NlwY2qomOWjrx0EP86Admin gate + sub-workflow call
SH N8N Action ExecutorHpILpcfY8Z6OJq9b37Actual execution of N8N actions (extracted #279)

SH N8N · 6n

Check Admin · DT GET

Is Admin?

Restore Input Data

Call SH N8N Action Executor · 37n

SH Docker · 5n

Parse Docker Params

Call Docker Actions

Format Response · keyboard pattern

SH Odoo · 33n

Check Admin

Route by Action · 13 outputs

3x Search Contact + Merge + Dedup

IF Invoice Status

Switch Period · week/month/all

3 Odoo queries + Aggregate

Format Response

SH General · 10n

Send Typing

Route by Action

Generate Help Msg

Call Claude /api/generate

Format Response

Execute Workflow Trigger

service · action · params · chatId · isAdmin

Odoo and N8N actions are sensitive (data mutations, workflow execution). Both handlers query the same Telegram Authorized users Data Table (invf2IKyVDyzoBZr) and short-circuit when the flag is missing.

StepBehavior
Lookup Telegram Authorized users by telegram_user_idRetrieves user row
is_admin missing or falseImmediate exit with ⛔ Admin-only action.
is_admin === trueContinue business flow
Non-admin responseparseMode: HTML, keyboard.pattern: no_keyboard

Two actions: help (static HTML response), and chat (fallback, calls CLI Ollama /api/generate with model: codex-yolo, generated session_id). The request body is serialized via JSON.stringify to escape quotes and newlines — without this, text coming from Gemini Vision OCR with quotations broke the payload (fix d80e5ce).

The handler implements 13 actions via a Switch with 13 outputs + fallback:

ActionOdoo modelSpecifics
search_contactres.partner3 parallel searches (name / email / phone) + Merge + Dedup
create_contact / update_contactres.partnerUpdate via Custom Resource with dynamic field
search_invoiceaccount.moveConditional IF on payment_state
search_quote / search_opportunitysale.order / crm.leadPlain filters
create_opportunitycrm.leadFields: name, email, phone, revenue, probability
project_hoursaccount.analytic.lineSwitch Period (week / month / all)
search_project / search_taskproject.project / project.taskCustom Resource + name like filter
list_projectsproject.projectAll active projects (limit 15)
create_projectproject.projectVia /projet create <name>
project_status3 modelsChain: Get Project → Tasks → Timesheets → Aggregate Code

All Odoo nodes share the Odoo tool admin credential (xQmHuksl8E7nuwCV), onError: continueRegularOutput, and alwaysOutputData: true (top-level) so Format Response runs even on empty results.

The handler stays thin because the actual work is delegated to Docker Actions (SSH into the VPS, run docker compose). The Parse Docker Params node translates the IA Router intent into a (dockerAction, dockerStack) pair:

  • Passthrough if the action is already valid (status, restart, logs, update, start, stop, list-all).
  • Otherwise mapping through ACTION_MAP (container_statusstatus, restart_containerrestart…).
  • Container → stack mapping via CONTAINER_TO_STACK (n8nn8n-stack, caddysecurity-stack…).
  • Explicit error on unknown action (instead of a silent fallback on status).

Format Response returns keyboard.pattern among standard / critical / list-all / after-action. The Orchestrator then applies the right Switch on the sending side (per the “Reply Markup is not expressionable” rule).

PatternUse caseButtons
standardNon-critical stack after status/logsStatus · Restart · Logs · Update · Back
criticalsecurity-stack (restart/stop/start/update blocked)Status · Logs · Back
list-allAll containers viewStatus… · Restart… · Logs… · Back
after-actionAfter restart/updateStatus · Logs · Restart · Back

Service Handler - N8N · 6 nodes + sub-workflow 37 nodes

Section titled “Service Handler - N8N · 6 nodes + sub-workflow 37 nodes”

The parent only (1) gates admin via Data Table, (2) restores input, (3) delegates to SH N8N Action Executor via Execute Workflow. The sub-workflow exposes 15 actions:

FamilyActions
Workflowslist_workflows (paginated), toggle_workflow
Executionslist_running, list_executions (status filters), view_execution, retry_execution
Data Tableslist_datatables, view_datatable (paginated), view_row, edit_row, delete_row
Manual triggerslist_triggers, list_trigger_cat, confirm_trigger, execute_trigger

Long IDs (>64 bytes for Telegram callbacks) are stored in the n8n_pending_actions Data Table with an 8-char short ID and a 5-minute TTL. Manually triggerable workflows are declared in n8n_trigger_whitelist (categories backup / sync / cleanup / maintenance).

Telegram caps messages at 4096 characters. Three patterns coexist:

PatternWhen to use
TruncateSimple lists, last message ends with … N results
PaginateLong lists such as executions or data tables — buttons ◀️ N/M ▶️
Multi-messageExecution details with stack trace — multiMessage: true, keyboard only on the last
Data TableIDUsed by
Telegram Authorized usersinvf2IKyVDyzoBZrSH Odoo, SH N8N (admin gate)
n8n_trigger_whitelistSH N8N Action Executor (manual triggers)
n8n_pending_actionsSH N8N Action Executor (short ID ↔ payload)
n8n_datatable_metadataSH N8N Action Executor (exposed tables)

Input (from Orchestrator):

{
"service": "odoo",
"action": "search_contact",
"params": { "query": "Dupont" },
"chatId": 123456789,
"userId": 123456789,
"isAdmin": true,
"originalText": "Find contact Dupont"
}

Output (back to Orchestrator):

{
"chatId": 123456789,
"text": "📇 <b>Contacts</b> (2 results)...",
"parseMode": "HTML",
"keyboard": { "pattern": "odoo_contacts" }
}

The Orchestrator reads only text, parseMode, and keyboard.pattern to route to the right Telegram node (Reply Markup structure is hardcoded per pattern in the UI — N8N does not support expressions on keyboard structure, only on button values).


LimitImpactMitigation
SH Odoo at 33 nodesCanvas readability degradesSplit planned (by action family) following the #279 model
SH General is statelessFree chat has no multi-turn historyDedicated Conversation Agent handles multi-turn chats
SH Docker depends on SSHAny SSH outage blocks all Docker actionsDocker Actions returns success/error, handler formats the error
Admin gate = column lookupNo fine-grained RBAC (just admin / non-admin)Sufficient for solo use, to extend if multi-user
Reply Markup not expressionableEach pattern needs a dedicated Telegram nodeSwitch + hardcoded keyboards pattern (see telegram orchestrator doc)

If SH Odoo passes 40 nodes:

  • Split by family (SH Odoo Contacts, SH Odoo Projects, SH Odoo Sales) on the #279 model
  • Keep parent as admin gate + initial Switch dispatcher
  • Benefit: isolated testing, adding actions no longer touches the rest

If finer-grained permissions are needed:

  • Add columns to Telegram Authorized users (can_docker, can_odoo, can_n8n)
  • Replace binary is_admin check by a capability-based check in each handler
  • Keep backward compatibility: is_admin = true implies all capabilities

If Claude chat volume grows:

  • SH General remains single-turn (limited usefulness for ongoing conversation)
  • Route proactively to Conversation Agent beyond N messages in the same window
  • Session caching on CLI Ollama via Redis (already in place)

If a new domain (e.g. direct monitoring):

  • Create Service Handler - Monitoring (Prometheus queries, Grafana dashboards)
  • Add a monitoring output to the Orchestrator’s Route by Service
  • IA Router learns the new category through examples in its prompt