Answers customer questions in Telegram from your own business notes, using a free local Ollama model or OpenAI or Anthropic with your own key. It remembers the conversation, so follow-up questions make sense. If it can’t answer, the customer gets your own fallback message instead of a made-up answer.
Setup, about ten minutes:
- Message @BotFather in Telegram, send /newbot, and copy the token.
- Import the workflow.
- Open Send the answer, create a Telegram credential with your token, then select the same credential on Say you will come back to them.
- Open the Settings node and fill in ai_provider (ollama, openai or anthropic), ai_model, business_name, and knowledge: your facts, one per line. For OpenAI or Anthropic, add your API key as a Header Auth credential on the Ask the AI node. It stays encrypted in n8n and never travels with the exported workflow.
- Activate the workflow and message your bot.
Text messages only. This lite version has no human handoff. The full version adds handoff to a person, operator alerts, forwarding while a person is on the chat, and /resume to hand the chat back: Telegram AI Bot with Human Handoff — n8n template | Whop
1 curtida
Nice pattern. The part I’d be most interested in is how the handoff state is stored per chat.
I’d use chat_id as the key with fields such as mode (AI or human), operator, started_at, and expires_at, then have both the AI branch and /resume branch update that same record atomically. That prevents a late AI execution from replying after the human has taken over.
An idempotency key based on Telegram’s update_id would also help with retries. Did you add a guard for simultaneous messages during the switch?
Good point about the handoff state. One thing I’d be worried about as well is message ordering during the switch. If a user sends 2-3 messages quickly while the chat is moving from AI mode to human mode, it’s easy for an older execution to respond after the state has already changed.
I’d be interested to know whether the handoff check happens only at the start of processing or again immediately before sending a reply. That extra check has saved me from a few awkward double-response situations.
Thanks, this is a good catch and you found a real gap.
Right now the state lives in n8n’s workflow static data, keyed by chat id, with turns, handed_off and handed_off_at. The flag is read at the start of each run and written at the end.
So your race is real. If two messages land close together during the switch, both runs can read handed_off as false, and the second one can reply after the chat was already handed to a person. There’s also no update_id dedupe yet, so a Telegram retry could run twice.
Static data can’t fully fix that on its own, since it’s read per execution and written at the end with no compare-and-set. The real fixes are an external store like Redis or a Postgres row, or limiting it to one execution per chat.
For a single shop’s traffic it was a deliberate trade, but a trade is still a trade. Next version adds update_id dedupe and a re-check right before sending.
Good question. Right now the check only happens at the start of the run. There’s no second read right before sending, so an older execution can still reply after the state has changed, exactly like you described.
Your re-check before send is the cheapest fix, since it closes most of that window without adding an external store. I’m adding it in the next version, along with update_id dedupe.
Thanks for sharing what’s worked for you.
That sounds like the right next increment for a single-shop workflow.
I’d make the final check fail closed: if the handoff state cannot be read, or it changed while the AI was generating its response, suppress the reply and log why it was discarded.
A simple state_version could help with this. Capture its value when the execution starts, then compare it again immediately before sending. If the version or handed_off value changed, the execution is stale and should stop.
It still isn’t a fully atomic solution with static data, but it makes stale responses visible and testable. Later, that same version field can become a proper compare-and-set condition when the state moves to Postgres or Redis.
I’d also put the update_id uniqueness check at the very beginning so duplicate Telegram deliveries never reach the AI call.
That’s a clean design, thanks. I’m taking all three for v2:
- update_id check at the very start, so duplicate deliveries never reach the AI call.
- A state_version captured when the run starts and compared again right before sending. If it or handed_off changed, the run is stale and stops.
- Fail closed: if the state can’t be read or changed mid-run, the reply is suppressed and the reason is logged.
Agreed it’s not truly atomic on static data, but making stale replies visible and testable is the right step, and the version field carries over cleanly to compare-and-set later. I’ll post here when v2 is up.
Great thread. Mukarram Ali and Ayub Shah have already covered the race and the fixes (pre-send re-check, update_id dedupe, fail closed, state_version), so I won’t repeat those. One thing I’d add is about the handoff itself, from the process side.
A handoff isn’t a flag on the chat. It’s the conversation changing owners, so I’d model it as a small state machine instead of a boolean. Each change is a step with a name, a person and a time:
AI Handling → Handoff Requested → Human Handling → Handback Pending → AI Handling, with Unanswered as a side state.
The useful part is that the states decide what each actor is allowed to do, so you’re not stacking more checks on top of a flag:
- The race fix becomes a rule. “Bot can send” is only valid while the chat is in
AI Handling. The pre-send re-check just asks “is this transition still allowed?”, and once a human has taken over the answer is no, whatever the timing.
- What happens when nobody picks up? If a chat sits in
Handoff Requested for 10 minutes with no operator, it moves to Unanswered and someone gets pinged. Today the customer just waits.
- Handback is a human decision. Only the operator can move the chat from
Human Handling to Handback Pending. The bot doesn’t resume on a timer, so it never jumps into a conversation a person is still handling.
- “Why did the bot reply?” has an answer. Every transition is logged with who and when, so when a customer complains you can see who owned the chat at that moment.
- Mukarram’s state_version falls out naturally. It’s just the count of transitions.
This is also why the bot shouldn’t own the state. n8n runs the replies, but the conversation’s state should live somewhere the bot, the operator and the timeout job all read from and change through the same transitions.
For the paid version, that transition history is probably also the first thing support managers will ask for.
This is a better model than the one I’m patching, thanks. You’re right that the flag is the problem: every fix so far has been another check stacked on top of it.
I’m moving v2 to the state machine you laid out: AI Handling, Handoff Requested, Human Handling, Handback Pending, back to AI Handling, with Unanswered as the side state. “Bot can send” only while in AI Handling, so the pre-send re-check becomes “is this transition still allowed”, and state_version is just the transition count, like you said.
The Unanswered timeout is the part I hadn’t thought about at all. Today a customer can sit there forever if nobody picks up, so v2 gets the 10 minute move to Unanswered plus an operator ping.
Handback is already human-only (the operator sends /resume), and v2 will log every transition with who and when.
Agreed the state shouldn’t live in the bot long term. For the single-shop version it stays in n8n with the same transitions, so moving it to Postgres or Redis later is a storage swap, not a redesign. And good call on transition history for support managers. That’s going in the paid version.
Appreciate it. I’ll post v2 here with all three of you credited.
1 curtida