Using semantic benchmarks to build a self-improving text-to-query agent Conversion, a marketing intelligence company, announced it built a self-improving text-to-query agent that converts natural-language audience descriptions into editable filters, using a tool-using agent, an intermediate representation, a deterministic compiler, and a semantic benchmark called Statement Bench. The agent was tested against eight models, including Claude Opus 5, Kimi K3, GLM 5.3 Flash, and the Gemini 3.8 Flash release from this morning, with results indicating the task was more complex than standard structured generation. Most of our recent AI work at Conversion has focused on general marketing intelligence. Marketing automation teams do a wide range of work across systems: researching accounts, building audiences, planning campaigns, writing content, and acting on performance data. We have been building agents that can reason across those workflows and use the same tools that a skilled marketer would use. Those systems benefit from capable, general-purpose models. The work is open-ended, and good judgment is often more important than completing a task quickly. But we also had a backlog of smaller, more focused AI features. One was natural-language filters: let a user describe an audience in plain English and turn that description into a filter they could inspect and edit in Conversion’s existing statement builder. In Conversion, a filter is called a statement. At first, this seemed like a straightforward structured-generation task. Give a model the available fields, describe the output format, and ask it to produce JSON. It turned out to be considerably more difficult than that. Take the following example: Find contacts who submitted the demo form at least once in the last 30 days and work at a software company with an open opportunity worth more than $50,000. This requires the system to: - Find the specific form the user means by “the demo form” - Determine which field represents a company’s industry - Learn how that workspace represents “software,” which means looking at the values actually stored in that field rather than guessing - Traverse from a contact to its company and then to that company’s opportunities - Ensure that “open” and “more than $50,000” apply to the same opportunity - Apply a relative event window It also needed to do all of this quickly enough to feel like a filter interface, not a research agent. What looked like a small prompt-engineering task had become a constrained text-to-query problem. Solving it required a tool-using agent, an intermediate representation IR , a deterministic compiler, and a semantic benchmark. We ran eight models through the resulting benchmark, Statement Bench, including Claude Opus 5, Kimi K3, GLM 5.3 Flash, and the Gemini 3.8 Flash release from this morning. Results are below. Giving the agent tools Most of the information needed to answer the request above is specific to the customer’s environment. A single workspace can hold hundreds of millions of historical field values, along with its assets and objects. For obvious reasons, we could not put all of that into one prompt. Our first useful architectural decision was to stop treating the problem as ordinary structured generation. Instead, the model receives a small set of tools. It can search fields, inspect historical values, and resolve business-specific assets such as forms, campaigns, emails, and audiences. It uses those tools only when the request requires them. Much of this search infrastructure came from our recent Global Search work, which provides text and semantic search over all records in Conversion. We plan to share more on that soon The basic flow looks like this: Natural-language request | v Tool-using agent <-----------------+ / | \ | fields assets relationships | rejection with reasons \ | / | v | Constrained IR | | | v | Validator and compiler ---------------+ | v Production statement This keeps the initial context small. It also makes failures much easier to understand. If a statement is wrong, we can determine whether the agent found the wrong asset, selected the wrong field, misunderstood a relationship, represented the correct idea incorrectly, or exposed a bug in the compiler. That distinction later became important for our evaluation loop. Creating a smaller language Tool use solved the context problem. It did not solve latency. One lesson from early feedback: users tolerate far less latency in a purpose-built interface than in chat. This points to a broader paradox. We set latency expectations based on how difficult a task feels to us, not how difficult it is for the system. Writing content feels difficult because we can see the work. Describing a filter feels simple because our minds silently resolve context, entities, relationships, and intent. For the model, reconstructing those hidden assumptions is the task. The less work the user perceives, the less time they give the system to do it. Based on early feedback, we set two goals: more than 95 percent accuracy and a response time around 5 seconds for common queries. Conversion has an expressive internal query language. In our early tests, using the production format directly, only the largest models such as Claude Opus could generate it reliably. Even simple statements took around 45 seconds. The visual statement builder exposes only a subset of the full language. We created a smaller, agent-friendly intermediate representation for that subset. Smaller models could produce it using fewer tokens, while a deterministic compiler handled the full production format. Consider the statement: Job title contains “Director.” The original production statement looks like this: { "type": "LOGICAL", "version": 1, "logical": { "operator": "OR", "operands": { "type": "LOGICAL", "version": 1, "logical": { "operator": "AND", "operands": { "type": "VARIABLE", "version": 1, "variable": { "variableSchemaId": "550e8400-e29b-41d4-a716-446655440000", "where": { "type": "LOGICAL", "version": 1, "logical": { "operator": "AND", "operands": { "type": "LOGICAL", "version": 1, "logical": { "operator": "CONTAINS", "operands": { "type": "ATTRIBUTE", "version": 1, "attribute": { "name": "value" } }, { "type": "CONSTANT", "version": 1, "constant": { "value": "Director" } } } } } } } } } } } } The model-facing representation of the same filter is: { "field": "550e8400-e29b-41d4-a716-446655440000", "op": "contains", "value": "Director" } The IR has already been through several generations, and the latest was shaped by watching small models fail on the earlier ones. One large improvement was introducing better same-record semantics something schema validation cannot catch : { "related": "OPPORTUNITY", "all": { "field": "