The Agent Layer

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 they 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
What you control

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
What they cannot

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
01 / Analyst

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.

02 / Composer

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.

03 / Modeler

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.

04 / Investigator

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.

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
Grid 12 columns, with column and size bounds enforced Never more than 12 cards Always set a title override
Narrative ordering

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.

The filter rule

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.

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.

  1. 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.
  2. Join conservatively. Prefer inner joins on primary key → foreign key. Where the join cardinality is unknown, sample the data rather than assume.
  3. 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.
  4. 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.
  5. 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.
  6. 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.

The fixed method

  1. Confirm the anomaly. Re-measure it before explaining it. A surprising number of "incidents" do not survive this step.
  2. Inventory the dimensions. Establish what can be sliced at all, before slicing anything.
  3. 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.
  4. Correlate adjacent metrics. Did anything else move at the same time, in the same direction?
  5. Historicise. Compare against the same period last week, last month and last year — the cheapest available defence against calling a seasonal pattern a crisis.
  6. Synthesise into the findings document, with up to three suggested follow-ups.
Caps

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.

Output

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.

The validator

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.

Trigger

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.

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.

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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 Tool Never 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 · 13 Tool Each 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.
Remember Approval dispatches to the owning domain service The tool itself has no path there The MCP surface publishes no write tools at all
01 / Termination

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.

02 / Loops

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.

03 / Scope

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.

04 / Audit

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.

05 / Compatibility

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.

06 / Memory

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.

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
Fallback Unrecognised provider types fall back to the OpenAI adapter Shared HTTP client: 10 s connect, 2 min request API keys decrypted per call
Sovereignty

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.

The provider row

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.

What an administrator turns on

  1. The master switch. 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.
  2. 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.
  3. Write safety. Approval required, default true. The one setting most organisations should never change.
  4. 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.
  5. 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.
  6. 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.

7 detectors Insight Feeds 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.
Feed Unread badge, dismiss, mark read Promote any insight to a real alert Push honours the org severity floor and per-user preferences
Narration

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.

Anomalies

|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.

Investigations

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.

Push

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.

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?

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.

An AI layer your security review can actually pass

Off by default. Read-only by construction. Every write a proposal. And a model that can run entirely inside your own network.