Multi-channel

We replaced Twilio Studio with a visual IVR — here's why the simulator matters

Most IVR builders are paint-on-glass. The simulator runs different code than production, so flows that pass the simulator misbehave on real calls. We rebuilt it as one state machine.

9 May 2026 6 min readBy Autocloz Editorial, Voice team
We replaced Twilio Studio with a visual IVR — here's why the simulator matters

The bug that bit us

We'd "tested" a flow 40 times in Twilio Studio's simulator. It went live. Three minutes into the first real inbound, the call hung up at the wrong node. In the simulator, "press 1 → end_call" showed end_call. On the real call, it hit a no-match fallback we'd defined a week earlier and forgotten.

The reason: the simulator and the runtime were two different code paths. The simulator was a JavaScript walker over the JSON tree. The runtime was a TwiML emitter that read the fallback semantics differently. They drifted apart, and a real customer call is where we found out.

What we built instead

One state machine. The Autocloz IVR runtime, the simulator, and the production TeXML emitter all call the same _load_tree_pack() + _texml_for_node(). The only thing that changes between a simulation and a live call is whether Telnyx is on the other end of the line.

Sounds trivial. It isn't. Here's what it buys you:

  • If the simulator says "press 1 → end_call", the real call does too. Always.
  • A no-match fallback you add today is in the simulator the moment you save it.
  • A status='paused' tree returns a polite "service unavailable" both in simulation and in production.
  • Action types — dtmf_route, goto_node, play_audio, transfer_to_did, end_call — share one validation path.

Defense in depth: status filter

The status filter (status='active' AND archived_at IS NULL) kills a different bug: you edit a tree mid-shift and half-deploy it. Set the tree to "draft" and Telnyx gets a polite hangup. Only "active" trees route real calls.

XML escaping isn't optional

Every dynamic value we render into TeXML gets escaped — ampersand, less-than, greater-than, quote. Yes, even speak_text. There's a test that feeds a node the prompt A & B "d" and asserts the rendered XML contains A & B <c>, not the raw string. Nobody's injecting malicious XML into your speak_text. That's not the risk. The risk is that the moment you skip escaping, your flow breaks in ways that are miserable to debug.

What it looks like

Drag a "menu" node onto the canvas. Add a DTMF action mapped to "1" that goes to "end_call". Click Simulate, type "1", and watch the trace render: Welcome (menu) → 1 → end_call. Hit Save. Flip status to "active". Point Telnyx's CallControlApp URL at /api/voice/ivr/runtime/telnyx/{tree_id}. The first inbound call walks that exact trace.

That's the whole pitch. The simulator is the runtime. The runtime is the simulator. No drift, no surprises on a live call.

Share
Free to start

Stop reading. Start sending.

Every tactic in this article is implemented behind the Autocloz dashboard.