---
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