ag
← back
June 10, 2026Case study

A voice agent that fills client forms 3× faster

Custom Webfuse MCP tools for an ElevenLabs voice agent driving a multi-step intake form - 22× fewer tokens (66K → ~3K) and fill time cut from 3 minutes to 1.

WebfuseElevenLabsMCPVoice AIAutomationTypeScript

A voice agent that fills client forms 3× faster

Client work delivered on Webfuse. Specifics are kept confidential; what follows is the architecture and the results.

The problem

A client wanted callers to complete an online intake form - a short, branching, multi-step assessment - entirely by voice. The build was an ElevenLabs conversational agent driving the live page inside a Webfuse session, using Webfuse's automation tools over MCP.

It worked, and it was too slow and too expensive to ship. Each step ran the generic agentic loop: take a DOM snapshot (Webfuse's see_domSnapshot automation call), reason over it, guess a selector, click, snapshot again. The snapshot was the expensive part - a single one of this page could run 3K+ tokens on its own. Multiply that by every screen in a branching form and you reach tens of thousands of tokens before the agent has accomplished anything. A run took about three minutes, and the agent still mis-clicked because it was inferring targets from coordinates.

The instinct on a team is usually to fix this with a better system prompt. I read it differently: the model wasn't the bottleneck - the interface we'd handed it was. We were asking a language model to do computer vision and DOM archaeology on every turn, when the page already knew its own state. Token cost and latency scaled with the size of the DOM times the number of steps. No prompt change bends that curve.

The design

So I changed the interface, not the prompt. I registered a small set of custom MCP tools, scoped to this form, on the Webfuse Session MCP server. The agent lists and calls them like any other tool - but now it acts against a purpose-built surface instead of raw markup. Five tools: one reader and four actions.

  • get_state (the reader) returns a compact, structured view of the current screen - about 200 tokens describing the active question and its valid options. This is the one that matters most: because the agent reads state here, it never needs a DOM snapshot during the flow. The single most expensive call in the old loop - the 3K+ token see_domSnapshot - is gone entirely.
  • select_option answers the current question by choosing one of the options get_state returned. One generic tool, not one per screen.
  • lookup_address handles the single typed-input step that needs a lookup, then returns the resulting state.
  • submit fills the final step - contact details and consent - and confirms.
  • back steps back one screen.

The deliberate choice was a constant, five-tool surface for a form with a variable number of screens. The screens share one shape - "answer the current question" - so a state reader plus a generic selector cover every choice step; only the two free-text steps and the back action need their own tools. The agent's tool list doesn't grow when the client adds questions, and every action drives a known target through the Webfuse Automation API, so a click is an instruction, not a guess.

There's a deeper reason those targets are known. The engagement gave me access to the client's codebase, so I fixed the targeting problem at its source: I added stable element IDs to the form itself and pointed the tools at those IDs. The reliability win came from controlling both ends of the contract - the IDs in the app and the tools that drive them - instead of reverse-engineering brittle selectors from the outside. That is what took the agent's error rate down: a click can't miss a target that's guaranteed to be there.

Three judgment calls did the real work:

  1. Pick the right abstraction boundary. The model should reason about intent ("which answer"), not mechanics ("which pixel"). Moving the mechanics behind tools is what collapsed the snapshot-reason-snapshot loop into a single read.
  2. Design the tool surface for change, not for today's form. Granular per-screen tools would have looked tidy and rotted the moment the client edited the flow. The surface I shipped doesn't churn when the form does.
  3. Respect the framework underneath. The form is a React controlled-component app, so writing values to the DOM directly would be silently discarded. Actions go through the Automation API, which dispatches real user events React actually observes - and, as a bonus, renders the visible pointer and typing feedback in the live session.

Results

  • Token use: 66K → ~3K per run - 95.5% fewer, a 22× reduction.
  • Fill time: ~3 minutes → ~1 minute - 3× faster.
  • A much shorter, cheaper system prompt - behaviour lives in the tools, so the prompt doesn't have to carry it.
  • A sharply lower error rate - actions target stable IDs I added to the form, so clicks are deterministic by construction instead of best-effort guesses against inferred coordinates.

The principle

Treat the model as the consumer of a well-designed API, not a user squinting at a screen. The leverage in agentic systems is rarely in the prompt; it's in choosing where the boundary between the model and the system sits, and giving the model the smallest, sharpest surface that still expresses intent. Get that boundary right and cost, latency, and reliability all move at once - which is exactly what happened here.