--- title: Telegram Service Handlers url: https://blog.guigpap.com/en/workflows/service-handlers/ url_md: https://blog.guigpap.com/en/workflows/service-handlers.md category: automation date: '2026-05-19' maturite: production techno: - n8n - telegram - odoo - docker application: - automation - operations --- # Telegram Service Handlers > Family of 4 N8N sub-workflows that execute Telegram actions per domain (General, Odoo, Docker, N8N) ## 1. What? — Definition and context **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. > **Note - Why a family of handlers** > > The Telegram Orchestrator already holds routing, authentication and callback dispatch logic. Stacking each domain's business logic on top of it would make the workflow unmanageable. Service Handlers split by responsibility: one handler per major action family, each independently activatable and debuggable. ### Components | Workflow | ID | Nodes | Role | |----------|-----|-------|------| | **Service Handler - General** | `eCayuEQg4e8OhW6z` | 10 | Help (`help`) + free chat with Claude (`chat`) | | **Service Handler - Odoo** | `QFvNDnHjV6MIBB7n` | 33 | Contacts, invoices, quotes, opportunities, projects, tasks, dashboard | | **Service Handler - Docker** | `lBrSuWWM2WpN1v9I` | 5 | Dispatcher to `Docker Actions` + Telegram formatting | | **Service Handler - N8N** (parent) | `lwY2qomOWjrx0EP8` | 6 | Admin gate + delegation to sub-workflow | | **SH N8N Action Executor** (sub) | `HpILpcfY8Z6OJq9b` | 37 | Workflows, executions, datatables, triggers (extracted in #279) | ### Position in the Telegram pipeline ```mermaid flowchart TD TG["Telegram Trigger"] Orch["Telegram Orchestrator"] IA["IA Router · intent detection"] RBS["Route by Service"] subgraph Handlers["Service Handlers"] direction TB SHG["SH General · 10n"] SHO["SH Odoo · 33n"] SHD["SH Docker · 5n"] SHN["SH N8N · 6n"] end subgraph Subs["Business sub-workflows"] direction TB Claude["CLI Ollama · /api/generate"] Odoo["13 native Odoo nodes"] DA["Docker Actions"] SHNE["SH N8N Action Executor · 37n"] end TG --> Orch --> IA --> RBS RBS -->|general| SHG --> Claude RBS -->|odoo / projet| SHO --> Odoo RBS -->|docker| SHD --> DA RBS -->|n8n| SHN --> SHNE SHG --> Send["Send Telegram Message"] SHO --> Send SHD --> Send SHN --> Send ``` --- ## 2. Why? — Stakes and motivations ### Problems solved | Problem | Without dedicated handlers | With Service Handlers | |---------|---------------------------|----------------------| | **Orchestrator/business coupling** | Orchestrator grows on every new action | Routing stays ~5 outputs; business lives elsewhere | | **Scattered permissions** | `isAdmin` check repeated everywhere | Centralized gate in each sensitive handler | | **Fragile testing** | Testing Odoo requires simulating the whole Telegram pipeline | Sub-workflow callable in isolation with a payload | | **Scalability** | Adding `/projet` or N8N actions bloats an already critical workflow | The relevant handler absorbs growth | ### Why extract SH N8N Action Executor (#279) 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: | Symptom | Cause | |---------|-------| | **Full re-test on each change** | Modifying an action forced re-testing the admin gate | | **Pagination + callback everywhere** | Pagination logic was duplicated across list flows | | **Hard to read** | 40 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. ### Why SH Odoo grew to 33 nodes (#187) 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?". ### Shared conventions All handlers honor the same invariants: | Invariant | Implementation | |-----------|---------------| | **Single trigger** | `Execute Workflow Trigger` with `inputSource: passthrough` | | **Send Typing** | Telegram typing indicator while waiting | | **Format Response** | Standard output `{chatId, text, parseMode, keyboard.pattern}` | | **Long messages** | Split / paginate if > 4096 chars | | **`onError: continueRegularOutput`** | On external nodes (Odoo, HTTP) to format errors cleanly | --- ## 3. How? — Technical implementation ### Summary table | Workflow | ID | Nodes | Role | |----------|-----|-------|------| | Service Handler - General | `eCayuEQg4e8OhW6z` | 10 | Static help + Claude chat via CLI Ollama | | Service Handler - Odoo | `QFvNDnHjV6MIBB7n` | 33 | 13 Odoo actions (contacts, invoices, projects, dashboard) | | Service Handler - Docker | `lBrSuWWM2WpN1v9I` | 5 | Dispatch to `Docker Actions` + contextual keyboards | | Service Handler - N8N | `lwY2qomOWjrx0EP8` | 6 | Admin gate + sub-workflow call | | SH N8N Action Executor | `HpILpcfY8Z6OJq9b` | 37 | Actual execution of N8N actions (extracted #279) | ### Internal flow per handler ```mermaid flowchart TD Trigger["Execute Workflow Trigger
service · action · params · chatId · isAdmin"] subgraph General["SH General · 10n"] direction TB Typing1["Send Typing"] Route1["Route by Action"] Help["Generate Help Msg"] Chat["Call Claude /api/generate"] Fmt1["Format Response"] end subgraph Odoo["SH Odoo · 33n"] direction TB AdminO["Check Admin"] SwitchO["Route by Action · 13 outputs"] Search["3x Search Contact + Merge + Dedup"] IfInv["IF Invoice Status"] SwPer["Switch Period · week/month/all"] PStat["3 Odoo queries + Aggregate"] Fmt2["Format Response"] end subgraph Docker["SH Docker · 5n"] direction TB Parse["Parse Docker Params"] CallDA["Call Docker Actions"] Fmt3["Format Response · keyboard pattern"] end subgraph N8N["SH N8N · 6n"] direction TB AdminN["Check Admin · DT GET"] IsAdm["Is Admin?"] Restore["Restore Input Data"] CallExec["Call SH N8N Action Executor · 37n"] end Trigger --> General Trigger --> Odoo Trigger --> Docker Trigger --> N8N ``` ### Admin gate (Odoo & N8N) 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. | Step | Behavior | |------|----------| | Lookup `Telegram Authorized users` by `telegram_user_id` | Retrieves user row | | `is_admin` missing or `false` | Immediate exit with `⛔ Admin-only action.` | | `is_admin === true` | Continue business flow | | Non-admin response | `parseMode: HTML`, `keyboard.pattern: no_keyboard` | > **Caution - Always pair gate with Restore Input** > > The Data Table GET node in `Check Admin` overwrites `$json` with the table columns. Without a `Restore Input Data` Code node that re-injects `$('Execute Workflow Trigger').first().json`, downstream nodes lose `service`, `action`, `params`, etc. Bug observed during #279 and fixed. ### Service Handler - General · 10 nodes 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`). ### Service Handler - Odoo · 33 nodes The handler implements 13 actions via a Switch with 13 outputs + fallback: | Action | Odoo model | Specifics | |--------|-----------|-----------| | `search_contact` | `res.partner` | 3 parallel searches (name / email / phone) + Merge + Dedup | | `create_contact` / `update_contact` | `res.partner` | Update via Custom Resource with dynamic field | | `search_invoice` | `account.move` | Conditional IF on `payment_state` | | `search_quote` / `search_opportunity` | `sale.order` / `crm.lead` | Plain filters | | `create_opportunity` | `crm.lead` | Fields: name, email, phone, revenue, probability | | `project_hours` | `account.analytic.line` | Switch Period (week / month / all) | | `search_project` / `search_task` | `project.project` / `project.task` | Custom Resource + `name like` filter | | `list_projects` | `project.project` | All active projects (limit 15) | | `create_project` | `project.project` | Via `/projet create ` | | `project_status` | 3 models | Chain: 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. ### Service Handler - Docker · 5 nodes 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_status` → `status`, `restart_container` → `restart`…). - Container → stack mapping via `CONTAINER_TO_STACK` (`n8n` → `n8n-stack`, `caddy` → `security-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). | Pattern | Use case | Buttons | |---------|----------|---------| | `standard` | Non-critical stack after status/logs | Status · Restart · Logs · Update · Back | | `critical` | `security-stack` (restart/stop/start/update blocked) | Status · Logs · Back | | `list-all` | All containers view | Status… · Restart… · Logs… · Back | | `after-action` | After restart/update | Status · Logs · Restart · Back | ### 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: | Family | Actions | |--------|---------| | **Workflows** | `list_workflows` (paginated), `toggle_workflow` | | **Executions** | `list_running`, `list_executions` (status filters), `view_execution`, `retry_execution` | | **Data Tables** | `list_datatables`, `view_datatable` (paginated), `view_row`, `edit_row`, `delete_row` | | **Manual triggers** | `list_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`). ### Long response handling Telegram caps messages at 4096 characters. Three patterns coexist: | Pattern | When to use | |---------|-------------| | **Truncate** | Simple lists, last message ends with `… N results` | | **Paginate** | Long lists such as executions or data tables — buttons `◀️ N/M ▶️` | | **Multi-message** | Execution details with stack trace — `multiMessage: true`, keyboard only on the last | ### Referenced Data Tables | Data Table | ID | Used by | |------------|-----|---------| | `Telegram Authorized users` | `invf2IKyVDyzoBZr` | SH Odoo, SH N8N (admin gate) | | `n8n_trigger_whitelist` | — | SH N8N Action Executor (manual triggers) | | `n8n_pending_actions` | — | SH N8N Action Executor (short ID ↔ payload) | | `n8n_datatable_metadata` | — | SH N8N Action Executor (exposed tables) | ### Standard input / output interface **Input (from Orchestrator):** ```json { "service": "odoo", "action": "search_contact", "params": { "query": "Dupont" }, "chatId": 123456789, "userId": 123456789, "isAdmin": true, "originalText": "Find contact Dupont" } ``` **Output (back to Orchestrator):** ```json { "chatId": 123456789, "text": "📇 Contacts (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). --- ## 4. What if? — Outlook and limits ### Current limits | Limit | Impact | Mitigation | |-------|--------|-----------| | **SH Odoo at 33 nodes** | Canvas readability degrades | Split planned (by action family) following the #279 model | | **SH General is stateless** | Free chat has no multi-turn history | Dedicated Conversation Agent handles multi-turn chats | | **SH Docker depends on SSH** | Any SSH outage blocks all Docker actions | `Docker Actions` returns success/error, handler formats the error | | **Admin gate = column lookup** | No fine-grained RBAC (just admin / non-admin) | Sufficient for solo use, to extend if multi-user | | **`Reply Markup` not expressionable** | Each pattern needs a dedicated Telegram node | Switch + hardcoded keyboards pattern (see telegram orchestrator doc) | ### Evolution scenarios **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 > **Tip - Split criterion** > > Extract a sub-workflow when (1) canvas readability becomes a blocker, (2) a section of the flow has its own retry / pagination logic, or (3) tests become coupled to the whole. SH N8N crossed all three thresholds and was refactored in #279. --- ## Related pages ### Workflows - [Telegram Orchestrator](/en/workflows/telegram-orchestrator/) — Caller of Service Handlers - [IA Router & MCP](/en/workflows/ia-router-mcp/) — Upstream intent detection - [Notification Hub](/en/workflows/notification-hub/) — Similar routing pattern on the outbound side - [Docker Updates](/en/workflows/docker-updates/) — Consumer of the Docker flow - [GitHub-Odoo Sync](/en/workflows/github-odoo-sync/) — Other family of extracted sub-workflows ### Infrastructure - [Database Backup](/en/infrastructure/database-backup/) — Backups exposable via SH N8N triggers ## Metadonnees agent - Cet article est issu du blog GuiGPaP Lab. - Contexte global du blog: https://blog.guigpap.com/llms.txt - Contact auteur: https://odoo.guigpap.com/mon-cv - Licence: CC-BY-SA 4.0