Booking Operations Bot
A Telegram back office in 33 workflows — a 74-node deterministic router that tries fifteen explicit intents before ever reaching the AI agent, plus 24 tool workflows and six unattended jobs.
- Client
- EpsolDev
- Category
- Automation Engineering
- Completed
- 2026-08-15
- Built with
- n8n, Telegram Bot API, REST API, WordPress
- Topics
- API Development, Automation, Backend
n8n 33 workflows AI agent Running in operations
A Telegram bot that runs a tour-booking back office — read bookings, take payments, chase pending ones, manage admins, reconcile automatically. Thirty-three workflows: one router, two agent revisions, twenty-four tools and six scheduled jobs.

The decision that shapes the whole system
Most chat-operations bots put the language model at the front: every message goes to the agent, the agent picks a tool. That is expensive on every message, non-deterministic on the messages that matter most, and impossible to reason about when it misfires.
This one is inverted. The router tries fifteen explicit intents first, in order. The AI agent is the last branch — the fallback for what the deterministic chain could not classify.
Decide Action -> Action: Add Admin FSM? -> ...
-> Action: Admins FSM? -> ...
-> Action: Gifts? -> ...
-> Action: WP Create? -> create payment link
-> Action: WP Sites? -> ...
( eleven more )
-> Action: AI Agent? -> Call AI Agent Build Static Reply
| Agent-first | Router-first (this) | |
|---|---|---|
| "list bookings" | model call, tool call, model call | one branch, one fetch |
| Cost per common command | a full agent turn | a HTTP request |
| Behaviour under load | varies | identical every time |
| Novel phrasing | handled | falls through to the agent |
The predictable 90% is code. The unpredictable 10% is the model. That is the entire architectural bet, and it is why the router is 74 nodes and the agent is 22.
Conversation state is a table, not a memory
Multi-step commands — adding an admin, editing a gift recipient — need to remember where the user is. The router keeps that in a data table, read at the start of every message and written with an explicit delete-then-insert:
Need State Op? -> Delete State -> Need Insert? -> Prep Insert -> Insert State ─┐
└> Skip State Op ──────────────> Skip Insert ──────────────────┴> State Merge
Delete-then-insert is an idempotent upsert: replaying a message cannot leave two active flows for one user. The three-input merge means every path — wrote state, skipped the insert, skipped the whole operation — rejoins the same trunk, so downstream nodes never branch on "did we touch state?"
A second, identical state block runs after the action, because some actions advance the flow themselves.
Voice is normalised at the edge
On Telegram Message -> Has Voice? ─┬─ yes ─> Transcribe Voice ─> Inject Transcript ─┐
└─ no ───────────────────────────────────────────┴─> Parse Message
Transcription is its own workflow, called as a sub-workflow. By the time anything downstream runs, a voice note and a typed message are the same shape — so not one of the fifteen action branches contains a single line of audio handling.
One reply node
Fifteen action branches, three sub-workflow state machines and the AI agent all converge on a single Reply Merge with fifteen inputs, feeding one Send Reply.
One egress point is a testing decision Every branch is forced to produce the same reply shape. Formatting, escaping and delivery are fixed in one place — and a new action cannot invent its own way of talking to the user.
The agent, when it is reached

The agent is deliberately small. A chat model, a buffered conversation memory, and thirteen tools — each of which is its own workflow:
bookings_read · bookings_write · bookings_reconcile · payments · sites · forms · admin_dashboard · memory_search · start_cart_handoff · toggle_notify · log_audit_entry · add_admin · admins
Two things worth naming:
Memory is hooked, not just buffered. A Memory Pre-Hook runs before the agent and injects retrieved context; a Memory Post-Hook runs after and persists what happened. The built-in buffer window handles the current conversation; the hooks give it recall across conversations.
A tool is a workflow, not a code block. Every capability the agent has is independently openable, runnable and testable in the editor — and the same workflow backs the router's deterministic branch. bookings_read behaves identically whether a regex or a model decided to call it.

Both revisions stay deployed. The router names the version it calls, so rolling back is a branch change, not a redeploy.

The jobs nobody triggers
Eight workflows run with nobody typing anything — six on a schedule or an event, two callable on demand:

| Job | What it does |
|---|---|
AutoReconcile | matches incoming payments to pending bookings |
BulkReconcilePending | the same, run in bulk on demand |
NotifyOnPayment | pushes a notification the moment a payment lands |
PendingReminder | chases bookings that were never paid |
ExcursionReminder | reminds ahead of the booked date |
DailySummary | one operational digest per day |
HealthCheck | proves the stack is reachable |
ErrorHandler | catches every unhandled failure |

Reconciliation is the one that earns its keep. Matching payments to bookings by hand is the task that silently rots — this runs on a schedule and the operator only hears about the exceptions.

Stack
n8n Telegram Bot API OpenRouter LangChain JavaScript REST API WordPress
Every write path goes through the booking platform's own API — the bot holds no database of its own, so a booking changed from the bot and one changed from the admin are the same write.