- Jh123x: Blog, Code, Fun and everything in between./
- My Blog Posts and Stories/
- What I Learnt Revamping the Telegram Bot on a Browser/
What I Learnt Revamping the Telegram Bot on a Browser
Table of Contents
Introduction #
While I was working on my Telegram bot, I realised that it was getting hard to extend. The bot lives entirely in the browser without a backend. Web Workers talk to the Telegram Bot API directly, and users build reply rules with a drag-and-drop editor.
Before the revamp, the app was a command-based interface built on Create React App. Users could add simple command-response pairs in a table, send custom messages, and watch incoming logs. There were no user rules yet; the bot could only reply with fixed responses based on a predetermined message.

Last month, I decided to revamp it. The result is a graph-based flow editor built on React Flow. Each flow is a visual graph:
- A Start node where Telegram messages come in.
- Transform and condition nodes in the middle.
- Send nodes that send messages/polls.
Messages travel along the edges, and condition nodes branch them with explicit if/else paths.

In this blog post, I will share what I learnt during the process. It covers three things:
- The considerations behind choosing the flow implementation over the alternate block implementation
- The technical lessons from the revamp
- How I worked with an AI agent to make this happen
The Revamp #
The core was to give users the ability to define their own reply rules.
To test out which implementation worked better, I built two implementations of that feature.
- A block-based version
- A flow-based version
In the end, I went with the flow version, and the alternate block-based implementation was later removed.
Flow vs Block: The Decision #
The reason I could afford to try both is that AI agents made prototyping cheap. Instead of going all in on a single design, I could sketch both and see which is better.
Having both implementations to play around with visually cemented my decision about which one to go with.

