There is a shape that natural-language querying tends to take, and it is the wrong one: send the question and the schema to a model, ask for a database query, run whatever comes back. It demos well. It fails in a specific and expensive way — a query that runs, returns rows, and answers a question nobody asked.
That failure is expensive because it is invisible. A syntax error is free; you see it immediately. A wrong-but-plausible number gets read out in a meeting.
Separate the language problem from the query problem
The version that holds up starts by refusing to treat these as one thing.
Underneath sits a query engine that has nothing to do with language. It takes a versioned intermediate representation — entities, fields, operators, predicates — validates every part of it against a catalog, compiles it, and runs it. No model is involved. It is deterministic, it is free to run, and it is either correct or broken in the ordinary way that code is.
Above it, exactly one component is allowed to call a model, and its only job is turning a sentence into that IR. Not into rows. Not into a database query. Into the same structured thing a client could have written by hand.
The value of the split is that it is checkable. A test asserts that nothing else in the package imports the AI service, and that the engine still answers with the model provider switched off. Those two properties are what make “the model is not in the critical path” a fact about the system rather than a claim about its intentions.
Make the wrong answer unrepresentable, then handle the rest
A planner’s characteristic bug is naming a field that doesn’t exist. The instinctive fix is to tell the model not to — put the schema in the prompt, ask nicely, add an example.
The structural fix is better: generate the model’s output schema from the catalog itself, and constrain generation to it. Then an unknown field is not something the model is discouraged from emitting. It is something it cannot emit. The same generation step is where scope belongs, too — if the catalog was filtered by what this person is allowed to read before it became a schema, then the model cannot name a field they may not see, and no downstream permission check has to catch it. Nothing in the prompt has to ask for either property.
What this does not fix is the interesting part. The model can still pick the wrong field, the wrong time window, or the wrong predicate, and produce a perfectly valid IR that answers a question the person didn’t ask. No schema prevents that, because the output is well-formed. It is the understanding that is wrong.
Design for the failure you can’t remove
So the design stops trying to prevent it and starts trying to make it visible.
Every clause the engine resolved gets rendered back as a plain statement of what the question was taken to mean — this field, this operator, this value, this window. Each one is editable. Correcting one re-runs the deterministic path, which calls no model and costs nothing.
That is the whole asymmetry, and most of the rest of the design falls out of it:
The model gets one shot at understanding. The person gets unlimited cheap corrections.
Cheap is load-bearing. If re-running cost a model call, corrections would be rationed, and a rationed correction is one that doesn’t happen — people accept the first answer instead. Because the engine underneath is free, the interface can afford to invite disagreement. The system’s willingness to be told it was wrong is a direct consequence of where the model was placed.
What it costs
Two things, and neither is free.
The catalog becomes a contract with several consumers — the endpoint that publishes it, the prompt that renders it, the schema that constrains generation, the compiler that validates against it. Change it carelessly in one place and the others disagree silently. That has to be held together by tests that check the declarations against the actual data model, because nothing else will notice.
And the interface carries weight the demo version doesn’t. Showing what was understood is more work than showing an answer, and it is less impressive. A system that says “here is what I think you asked” is admitting that it might be wrong, which is a strange thing to build on purpose when the alternative looks more confident.
It looks more confident because it is hiding the same uncertainty. That is the only real difference between the two.