Skip to content
AnrilX
All insights

Why we never let a model write the SAP query

· 4 min read · The AnrilX team

There is an obvious way to build AI over an ERP: describe the schema to a language model, ask it to write a query, run the query, show the answer. It demos beautifully. We built the first version that way and then spent a long time understanding why it could not be trusted.

The failure is not that the model writes invalid queries. Invalid queries are the good case: they error, you see the error, you fix something. The failure is that it writes valid queries against the wrong thing.

What a wrong answer looks like

Ask an SAP system how much discount you gave last month. There are thirty-six entity sets whose name contains some form of “sales order”. One of them is the order header. It carries a field called TotalNetAmount, which is a real field holding a real number.

A model asked to write that query will very often sum TotalNetAmount, label it “discount”, and return it. Every part of that is well-formed. The entity exists, the field exists, the aggregation is correct, the period filter is right. The number that comes back is a genuine measurement, of something else entirely.

Nothing downstream can catch it. It is not an anomaly, it does not fail a sanity check, and it is the right order of magnitude for an answer somebody was expecting. It is simply a different figure wearing the name of the one you asked for.

The rule we settled on

The model reasons. Code queries.

A question is compiled (in one model call) into a typed specification. That specification is not a query string. It cannot hold one. It holds an entity label chosen from a list the platform built, a measure, a grouping, a set of structured predicates and a period token.

Four things follow, and each closes a failure we actually measured:

It cannot name the wrong entity. The candidate list is assembled by code from the catalogue and ranked. The model picks a label out of it. An entity outside that list has no way to be expressed; this is different in kind from being told not to.

It cannot write a filter. Predicates are structured, and each field is resolved against the live catalogue. There is no slot anywhere in the format for a filter string. This one came from watching a model spend fifteen minutes cycling through in (...), padded keys, substring(), endswith and finally eq '4': thirty reads, twenty-two of which returned byte-identical empty results.

It cannot write a date. Periods arrive as tokens (this_month, last_quarter, ytd) and are resolved against the server clock at execution. This is why a saved view called “last 30 days” still means the last 30 days a year later. We found eight saved dashboards carrying hardcoded date ranges, including one titled “Last 30 Days” showing a window from the previous November.

A wrong field name is repaired or the question escalates. Resolution requires both a similarity floor and a margin over the runner-up, because near-identical field names are real: there are systems carrying AdditionalCustomerGroup1 through 5, which score 0.958 against each other with a margin of zero. A repair is shown in the answer. Ambiguity is escalated, not guessed.

What it costs

Expressiveness. There are questions this cannot compile, and they escalate to a slower path that reasons. We consider that the correct trade: the slow path is a cost in seconds, and the fast wrong answer is a cost in trust that you only discover much later.

It also costs a candidate list. Building one means indexing the whole SAP surface into something searchable, ranking it well, and accepting that ranking is a real engineering problem rather than an embedding call. That is most of the work.

The part that is not solvable this way

One thing does not yield to structure. Whether the word “discount” describes the field TotalNetAmount is a judgement about meaning, and no similarity threshold separates it cleanly; we measured the distributions and they overlap where it matters.

So we do not judge it. We state it. Every figure prints what it actually computed:

Source: zsd_sales_order / ZA_SalesOrder · discount = sum of TotalNetAmount · period all time

Wrong in a way a reader can see at a glance, rather than wrong in a way that rests entirely on the model’s choice of words. That is a weaker guarantee than the other four, and it is the honest one.

In the product

The same idea, in the product

The parts of AnrilX this article argues about.

Questions this raises

Why not let a language model write the SAP query directly?

Because a plausible query against the wrong entity returns a plausible number, and nothing downstream can tell it is wrong. Asked how much discount you gave last month, a model will very often sum TotalNetAmount on the order header: a real field holding a real number, correctly aggregated over the right period, measuring something else entirely. It is not an anomaly, it fails no sanity check, and it is the right order of magnitude for the answer somebody expected.

What does AnrilX do instead?

The model reasons and code queries. One model call compiles the question into a typed specification that cannot hold a query string: an entity label chosen from a list the platform built, a measure, a grouping, structured predicates and a period token. The model cannot name an entity outside that list, cannot write a filter and cannot write a date, because the format has nowhere to put any of them.

What does compiling the query instead of writing it cost?

Expressiveness. Some questions cannot be compiled and escalate to a slower path that reasons. That is the intended trade: the slow path costs seconds, while a fast wrong answer costs trust you only discover much later. It also costs a candidate list: indexing the whole SAP surface into something searchable and ranking it well is most of the work.

Bring the question your reports cannot answer

Thirty minutes against a live SAP system we provide: no access to yours, nothing to set up. If it cannot answer, you find that out in half an hour rather than three months into a pilot.