Source: https://velofy.co/querion/how-it-works/

[Querion](https://velofy.co/querion/) / Get started

# How it works

Querion's design from first principles: the problem, the constraints, the read-only directive loop, the trade-offs and the failure modes.

Querion lets a language model do an analyst’s work against live data without giving it any way to change that data. This page explains why it is built the way it is.

For the module layout, see [Architecture](https://velofy.co/querion/architecture/). For every check exactly as the code applies it, see the [Safety model](https://velofy.co/querion/safety-model/).

## The problem

An analyst answers a question in a loop. Find the table or system that holds the answer. Write a query. Read the result. Decide whether it answers the question or whether another query is needed.

A language model is good at the language parts of that loop: understanding the question, writing Structured Query Language (SQL), and explaining a result. The risk is in the part that touches systems. A model that can run any statement can run `DELETE`. A model that can call any endpoint can call one that cancels an order.

So the problem is narrow. Give the model the analyst’s loop, and remove every path from its output to a write.

## The constraints

Five facts shape the design.

1.  **The model’s output is untrusted text.** It can be wrong, and the user, a schema note or an API response in its context can steer it. Anything it writes must be checked before it reaches a system.
2.  **Only the backend can enforce read-only.** Postgres decides what a role may do. An application programming interface (API) decides what a token may do. Querion can filter what it forwards, but it cannot make a privileged credential safe.
3.  **The model knows only its context.** It has never seen your schema or your APIs. If Querion does not put them in the prompt, the model guesses names.
4.  **Model calls are slow and metered.** Each call is a separate run of the Claude Code command-line interface (CLI), with a 200-second limit. A question that needs ten round trips costs ten calls.
5.  **Context is finite.** A full result set or a large API response does not fit in a prompt. What goes back to the model has to be cut.

## The mechanism

Each part of Querion answers one of those constraints.

### The model writes text, Querion acts

Querion runs `claude -p` with its file, shell, web and task tools disallowed and no Model Context Protocol (MCP) servers loaded. The model cannot run a shell command, read a file or fetch a page. It can only return text (constraint 1).

To read data, the model writes a fenced block called a directive. There are three kinds:

*   `run-sql`: one `SELECT` or `WITH` statement for Postgres.
*   `source:NAME`: one `GET /path` request to a configured API.
*   `chart`: a JavaScript Object Notation (JSON) spec drawn from data the model already has.

Querion finds these blocks with a regular expression and runs them itself. Everything else in a reply is plain text. The set of directives is closed and small, so the set of things the model can cause is closed and small too.

### Every executor only reads

Each directive goes to an executor that has no write path:

*   SQL is checked by `validate_sql`, then runs on a new connection opened as a read-only session with a statement timeout.
*   Hypertext Transfer Protocol (HTTP) requests are always sent with `requests.get`. Write verbs are refused, and a per-source allowlist (`safe_get`) can limit the paths.
*   Charts draw data that was already fetched. They read nothing new.

Under all of this sit your read-only database role and read-only API tokens (constraint 2). Querion’s checks are the backstop. The credentials are the guarantee.

### Knowledge goes in first

Before the first call, Querion assembles a knowledge block: the organization name, the database schema (a curated file or a live read of `information_schema`), one section per API with its docs file, and an optional [Trove](https://github.com/anishfyi/trove) semantic layer. The block goes into the system prompt together with today’s date, the directive rules and the read-only rules (constraint 3).

### Results come back short

Each result goes back to the model as text: at most 30 rows of a query result, with cells cut at 40 characters, or the first 6000 characters of an API response (constraint 5). The model sees enough to reason about the data, so questions work best when the database does the counting and the model reads the summary.

### The loop is bounded

One question runs at most `limits.max_steps` turns (default 7). Each turn runs at most `limits.max_directives_per_step` directives (default 4). A reply with no directive is the final answer. If the model still asks for data after the last turn, Querion makes one more call that asks for a final answer with no directive. The worst case is `max_steps + 1` model calls per question (constraint 4).

### Each turn starts from scratch

No CLI session carries over between calls. Every turn sends the full transcript so far, the question plus each reply and its results, to a fresh `claude -p` process. Nothing from one question leaks into the next, and there is no session state to clean up.

## One question, step by step

Take “how many orders were placed in the last 7 days?” against a database with an `orders` table.

1.  The write-request firewall checks the question for a pasted write statement. There is none.
2.  Querion builds the system prompt: today’s date, the directive rules, the read-only rules and the knowledge block.
3.  The model replies with a `run-sql` block, for example `SELECT count(*) FROM orders WHERE created_at >= now() - interval '7 days'`.
4.  Querion validates the statement, runs it in a read-only session and appends `1 row(s):` and a small text table to the transcript.
5.  The model replies with a headline number, the query in a plain `sql` block for display, and no directive. That reply is the answer.

That is two model calls and one query. For a question that compares the database with a live API, the prompt lets the model put independent reads, such as a query and an API call, in the same turn, and tells it to keep reads that depend on an earlier result for the next turn.

## Trade-offs

*   **The CLI instead of an API key.** There is no key to store or rotate, and calls run under the host’s existing Claude Code login. The cost is a new process per call, a host that must stay logged in, and a fixed 200-second limit per call.
*   **Stateless turns.** Simple and isolated. The full transcript and the knowledge block are sent again on every turn, so each extra step costs more than the one before.
*   **A keyword denylist instead of a SQL parser.** Small and easy to read. It refuses some harmless queries, such as one that names a column `comment`, and it does not understand SQL. The read-only session and the role are what make a write impossible.
*   **Short feedback.** It keeps prompts small. The model never sees more than 30 rows of a result, so it depends on SQL that aggregates.
*   **Default-deny allowlists.** A source with `safe_get` set reaches only the paths you listed. A read endpoint you forget is refused until you add it.
*   **A model-driven refusal for plain-English write requests.** It can catch “cancel order 14377”, which no fixed pattern can match reliably. It is not deterministic, so the executors must hold even when the model does not refuse.
*   **In-memory web state.** The server needs no database of its own. It must run as one process, and runs in progress are lost on restart.

## Failure modes

*   **A query fails.** The error text goes back to the model as the result, and it can correct the query in the next turn, within `max_steps`.
*   **The model uses names that do not exist.** The knowledge block is missing or stale. The server builds it once at startup, and live introspection covers only the `public` schema. If introspection fails, the model sees the error text instead of a schema. Write a curated `schema_doc` and restart.
*   **The step limit is reached.** Querion forces a final answer, which may be partial.
*   **The model ignores the read-only instruction.** A SQL write is refused by `validate_sql`, then by the read-only session, then by the role. An HTTP write verb is refused before any request is sent. A `GET` that changes state on the server is the remaining gap, and only `safe_get` and a read-only token close it.
*   **Data steers the model.** Rows and API responses go back into the prompt as text, so a value that reads like an instruction could change the next directive. The executors still only read. The damage is limited to a wrong answer or a read that the role and allowlist already permit. That read can carry data the model has seen to a configured API in a query string, because the allowlist checks only the path.
*   **The CLI is missing, logged out or slow.** The question ends with status `error` and a message such as `claude CLI timed out after 200s`. See [Troubleshooting](https://velofy.co/querion/troubleshooting/).

## What follows for your setup

*   Give Querion a role that can only `SELECT`, and revoke access to tables that hold secrets.
*   Use read-only API tokens, and set `safe_get` on every API that exposes anything other than reads over `GET`.
*   For a large schema, or tables outside `public`, write a curated `schema_doc`.
*   Put `querion serve` behind your own authentication before anyone else can reach it.
