Four personas — Analyst, Composer, Modeler, Investigator — running a ReAct loop over 29 tools against your own databases. Every write is staged as a pending proposal with its verbatim payload; the write tools have no path to the real service call. Bring your own model, including one that never leaves your building.
The interesting part of an analytics agent is not what it can do. It is what it has been built to be unable to do.
A persona is not a system-prompt flavour. It is a different tool set, a different output contract and a different set of things it is permitted to write.
The everyday surface. "What's our MRR this month?", "Compare with last quarter", "Why did revenue drop in March?", "Save this as a question called X". It answers in plain language with the key numbers and an optional suggested next step, rather than dumping a result grid and leaving you to interpret it.
It is read-only with exactly one exception: it can stage a save-question proposal. So the analyst who just talked their way to the right query can keep it — after a human has looked at what is being saved.
"Build me an executive revenue dashboard for last quarter." "Add a churn-rate card to dashboard 17." The Composer first produces a Markdown card plan for sign-off — what cards, in what order, at what size, on which questions — then stages the cards, then surfaces the URL.
It is the persona with the widest write surface: dashboards, cards, layout, filters, alerts, subscriptions and embeds. Which is precisely why the plan step exists before anything is staged.
"Build a sales model from connection 3." "Validate model 12." The Modeler either proposes a star or snowflake model, or returns a validation verdict that names a specific risk and a concrete remedy — not a grade, not a score, an actionable sentence.
It writes OLAP models and nothing else. Its rulebook is documented below, because a modelling agent whose heuristics you cannot read is a modelling agent you cannot review.
Not a chat surface at all. Given an anomaly, it runs a fixed six-step method and emits a strict JSON findings document. It is spawned by an anomaly plus a question, runs asynchronously on a virtual thread, and is polled for its result.
Its write list is empty — every write tool is filtered out of its prompt, so the persona that runs unattended is the one that structurally cannot change anything.
Most "AI dashboard" features generate a pile of charts and let taste sort it out later. Composer is given the layout rules explicitly, so the output is boring in the way a good dashboard is boring.
The reading order is fixed, not emergent. Headline numbers first, then how they moved, then who moved them, then the rows behind the claim. It is the order a competent analyst would have chosen, and it makes two dashboards built six months apart look like they came from the same organisation.
Composer must add a dashboard-level date-range filter in three cases: when the brief mentions a timeframe, when two or more cards use a date dimension, or when any line or area card exists. This is the single most common omission in hand-built dashboards, and it is the one an agent should never make.
Cards are bound to that filter through the same card × filter matrix a human author uses — no separate agent-only path.
You cannot review a proposed star schema unless you know which heuristics produced it.
These rules live in the persona's prompt. When the Modeler proposes something you disagree with, you can point at the rule it followed — which is a far better conversation than arguing with a black box.
Root-cause analysis is where language models are most persuasive and least reliable. So this one is fenced in from both ends.
At most six dimensions. Typically four to eight tool calls. And the rule that does the most work: every number in the output must trace back to a tool call. A figure the model produced from its own head has nowhere to live in the document.
Not prose. A fenced JSON structure with a summary, whether the anomaly was confirmed, its magnitude, the root cause with its contributing segments, correlations, the historical comparison and suggested actions. Machine-checkable, diffable, and attachable to an incident record.
This is the part that matters. An outlier threshold validator, entirely separate from the language model, re-checks every segment the Investigator claimed was anomalous — statistically, against the data — and annotates each one outlier_verified.
So a reviewer never has to take the narrative on trust. They read the claim, and next to it they read whether the arithmetic agrees. A confident sentence with an unverified flag beside it is exactly the signal you want.
Investigations start three ways: manually, from a weekly digest scheduler that launches up to three per organisation on Monday mornings, or from an auto-trigger listener that fires with no human present when an anomaly's severity clears the organisation's configured floor.
Running unattended is only acceptable because this persona cannot write. The two design decisions are the same decision.
Not a confirmation dialog bolted on top. An architectural choke point that the tools cannot route around.
Every one of the thirteen write-side tools does the same thing: it stages a pending action carrying the verbatim payload and returns the proposed action id. That is the whole tool. There is no branch inside it that reaches the domain service, no privileged mode, no "auto-apply for trusted models" flag. The capability simply is not there.
require_approval_for_writes defaults to true. An organisation can turn it off. Most should not, and the default is chosen accordingly.
The whole surface, named. Nothing is hidden behind a generic "run code" escape hatch.
list_connectionsget_schemaget_sample_datadescribe_columnvalidate_columnscompile_qqlexecute_queryget_question_resultsearch_questionslist_dashboardsget_dashboardsuggest_chartdetect_anomaliesget_olap_modelsuggest_olap_modelvalidate_olap_modelsave_questionsave_descriptioncreate_dashboardadd_dashboard_cardupdate_dashboard_cardremove_dashboard_cardupdate_dashboard_layoutupdate_dashboard_filterscreate_alertschedule_subscriptionshare_embedcreate_olap_modelupdate_olap_modelFailure is never thrown as an exception here, because the interface has to render every stop condition. A run ends as COMPLETED, MAX_STEPS, BUDGET_EXHAUSTED, TIMED_OUT or ERROR — and the user sees which.
A step cap. A daily token budget, re-queried live mid-run so two concurrent sessions cannot double-spend the same allowance. A wall-clock deadline, 120 seconds by default. And a provider error. Any one of them ends the run cleanly, and every non-error stop leaves the session resumable.
The same tool call issued three times returns a synthetic circuit-breaker observation telling the model to change approach or finalise — and is not counted as executed. Models get stuck; the runtime notices before your budget does.
Prompt-level filtering decides what the model is told about. The per-session allowlist, enforced server-side, decides what it can actually invoke. Two mechanisms, because the first one is a suggestion and the second one is a rule.
Unknown tool names return an observation rather than an error — models routinely invent names — and tool exceptions come back as observations too, so the loop survives and self-corrects instead of dying on a typo.
Step number, tool name, input, output, truncation flag, duration, status. The truncation flag matters more than it sounds: it tells a reviewer whether the model saw the whole observation or a clipped one, which is often the difference between a sound conclusion and a confident wrong one.
Opt-in per-step context auditing goes further and records the exact system prompt sent, the provider, the model, the token counts and a breakdown of every injected section. The UI exposes the same thing live: a chip under any assistant message opens the prompt with its provenance.
Not every model you might want to run locally speaks a tool-calling API. The fallback parser accepts either a <tool_call> tag or a fenced JSON block, and preserves the thinking preamble rather than discarding it. Small local models become usable rather than theoretically supported.
Memories are extracted from completed transcripts on a named virtual thread, double-guarded, and injected into every system prompt. If extraction fails, it degrades silently — the user's request has already been answered by then, and a memory-writing bug is not permitted to take the answer with it.
Every other prompt side-block behaves the same way: connections, recent questions, schema and existing models each degrade to an in-prompt note on failure, so one dead data source cannot block a session from starting.
The LLM provider is a row in your own database, not a vendor relationship you inherited by installing our software.
CLAUDEclaude-sonnet-4-6OPENAI, LM_STUDIO, JAN_AI, OPENCHAT, LOCAL_AI, GPT4ALL, LIBRECHAT, LOBE_CHAT, OPEN_WEBUI, MISTRALgpt-4o-miniAZURE_OPENAI — deployment-qualified base URL, fails loudly if blank rather than guessingGEMINIgemini-1.5-flashOLLAMA — sampling nested under options, tools key omitted for older buildsllama3.1This is the argument, and it is worth stating without hedging. Qrly is self-hostable. The LLM provider is a per-organisation row you configure. Point that row at an Ollama or LM Studio instance on your own network and there is no egress to reason about — not the schema, not the sample rows, not the prompt, not the answer.
Every SaaS analytics vendor that offers "AI" is asking you to send your data somewhere. This is the same product with that requirement removed, and it is a checkbox rather than an enterprise negotiation.
Each organisation's provider carries a name, type, encrypted API key, base URL, default model, max tokens (4096), temperature (0.7), an enabled flag, input and output cost per million, a currency, a budget period and amount, extra options, a security system prompt and the creator.
Agents can be pointed at a different provider than the rest of the application — a cheap local model for narration, a stronger one for investigations — otherwise the organisation's first enabled provider by name is used. Adding a new dialect means dropping in a component; the factory needs no edit.
Everything below is an administrator setting, not a support ticket.
agents_enabled defaults to false, with per-persona toggles underneath. You enable the Analyst without enabling the Composer if that is where you want to start.Budget accounting has two checkpoints, not one. A pre-call check before the model is invoked, and a post-tool re-check on every iteration — so a long run cannot overshoot its allowance by an entire turn. Spend is accumulated per step and flushed to the session, and budget state is surfaced to clients in the 429 response rather than being an opaque refusal. Administrators get endpoints for settings, usage, usage detail and the daily reset.
Narration, seven insight detectors, anomaly detection and investigations — each with a numeric trigger you can read, argue with and change.
TREND_CHANGEMILESTONENEW_RECORDSTALE_DATACORRELATIONSEASONALITYDATA_QUALITYOne-paragraph AI summaries for questions, dashboards and connections, stored per resource and per language, queued and drained by a worker with a per-organisation concurrency cap and a fresh-cache skip. Narration never blocks a page render and never re-bills you for a summary that has not gone stale.
The detector re-runs recently used questions, treats the first numeric column as the series, and emits ANOMALY above a z-score of 2.5 and WOW_DELTA above 25 % week-over-week. Findings are deduped within 24 hours, with an optional AI prose summary on top of the numbers rather than instead of them.
The pair spawns an Investigator session on a virtual thread, asynchronous and polled. A weekly digest scheduler launches up to three per organisation on Monday morning. An auto-trigger listener starts one with no human present when severity clears the organisation's floor.
High-severity insights are dispatched honouring the organisation's minimum severity and each user's own preferences. The default posture is quiet: an insights feed you visit, with push reserved for the things that actually clear the bar you set.
No. Every write-side tool stages a pending proposal carrying the verbatim payload; write tools have no path to the real service call at all. A human approves or rejects, and only approval dispatches to the owning domain service. The require_approval_for_writes setting defaults to true, and there is a second layer behind it: while approval is required, a write tool that returns success without having staged a proposal is replaced with an error, so a future tool author who forgets to stage cannot open a hole.
Agents also never write to your data sources — the SQL sanitizer accepts SELECT and WITH only.
Five adapters cover Claude, the whole OpenAI-compatible family (OpenAI, LM Studio, Jan AI, OpenChat, LocalAI, GPT4All, LibreChat, Lobe Chat, Open WebUI and Mistral), Azure OpenAI with a deployment-qualified base URL, Gemini, and Ollama.
The factory upper-cases the provider type, takes the first adapter that supports it, and falls back to the OpenAI adapter — so any OpenAI-compatible endpoint works out of the box without a code change.
That is your decision, and it is a configuration setting rather than a vendor policy. Qrly is self-hostable and the LLM provider is a per-organisation row you own — name, encrypted API key, base URL, default model, budget.
Point it at a local Ollama or LM Studio instance on your own network and no row of customer data, no schema and no prompt ever crosses your perimeter. Point it at Claude or Azure OpenAI instead and it behaves like any other cloud AI feature.
Four independent stop conditions end every run: a step cap, a daily token budget re-queried live mid-run so concurrent sessions cannot double-spend, a wall-clock deadline defaulting to 120 seconds, and a provider error. Budget is checked before a call and re-checked after every tool, so a long run cannot overshoot by a whole turn.
A repetition circuit breaker intercepts the same tool call issued three times. Defaults are 10 steps with a ceiling of 25, 50,000 tokens per day and 50 sessions per day, all per organisation.
It is not a chat surface. It runs a fixed six-step method — confirm the anomaly, inventory dimensions, segment one dimension at a time across the anomaly and baseline windows, correlate adjacent metrics, historicise against the same period last week, month and year, then synthesise — under hard caps of at most six dimensions and typically four to eight tool calls, with every number required to trace back to a tool call. Its output is a strict fenced JSON findings document, and every write tool is filtered out of its prompt so it structurally cannot write.
An independent statistical outlier validator then re-checks every claimed segment and annotates it, so a reviewer does not have to take the model's narrative on trust.
No. The master agents_enabled switch defaults to false, with separate per-persona toggles underneath it. An administrator turns on exactly the personas they want, sets the step limits, result truncation, approval requirement, daily token and session budgets, the MCP toggle, the narration settings and the proactive settings, and can point agents at a different LLM provider than the rest of the application.
Off by default. Read-only by construction. Every write a proposal. And a model that can run entirely inside your own network.