Skip to content

Migrating Off a No-Code Chatbot Builder to Custom Code

11 min read · updated August 11, 2026

A visual flow builder is an interpreter with a graphical editor. The migration is not a rewrite in a new language; it is writing down, in code, the interpreter’s semantics for the twelve node types you actually used — including the behaviours you never configured because the default was invisible.

What the canvas actually was

Strip away the drawing and a flow builder runtime does four things: it holds a mutable bag of variables for the conversation, it evaluates one node at a time against that bag, it decides which node comes next, and it persists the bag between turns. Every node type is a function from the bag to a new bag plus a next-node decision.

That framing is the whole migration, because it tells you what your replacement needs before you write a line: a typed context object, a dispatcher, an explicit control-flow structure, and a store. If you begin by writing an equivalent of the canvas — a generic node interpreter driven by the exported JSON — you will have rebuilt the platform, including its opacity. Do not. The value of moving to code is that the control flow becomes readable as code, so write it as functions and conditionals, not as data.

Node type to code pattern

The names differ per platform; the semantics cluster into a small set. For each one, the pattern that replaces it and the hidden contract to go and check in the live system before you switch it off.

  • Message / say node → a function returning a string or a structured message. Hidden contract: whether variable interpolation into the message escapes anything, and what it renders when the variable is unset.
  • Prompt / LLM node → one call, with the model configuration written out explicitly rather than inherited. Hidden contract: the workspace-level system message, the sampling defaults, and the max output tokens. This is the same set of losses covered in extracting the prompt templates.
  • Condition / branch node → an if chain or a switch over a discriminated union. Hidden contract: the fall-through. Every builder has a default branch, most of them make it optional, and an unmatched condition with no default frequently ends the conversation silently. Reproduce the silent end deliberately or fix it deliberately, but know which you did.
  • Intent classifier node → either a call with a constrained output, or a real classifier. Hidden contract:the confidence threshold below which it routes to fallback. That number is in a settings panel and it is load-bearing.
  • Variable / set node → an assignment on the typed context. Hidden contract: the type coercion rules. A canvas that treats everything as strings will have compared "0" to 0 somewhere and got an answer you now have to match.
  • API / HTTP node → a typed client function. Hidden contract: the timeout and the retry count, which are platform defaults you never chose. Those defaults are almost certainly not your HTTP library’s defaults — the gap between default timeouts is its own source of migration surprises.
  • Knowledge / retrieval node → a retriever call plus prompt assembly. Hidden contract: top-k, the score threshold, and what the flow did when nothing cleared the threshold.
  • Loop / iterate node → a bounded for loop. Hidden contract: the platform’s maximum iteration count, which existed whether or not you set it, and which was silently protecting you from an infinite loop that will now cost money.
  • Human handoff node → an integration call plus a state transition that stops the bot from replying. Hidden contract: what happens to messages that arrive while the handoff is pending.
  • Fallback / catch-all node → the default arm of your dispatcher and an error boundary around each step. Hidden contract: whether it fires on classifier misses, on node errors, or both, and whether it consumes the turn or retries it.

Write this table out for your own flow, with a row per node instance rather than per node type, and mark each row unverified until you have watched the live system do it. The rows that stay unverified are the bugs you will ship.

The conversation state the platform held

The builder persisted the variable bag between turns and you never thought about it. Now you own three decisions it made for you.

What is in the bag. Model it explicitly as a typed record with a version field, not as a loose dictionary. The version field matters because you will change the shape while sessions are live, and a running conversation holding last week’s shape has to either migrate or be retired on read.

How long it lives. Builders expire sessions after some idle period. Find yours and reproduce it, because it silently defined your product: a user returning after an hour either continued or restarted, and both are defensible but only one matches the behaviour people have learned.

What of it goes into the prompt. The platform decided how many previous turns to include, and that decision governed both cost and the moment at which the bot forgot something. Make it an explicit windowing function with a token budget rather than a constant turn count, and pair it with a test for what falls out of the window.

Building it in order

  1. Capture traces from the live flow first. Fifty to a few hundred real conversations with the node path each one took. This is your specification and your test set, and it stops existing the day the account closes.
  2. Define the context type and the store. Everything else depends on it, and getting it wrong late is expensive.
  3. Implement the happy path end to end, one branch only. A single working path through the whole system beats ten half-implemented nodes, because it proves the state store, the model call and the response assembly all fit together.
  4. Add branches in traffic order. Sort the captured traces by node path frequency and implement the paths in that order. The tail is long and much of it is unreachable; you will find flows that no conversation has taken in months.
  5. Replay the captured conversations against the new code. Compare node paths first, not text. A divergence in the path is a logic bug; a divergence in wording with the same path is a prompt difference, and separating those two questions is what makes the failures debuggable.
  6. Add structured logging at every node boundary before cutover, not after. Node entered, decision taken, variables changed, latency, cost. You are about to lose the platform’s debugging view and you need yours working first — the log schema is worth designing on purpose.

Running both until one wins

Do not cut over on a date. Route a small share of live conversations to the new code, holding the same provider and model the platform used, so that the only variable that has changed is your implementation. Compare the node-path distribution and the completion rate between the two populations; a shifted path distribution is the earliest visible sign of a mis-mapped branch condition, and it shows up long before anyone complains about an answer.

Set the rollback trigger before you start and make it a number: completion rate below the incumbent by more than some margin, or handoff rate above it, evaluated on a fixed window. A rollback criterion agreed after the incident is not a criterion. Raise the share in steps, and only after the new path is carrying all traffic should you change the model or the provider. One change at a time is the whole discipline of this cluster.