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
A flow we'd "tested" 40 times in Twilio Studio's simulator went live, and three minutes into the first real inbound the call hung up at the wrong node. The simulator's "press 1 → end_call" path showed end_call. The real call hit a no-match fallback we'd defined a week earlier.
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 didn't understand the same fallback semantics. They drifted, and we paid for it on a real customer call.
What we built instead
The Autocloz IVR runtime is a single state machine. The simulator and the production TeXML emitter call the same _load_tree_pack() + _texml_for_node(). The only difference between simulating and running live is whether Telnyx is on the other end.
That sounds trivial. It isn't. It means:
- 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) prevents a different class of bug: editing a tree mid-shift, accidentally deploying it half-built. 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 rendered into TeXML is escaped (ampersand, less-than, greater-than, quote). Yes, even speak_text. We wrote a test that pumps a node with A & B as the prompt and asserts the rendered XML contains A & B <c>, not the raw string. The point isn't that anyone is going to inject malicious XML into your speak_text — the point is that the moment you don't escape, your flow becomes brittle in ways that are hard 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" in the input box, and watch the trace render: Welcome (menu) → 1 → end_call. Hit Save. Set status to "active". Point Telnyx's CallControlApp URL at /api/voice/ivr/runtime/telnyx/{tree_id}. The first inbound call walks the same trace.
That's the whole pitch. The simulator is the runtime; the runtime is the simulator. No drift, no surprises.