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.
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 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.