The Three Versions #
The table below compares the features of the old command-based app and the two implementations.
| Dimension | Old | Blocks | Flows |
|---|---|---|---|
| Programming model | Command-response pairs in a table | Linear list of blocks | Visual graph of nodes and edges |
| Branching | No branches, fixed replies only | Single fallback per block | Explicit if/else edges |
| Rule types | Command + response | Generic blocks with type selectors | Concrete types (lowercase, equals, send, etc) |
| Runtime state | N/A | Per-user state | Stateless, new messages re-run from start |
| Editing while running | N/A | Needs state reset | No reset needed |
| Learning curve | Simplest to start | Simple to start, confusing to scale | Slightly steeper to start, scales better |
Pros and Cons #
It is immediately clear that both new implementations allow the user to do much more than the fixed message response pattern from before. However, it is not clear which of the two implementations is better.
After using each implementation in the prototyping phase, the trade-offs became clear.
| Aspect | Blocks | Flows |
|---|---|---|
| Pros | Simple mental model for one or two blocksFamiliar card-based editorMore mobile-friendly | Branches are explicit and visibleConcrete node types simplify the UI and validationStateless runtime is easier to test and persistNew node types slot in cleanlyFlows are easier to trace |
| Cons | Branching beyond if/else is painfulType selectors add complexityPer-user state makes live edits awkwardHard to see the whole path | Graph editors are more complex to buildSteeper learning curve for new usersHarder for mobile users to edit |
Final Decision #
The app targets people who use Telegram but can’t host a server, and who want to try building a bot without programming.
Based on the target audience defined above, I determined that a graph-based editor would work better. It is easier for them to trace the logic of the flow. A flow shows the whole program at a glance.
I even tried creating some sample programs, and the block editor’s limitations kept getting in the way — which confirmed the decision.
Current Architecture #
Before looking at how messages are processed, it helps to see the shape of the app as it stands today. The whole bot still runs in the browser with no backend.
The app has five moving parts inside the browser:
- React UI: the Flow editor and the Chat page. Components are thin; the complex pieces are split into small files.
- Redux Store: holds the bot token, the flows, and the message history. The token, flows, and a few settings persist to localStorage.
- Logic Engine: a pure layer with no React or Redux dependencies.
FlowRuntimewalks a flow from its start node and returns the replies. - BrowserBot: the transport layer. It wraps the Telegram Bot API and runs a Bot Worker so polling and sending never block the UI.
- localStorage: keeps the token and flows across reloads.
Outside the browser, the app talks to the Telegram Server, which delivers messages to and from Telegram users. The next section shows one full message cycle in detail.
How Messages Are Processed #
Before diving into the lessons, it helps to see how a message flows through the app.
The bot polls the Telegram Server for new messages.
A Bot Worker calls getUpdates in a loop at a fixed interval, hands each new message to the bot logic, and sends the replies back.
The diagram below shows one full polling cycle.
The interesting part is the Flow Runtime. It walks the graph from the start node, applying transforms and evaluating conditions along the way. When a condition matches, it follows the if edge; otherwise, it follows the else edge. If a send node is reached, the bot replies. If no send node is reached, the flow declines and the next rule gets a chance.
Lessons Learnt #
Engineering Trade-offs #
State Placement Matters #
The selected chat used to live in local component state. The problem was that switching tabs unmounted the page, so the selection was forgotten. I moved it into the Redux store, and now it survives tab switches. The rule of thumb I took away: if state must survive navigation, put it in the store.
The Node Type Is the Operation #
The first flow design used generic nodes with a type selector. I refactored it to concrete node types: lowercase, equals, send, random, poll. Node data became flat: label, value, find, replacement, pattern, replies. There are no wrapper objects and no per-node type selectors. This makes validation simpler and the UI simpler. One function, nodeCategory, drives palette grouping, validation, and runtime dispatch.
Keep the Runtime Stateless #
FlowRuntime holds no per-user state. Editing a flow needs no state reset; every message re-runs the flow from the start node. When a flow declines to respond, the bot falls through to the next rule. This is what makes multi-flow work. Stateless runtimes are easier to test, reason about, and persist.
Agent-Assisted Development #
This revamp was not a solo effort. An AI agent (Hermes) did most of the coding with me. Here is how we worked together.
- Feature work happened on a separate branch (
feat/...) - We discussed the design first, then the agent proposed a plan under
docs/plans/ - Implementation followed strict TDD: failing test first, then code
Subagents did the coding, each handling one concern. The main agent spawns a review subagent to review every subagent’s output before it moves on, and an extra review subagent checks the whole branch at the end. The full test suite and build had to pass before we pushed.
The agent also keeps a project skill file that records conventions, decisions, and pitfalls; every session starts by loading it. This is the closest thing the agent has to long-term memory, and it worked well.
A Typical Feature #
A typical feature follows the loop in the diagram below. The subagent keeps implementing until the tests turn green, and the review and build steps each feed back into the loop until they pass.
The palette revamp went through this loop. So did the flow editor and the poll node.
On Process #
The most important input turned out to be a clear spec. Vague requests produce vague code. TDD was not optional with agents, because it caught regressions the agent would otherwise introduce. Incremental commits kept review and rollback cheap. I also learnt to ask the main agent to spawn review agents to check claims, since a subagent that says “tests pass” may have missed some edge case.
On Delegation #
Review subagents caught what tests missed, such as stale validators, stale docs, and model drift. Subagents also hit iteration limits on granular edits, so the rule became: give them one file, or one concern. For bulk mechanical edits, a script beat a subagent with many tiny patches.
On Memory #
Updating the project skill after each feature is how the agent learns my codebase. The agent also remembered my design preferences, so the UI stayed consistent without me repeating them.
Quirks and Bugs #
These are the bugs and quirks I ran into during the revamp. Each one cost me time and left a lesson behind.
React Flow v12 Has Traps #
A few specific gotchas came up during the build:
- onDrop/onDragOver are not wired internally. You implement them yourself.
- A controlled editor can create an infinite dimensions loop that renders nodes invisible. The fix is to drop dimensions changes before applying node changes, and early-return when nothing meaningful remains.
- Multiple handles need explicit ids (if/else), or condition branches do not route.
- ResizeObserver loop errors are benign. Do not chase them.
The main lesson: a graph library has opinions; learn them before you build on top of it.
Tests Must Simulate Real Transitions #
Preloaded fixtures hid a stale-bot-instance bug. The fix was to mount with an empty token, then dispatch setToken / setHydrated in the test. If a test never exercises the real transition, it cannot catch transition bugs.
The main lesson: test the path your app actually takes, not just the happy path.
Validators Drift Silently #
Settings export/import validators checked the old node model. After the refactor, importing a valid flow failed with “invalid file”. All tests stayed green because the validators were only tested with old fixtures.
The main lesson: keep validators in the same change as the model, and test round-trips.
Docs Drift Too #
After removing the program system, docs/ still referenced it.
A review subagent caught it.
This is why my main agent now includes docs in the completeness pass and greps for deleted symbols across src/, docs/ and tests before calling it done.
The main lesson: docs are part of the codebase, include them in the completeness pass.
Conclusion #
The revamp made the app simpler and more powerful at the same time. The biggest win was choosing the flow implementation and removing the alternate block implementation. If you are revamping your own app, steal this: when a feature can be built multiple ways, prototype the alternatives cheaply, compare them head-to-head, and keep the one that scales.
Working with an AI agent was the second big lesson. The workflow (plan, TDD, subagents, review, incremental commits) did not just produce the app. It produced a codebase where every new feature starts from a clean base, and a skill file that remembers the pitfalls for next time.
The bot is still evolving, so stay tuned for more posts on this project.