Agents that propose. People that decide.
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.
What the agents do — and what they structurally cannot
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.
Analyse, compose, model, investigate
- Answer data questions in plain language with the numbers behind them
- Design a whole dashboard against a documented layout system
- Propose a star or snowflake model over a connection, or validate an existing one
- Run a fixed six-step root-cause method on an anomaly
- Read 40 connection types through the same compiler your analysts use
Per-organisation, off by default
- Master switch defaults to off, with per-persona toggles under it
- Step limits, result truncation, context size
- Daily token budget and daily session budget
- Which LLM provider, at what cost per million tokens
- Whether writes need approval — default true
Hard structural limits
- No write tool reaches a real service call — it can only stage a proposal
- No DDL or DML against your data sources; SELECT and WITH only
- No security-context access inside a tool; org and tenant scoping cannot be widened by an argument
- The Investigator has every write tool filtered out of its prompt
- The MCP surface publishes read tools only
One runtime, four jobs
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.
Read-only data analysis
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.
Dashboard designer
"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.
Dimensional modelling
"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.
Root-cause analysis
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.
A design system, encoded in the prompt
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.
| Data shape | Chart type | Grid size & placement |
|---|---|---|
| Single KPI value | NUMBER | 3 × 2 — top row |
| Time series | LINE / AREA | 12 × 4 — full width |
| Category breakdown | BAR | 6 × 4 — half width |
| Part of a whole, six slices or fewer | PIE | 6 × 4 |
| Two metrics against each other | SCATTER | 6 × 4 |
| Stage-to-stage conversion | FUNNEL | 6 × 4 |
| Row-level detail | TABLE | 12 × 5 — bottom |
KPI → trend → breakdown → detail
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.
A date range is mandatory when it matters
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.
The rulebook is published, not implied
You cannot review a proposed star schema unless you know which heuristics produced it.
How the Modeler decides
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.
- Classify the tables. Fact heuristics: high row count, many foreign keys, numeric measures. Dimension heuristics: low cardinality, descriptive columns, referenced by foreign keys. Many-to-many relationships get a bridge table rather than a quietly duplicated grain.
- Join conservatively. Prefer inner joins on primary key → foreign key. Where the join cardinality is unknown, sample the data rather than assume.
- Warn about fan-out before proposing it. A join that inflates rows silently corrupts every SUM downstream. The Modeler flags it in the proposal instead of discovering it in production.
- Pick aggregations by additivity. SUM for additive measures, COUNT(DISTINCT …) for headcounts, AVG for rates — and non-additive ratios are explicitly marked as such, so nobody rolls up an average of averages.
- Add the hierarchies that always exist. A time hierarchy whenever a date column is present, and a geographic hierarchy whenever country, region or city columns are. These are the drill paths every business asks for on day two.
- Hand it to a human. The suggestion engine runs nightly and proposes; an administrator approves or rejects. Nothing becomes a live model on a machine's say-so.
Six steps, hard caps, and a second opinion
Root-cause analysis is where language models are most persuasive and least reliable. So this one is fenced in from both ends.
The fixed method
- Confirm the anomaly. Re-measure it before explaining it. A surprising number of "incidents" do not survive this step.
- Inventory the dimensions. Establish what can be sliced at all, before slicing anything.
- Segment one dimension at a time across the anomaly window and the baseline window, flagging segments where the delta exceeds 50 % of the total movement, or sits more than 2σ away from its peers.
- Correlate adjacent metrics. Did anything else move at the same time, in the same direction?
- Historicise. Compare against the same period last week, last month and last year — the cheapest available defence against calling a seasonal pattern a crisis.
- Synthesise into the findings document, with up to three suggested follow-ups.
Bounded by construction
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.
A strict JSON findings 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.
An independent outlier check re-runs the model's claims
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.
Unattended, but harmless
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.
The write path has a single door, and a person holds the key
Not a confirmation dialog bolted on top. An architectural choke point that the tools cannot route around.
How a write actually happens
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.
- The model calls a write tool. It receives back the proposal id and the exact payload that was staged — so it can narrate honestly what it asked for, rather than claiming to have done something.
- The proposal waits. It is listed against the session, readable in full, with the payload as it will be applied. Nothing is partially applied while it waits.
- A human approves or rejects. Approval is what dispatches to the owning domain service — the same service a human clicking the same button in the UI would call, with the same permission checks.
- The session resumes. The loop can be driven further after an approval without appending an empty turn, so a multi-step build reads as one continuous conversation rather than a series of restarts.
- Defence in depth catches the future mistake. While approval is required, a write-side tool that returns success without having staged a proposal is replaced with an error. This exists to catch a tool author, a year from now, who forgets the staging call — the failure mode you cannot fix with a code review policy.
require_approval_for_writes defaults to true. An organisation can turn it off. Most should not, and the default is chosen accordingly.
Read tools look. Write tools propose.
The whole surface, named. Nothing is hidden behind a generic "run code" escape hatch.
| ReadToolNever mutates anything | What it does |
|---|---|
list_connections | Enumerate the data sources this session is scoped to. |
get_schema | Tables, columns and types from the schema cache. |
get_sample_data | A bounded sample, for when shape matters more than schema. |
describe_column | Detail on one column, before it gets used in a filter or a join. |
validate_columns | Check tables, columns and planned joins against the schema cache — returns per-name suggestions for typos and a fan-out warning when a join is not FK-backed. |
compile_qql | Turn a query document into dialect SQL without running it. |
execute_query | Run QQL or native SQL, read-only, with self-diagnostic hints for zero rows, slow runtimes and high-NULL columns. |
get_question_result | Fetch the result of an existing saved question. |
search_questions | Find questions that already answer part of the ask. |
list_dashboards | Enumerate dashboards in scope. |
get_dashboard | Read one dashboard's cards, layout and filters before changing it. |
suggest_chart | Recommend a visualisation for a result shape. |
detect_anomalies | Surface statistical outliers — the Investigator's entry point. |
get_olap_model | Read an existing semantic model definition. |
suggest_olap_model | Derive a candidate star or snowflake model from a connection. |
validate_olap_model | Check a model and return a verdict naming a specific risk. |
| Write · 13ToolEach stages a proposal. None executes. | What it proposes |
|---|---|
save_question | Persist the query just discussed as a saved question. |
save_description | Write a description onto an existing object. |
create_dashboard | Create a new dashboard shell. |
add_dashboard_card | Add a card at a given grid position and size. |
update_dashboard_card | Change a card's position, size, mappings, title override or visualisation. |
remove_dashboard_card | Take a card off a dashboard. |
update_dashboard_layout | Save a whole layout in one operation. |
update_dashboard_filters | Define or rebind the dashboard's cascade filters. |
create_alert | Set up a threshold alert on a question. |
schedule_subscription | Schedule a recurring delivery to a channel. |
share_embed | Produce an embed for an object — a write, and treated as one. |
create_olap_model | Propose a new semantic model. |
update_olap_model | Propose a change to an existing model. |
A loop that always stops, and always says why
Failure 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.
Four independent stop conditions
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.
A repetition circuit breaker
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.
A server-side tool allowlist per session
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.
Every call persisted, with the truncation flag
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.
A fallback parser for providers without native tool calling
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.
Cross-session memory that can never cost you your answer
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.
Five adapters. Any OpenAI-compatible endpoint. Including yours.
The LLM provider is a row in your own database, not a vendor relationship you inherited by installing our software.
| Adapter | Provider types | Default model |
|---|---|---|
| Claude | CLAUDE | claude-sonnet-4-6 |
| OpenAI-compatible | OPENAI, LM_STUDIO, JAN_AI, OPENCHAT, LOCAL_AI, GPT4ALL, LIBRECHAT, LOBE_CHAT, OPEN_WEBUI, MISTRAL | gpt-4o-mini |
| Azure OpenAI | AZURE_OPENAI — deployment-qualified base URL, fails loudly if blank rather than guessing | From the provider row |
| Gemini | GEMINI | gemini-1.5-flash |
| Ollama | OLLAMA — sampling nested under options, tools key omitted for older builds | llama3.1 |
Self-hosted Qrly plus a local model means nothing leaves the building
This 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.
Cost and budget live with the credential
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.
Per-organisation settings, and honest budget accounting
Everything below is an administrator setting, not a support ticket.
What an administrator turns on
- The master switch.
agents_enableddefaults to false, with per-persona toggles underneath. You enable the Analyst without enabling the Composer if that is where you want to start. - Loop limits. Max steps default 10 with a ceiling of 25; results truncate at 50 rows; context caps at 8,000 tokens. These bound the cost of a single question before any budget is consulted.
- Write safety. Approval required, default true. The one setting most organisations should never change.
- Budgets. 50,000 tokens per day and 50 sessions per day by default, plus a reset pointer that zeroes only the remainder of today — it does not rewrite or delete the session rows, so a mid-day reset does not destroy the audit trail you granted it against.
- Automation. Investigator auto-trigger and its severity floor, modeler enrichment, MCP enable and a max-concurrent-sessions cap, and a default provider override for agents specifically.
- Narration and proactive settings. Narration: enabled, auto-generate, max concurrent, refresh hours, language, and whether it is included in subscriptions. Proactive: insights enabled, schedule cron, max items, change detection and a push severity floor.
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.
The part that runs when nobody asks
Narration, seven insight detectors, anomaly detection and investigations — each with a numeric trigger you can read, argue with and change.
| 7 detectorsInsightFeeds the insights list | What fires it |
|---|---|
TREND_CHANGE | A slope reversal across three snapshots. |
MILESTONE | A round-number crossing. |
NEW_RECORD | A new maximum for the series. |
STALE_DATA | More than seven days without fresh data. |
CORRELATION | |r| ≥ 0.7 with a direction reversal between two verified questions. |
SEASONALITY | A shift of 20 % or more versus the same period last year. |
DATA_QUALITY | A row-count or metric collapse. |
One paragraph, cached, queued
One-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.
|z| > 2.5, or 25 % week-over-week
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.
An anomaly plus a question spawns a session
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.
Severity-gated, not volume-gated
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.
Common questions
Can a Qrly agent change my data or my dashboards without asking?
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.
Which LLM providers does Qrly support?
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.
Does customer data leave my building?
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.
What stops an agent from running away with my token budget?
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.
How is the Investigator persona different from a chat assistant?
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.
Is the agent layer on by default?
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.