Documentation
Finding the change somebody is asking about.
An investigation starts with a sentence, not a filter. These four turn one into the other — and then let you keep the filter.
Asking in plain language
"Who changed prices last week?" is a question; resourceType=catalog.price&from=… is a filter. The assistant turns the first into the second and then gets out of the way: what you read is the events themselves, with their ids on them, fetched by the same tenant-scoped query builder the rest of the dashboard uses.
| It produces a filter, not an answer | The output is the same shape the search form produces. Anything the model cannot express as a filter, it declines rather than approximates. |
| Your scope still applies | The filter is run through the same tenant and account limits as a typed search. A narrowed account cannot ask its way past its own boundary. |
| Reads are recorded like any other | A search the assistant ran is a search, and it appears in Who read the trail with the filter it used. |
| It is optional | No key configured means no assistant, and every other part of the dashboard works exactly as it did. The weekly digest beside it is plain arithmetic over your own last four weeks, so it needs no model at all. |
| The model can be one of yours | Set Assistant__Provider to OpenAiCompatible and Assistant__Endpoint to any endpoint speaking that shape — Ollama, vLLM, LM Studio, Azure OpenAI, OpenAI itself. A deployment that may not send a question to a third party can run the question box against a model on its own network instead of doing without one. |
Configure a provider key in Assistant__ApiKey to turn it on. Your events are not used for training and the prompt carries your question and the list of resource types in your tenant — never the records themselves. With Provider set to OpenAiCompatible the endpoint may name only a base URL; /chat/completions is appended for you.
Check it before anybody uses it
A question box that cannot reach its model says "the assistant could not be reached" and nothing more — it has to, because naming the reason on the page would hand your configuration to whoever typed the question. That leaves four failures looking identical: a revoked key, a model name the provider has retired, an endpoint in the other shape, an organisation key with no workspace. reldavi assistant-check is where the reason lives. It asks five questions — two ordinary, one Turkish, two trying to talk the model out of its job — prints the filter chosen for each, and on a failure prints the endpoint’s own words verbatim.
reldavi assistant-check --key "$RELDAVI_ASSISTANT_KEY" --provider openai --model gpt-4o
It exits non-zero only when the model could not be reached. A refusal is not a failure: a model declining to turn "show me everything" into a filter has done the thing it is there for. Add --clickhouse and --tenant to ask against your own resource types rather than the built-in sample.
Searching inside a change
Every filter in every audit product narrows by metadata: which record, which person, which day. None of them let you ask about the change itself — and the change is where the questions start. "A discount went above forty per cent." "A status moved from approved back to draft." "Somebody's salary field was touched at all."
| A property, a side, a comparison | Five operators: changed at all, is exactly, contains, is more than, is less than. Against the new value or the old one. Deliberately a small vocabulary rather than an expression language — anything richer is a query language reaching into customer data, which is a surface to build once there is a reason. |
| Two conditions make a transition | Old value is "Approved" and new value is "Draft" is the question "what got reverted", and the query API takes a list so both run at once. |
| Numbers stay numbers | A comparison reads the value as one, and a property holding "pending" is not a property holding zero — it simply does not match, rather than being counted as smaller than every threshold. |
| A comparison that cannot run is dropped | Ask for "more than quite a lot" and the condition is left out rather than emitted as something that silently matches everything. The results are the results of the conditions that could actually run. |
| The property name never reaches the statement | It arrives from a form and is passed as a parameter, which is the one injection point a query builder with no string concatenation could still have had. |
It is the most expensive condition here, because it reads a JSON column rather than an indexed one — so narrow by date or resource type alongside it where you can. The interface says so too, on the filter itself, rather than letting somebody discover it on a slow page.
Alerting on a search
An audit log somebody has to remember to read is an audit log nobody reads. Any filter you can search with becomes a standing rule: it runs every few minutes and sends what it matched to the same channels the anomaly digest uses.
| Somebody was made an administrator | iam.user · updated — the change most likely to matter and least likely to be noticed. |
| A service account touched payroll | hr.payslip · by svc-payroll — automation doing something nobody asked it to. |
| Anything was deleted from billing | billing.invoice · deleted — the events an auditor asks about first. |
| A rule has to narrow to something | One with no condition matches every event you record. The form refuses it, which is better than discovering it at three in the morning. |
| The window is "since last time" | Not "the last five minutes". A sweep that runs late, or a process that restarts, must not step over the events in between. |
| Late arrivals are waited for | Events are buffered and batched, so a window ends a few minutes behind the clock. Running right up to now would skip events that had not landed yet. |
| You are not told the same thing twice | An identical alert is not resent. One that repeats every sweep until somebody acts is one people mute, and a muted channel is worse than none. |
A rule is usually a search you have just run. Narrow the audit trail until it shows the thing you care about, then use Alert me about this — the filter carries over and you name it. The dates do not carry over: a search is about a window, a rule is about a kind of event and runs forever.
The rule’s name comes first in the payload, because the person it reaches is reading a phone and needs to know what happened before they need to know which records. The shape is flat JSON rather than any vendor’s message format, so a three-line workflow can turn it into whatever Slack, Teams or PagerDuty wants.
{
"kind": "alert-rule",
"rule": "Somebody was made an administrator",
"matchCount": 1,
"windowFrom": "2026-09-20T09:00:00Z",
"windowTo": "2026-09-20T09:05:00Z",
"matches": [{
"occurredAt": "2026-09-20T09:02:11Z",
"action": "updated",
"resourceType": "iam.user",
"resourceId": "USR-9",
"actorDisplayName": "Ayşe Yıldırım"
}]
}
Asking for an alert
The assistant turns a question into a filter and runs it once. The same filter can become a standing rule — the thing you actually wanted when you asked. "Tell me if anyone outside finance changes an invoice over fifty thousand" is a sentence; what watches for it from then on is a filter.
| You confirm the filter, not the sentence | The compiled fields are shown before anything is saved, and they are editable. What somebody meant is not enforceable; what the filter names is. |
| The dates do not carry | A search is about a window; a rule is about a kind of event and runs forever. Carrying "last Tuesday" into something permanent is how an alert quietly stops matching anything. |
| The rule remembers the sentence | Six months later somebody asks why this alert exists. "Because on 20 September somebody asked to be told if anyone outside finance changed an invoice" is an answer. A set of filter fields is not. |
| A rule that narrows nothing is refused | It would match every event you record. That is the difference between a form that says no and a pager at three in the morning. |
And if you have no model key configured, none of this is missing — the rule form is there, and Alert me about this on any search fills it in the same way. The assistant is a faster way to reach the same form, not a second mechanism.
What to read next
The schema, the SDK sources and the full architecture notes are open. If something is unclear, that is a bug in our documentation. Get in touch.