Skip to main content
  1. My Blog Posts and Stories/

What I Learnt Revamping the Telegram Bot on a Browser

··2108 words·10 mins

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.

Command-based interface with the command table and logs
The app before the revamp

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.

Flow editor with the node palette, canvas, and inspector
The flow editor after the revamp

In this blog post, I will share what I learnt during the process. It covers three things:

  1. The considerations behind choosing the flow implementation over the alternate block implementation
  2. The technical lessons from the revamp
  3. 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.

  1. A block-based version
  2. 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.

Block-based implementation with the palette, samples, and program cards
The alternate block-based implementation

The Three Versions #

The table below compares the features of the old command-based app and the two implementations.

DimensionOldBlocksFlows
Programming modelCommand-response pairs in a tableLinear list of blocksVisual graph of nodes and edges
BranchingNo branches, fixed replies onlySingle fallback per blockExplicit if/else edges
Rule typesCommand + responseGeneric blocks with type selectorsConcrete types (lowercase, equals, send, etc)
Runtime stateN/APer-user stateStateless, new messages re-run from start
Editing while runningN/ANeeds state resetNo reset needed
Learning curveSimplest to startSimple to start, confusing to scaleSlightly 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.

AspectBlocksFlows
ProsSimple mental model for one or two blocksFamiliar card-based editorMore mobile-friendlyBranches 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
ConsBranching beyond if/else is painfulType selectors add complexityPer-user state makes live edits awkwardHard to see the whole pathGraph 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.

Outside

Browser

getUpdates / sendMessage

user messages / replies

React UI
Flow Editor / Chat

Redux Store
token, flows, messages

Logic Engine
FlowRuntime

BrowserBot
Bot Worker

localStorage

Telegram Server

Telegram Users

The app has five moving parts inside the browser:

  1. React UI: the Flow editor and the Chat page. Components are thin; the complex pieces are split into small files.
  2. Redux Store: holds the bot token, the flows, and the message history. The token, flows, and a few settings persist to localStorage.
  3. Logic Engine: a pure layer with no React or Redux dependencies. FlowRuntime walks a flow from its start node and returns the replies.
  4. BrowserBot: the transport layer. It wraps the Telegram Bot API and runs a Bot Worker so polling and sending never block the UI.
  5. 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.

Flow RuntimeBrowserBotBot WorkerTelegram ServerFlow RuntimeBrowserBotBot WorkerTelegram Serverloop[Poll every poll interval]Telegram UsergetUpdatesnew messagesnew message eventhandleMessage (flow rules)executeFlow (graph walk)repliessendMessage(reply)sendMessage requestOKdelivers replyTelegram User

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.

matches

declines

matches

declines

Incoming message

Flow 1

Reply

Flow 2

Reply

Bot stays silent

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.

  1. Feature work happened on a separate branch (feat/...)
  2. We discussed the design first, then the agent proposed a plan under docs/plans/
  3. 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.

No, still RED

Yes

Yes

No

No

Yes

Describe feature

Agent proposes plan

Write failing test RED

Subagent implements

Tests GREEN?

Clean up REFACTOR

Review subagent

Issues found?

Full suite + build

Passes?

Push branch

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 #

These look like bugs, but they are React Flow v12 behaviour. Know the library before you fight it.

A few specific gotchas came up during the build:

  1. onDrop/onDragOver are not wired internally. You implement them yourself.
  2. 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.
  3. Multiple handles need explicit ids (if/else), or condition branches do not route.
  4. 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.

  1. Telegram Bot on a Browser
  2. telegram-bot-on-browser on GitHub
  3. React Flow