structured capture without scripted flows
Voice-agent platforms have, with eerie consistency, all converged on the same metaphor for capturing structured data: the flow. A node graph. A state machine. A diagram with happy paths and edge handlers. You drag boxes around and define transitions. It looks rigorous. It feels like control. It is the wrong abstraction for an LLM-native product.
What flows are good at
Constraining behavior. If you have a hard requirement that the agent cannot move past 'collect appointment time' until it has a valid datetime, a flow makes that easy to express and easy to audit. The diagram shows you exactly which transitions are allowed.
What flows are bad at
Everything else. Real conversations don't fit them. The recipient asks a question out of order; the flow either ignores it (rude) or has to define a 'detour' edge (state explosion). The recipient gives two answers in one breath; the flow has to decide which slot to fill first (race condition). The recipient says something the flow author didn't anticipate; the flow falls back to a script that exists for a reason no one remembers.
We adopted a popular flow library in late February. Two weeks later we ripped it out. Here's what we replaced it with.
Slot filling, language-driven
A capture schema is just a list of typed slots — name, type, description, required. The agent's system prompt knows the schema and is instructed to fill slots conversationally, in any order, asking follow-up questions as needed. Each slot becomes a typed function call when the agent has enough information to commit it.
{
"slots": [
{
"name": "appointment_time",
"type": "datetime",
"description": "When the caller wants to be scheduled",
"required": true
},
{
"name": "service_type",
"type": "enum",
"values": ["consultation", "follow-up"],
"description": "Type of appointment",
"required": true
},
{
"name": "callback_number",
"type": "phone",
"description": "Where to reach the caller for confirmation",
"required": false
}
]
}The pipeline factory generates one tool per slot — set_appointment_time, set_service_type, set_callback_number. Each tool's argument type is the slot type. Each tool call writes to a per-call capture table inside the same transaction as the call's audit log.
Why this works better
Resilience: the LLM handles ordering, clarification, and disambiguation. If the recipient says "3pm Thursday for a follow-up," the agent fires both tools in the same turn. If they correct themselves five turns later ("actually, make that 4pm"), the agent fires set_appointment_time again with the new value. The capture table records both.
Auditability: the full state of the capture is the sequence of tool calls. You can replay any call and see exactly what was committed and when. There is no opaque flow-engine state.
Determinism where it matters: the typed tool argument is the source of truth. The agent might phrase the same datetime ten different ways in conversation; only one normalized value lands in the database. The compliance layer reads the database, not the transcript.
The trade we accept
Loss of upfront constraint enforcement. With a flow you can guarantee the agent will not move past a step. With slot filling, the agent might 'forget' a required slot and try to wrap up the call. We catch this at the capture layer: if the call ends with required slots unfilled, the post-call analyzer flags it and the operator gets a notification with the unfilled slots and the transcript context.
Net: more captures land. Fewer calls require manual review. The team that maintained the flow diagrams now maintains the slot schemas, which are 10x smaller documents and don't need a visual editor. Sometimes the right abstraction is the boring one that lets the model do its job.