# KQL Lens > Paste one Kusto Query Language query — from Azure Monitor / Log Analytics, Application Insights, > Azure Data Explorer, Microsoft Sentinel or Azure Resource Graph — and get it reviewed, rewritten, > or explained stage by stage. A real KQL reader runs first, for free, in the browser: it splits the > pipeline the way the engine does and reports what it found before any model is involved. Derived > from @microsoft/azure-kusto. KQL Lens exists because the cost and the correctness of a Kusto query are decided almost entirely by the ORDER of its stages, and that is invisible in a code review. It is used at the moment somebody is about to run a query against real data, pin one into a workbook that refreshes all day, or take over a query whose author has left. ## The three lanes — one work object, three jobs Every lane takes the same paste: one KQL query, optionally its table schema. The app sends a `task` field naming the lane. - **`review`** — what is wrong with this query, stage by stage. A verdict of `ship`, `tune` or `rewrite`; a scan-risk reading; severity-ranked findings each quoting the exact stage with a drop-in corrected fragment; the nine checks; a reconciliation of the free browser read. - **`optimize`** — the query to run instead, written out in full and runnable as pasted, plus one note per change saying what that change buys. The rewrite is re-read by the same in-browser reader before it is shown, so a rewrite that reintroduced a problem is named rather than shipped quietly. - **`explain`** — what an inherited query actually answers: what one row of the output means, then one entry per pipeline stage with its purpose, the columns it reads, and the traps. Handoff is a button. A review offers "rewrite this query" and "explain it stage by stage"; a rewrite offers "review the rewrite", which puts the model's own output back into the box so the second run grades what the first produced. ## The nine checks Every lane answers exactly these, in this order, each with the evidence that decided it: time window, filter order, term index use, column pruning, join semantics, result bound, correctness hazards, schema grounding, scan volume. Status is `pass`, `fail`, `partial` or `unknown` — and the difference between the last two matters more here than anywhere else in the app. **A check whose evidence is the query text is allowed to fail definitively.** If nothing in the pasted pipeline bounds the time range, that is a `fail`: a stage that was not pasted cannot reach into a stage that was, so the absence is proof. **A check whose evidence is the schema is `unknown` when no schema was pasted** — whether a column exists, and how much data a window covers, are not things a query text can settle. Exactly two checks are in that second category (schema grounding, scan volume), and the downgrade is enforced in one function so it cannot drift between the free lane and the rendered result. ## The free in-browser read Not a keyword scan. The reader: - classifies every character as code, line comment or string first, so a `//` inside a string is not a comment and a `|` inside a string is not a stage boundary; - handles all four KQL string forms, including verbatim `@'…'` and `@"…"` where a backslash is not an escape; - splits statements on `;` and stages on `|` only at bracket depth zero, so a pipe inside `join (T | where …)` never splits a stage; - resolves one level of `let` bindings, so `let lookback = 30d; T | where TimeGenerated > ago(lookback)` reads as bounded rather than as a missing time filter; - extracts the tables actually read, including through `union`, `join`, `lookup` and `cluster()`. It then raises up to twenty-four deterministic findings, each quoting its stage: no time filter, a time filter that runs after expensive stages, absolute datetimes, `search` with no table scope, `contains` where `has` would use the term index, `tolower()` applied to a column inside a filter, `join` with no `kind=` (the implicit `innerunique` that silently de-duplicates the left side), an unfiltered join right side, a wildcard `union`, an unbounded result, `sort` before an aggregation that discards the order, `sort` then `take` where `top` does both, `take` before `sort` (which ranks an arbitrary sample), JSON parsed before the row set is reduced, `matches regex` where a term operator would do, missing column pruning before a join, `distinct` on a high-cardinality column, an unused `let`, a cross-cluster reference, `render timechart` with no `bin()`, unbounded `mv-expand`, inline `datatable`/`externaldata` test data, `arg_max(…, *)`, and an `extend` whose computed column is read only by the `where` that follows it. ## Masking runs before the read, not before the send A pasted KQL query is a reliable way to leak a tenant: subscription and workspace GUIDs, cluster URIs, the email address of whoever was being investigated, the IP that was blocked. Masking is therefore applied *before* the query is parsed, so the stage quotes, the findings, the facts and the context box are all derived from already-masked text and no field can carry a raw value by construction. The assembled request is then re-checked for every value the masker replaced, and the run button is **disabled** — not merely flagged — if one survived. Placeholders are stable per distinct value, so a query that joins on the same GUID twice still joins on the same token. The restore is the other half of the same idea. The rewritten query is headed "the query to run instead", and a query that still says `` is not one anyone can run — so copy and download put the real names back **in the browser, on the way out**, under a checkbox that says so. The model only ever saw the placeholders; the clipboard gets a query that works against the user's own cluster. A run reloaded from history no longer holds the masker that did the replacing, so the restore is unavailable there and the page says that rather than silently exporting placeholders. ## Input clipping is stage-aware A query is a pipeline, so `slice(0, MAX)` removes exactly the stages that decide the answer — the aggregation, the bound and the render all live at the end. Whole stages are removed from the MIDDLE instead, with the source and the first filters kept, the tail kept, and an in-band `// [... N stages removed ...]` marker so the model knows its view is partial. ## What it does not do It never connects to a cluster and never sees data. It cannot tell you how many gigabytes a query scans or what it costs in dollars — it has no table statistics, so it describes the shape of the cost and reports `unknown` where a number would be a guess. A rewrite is a suggestion to read before you run it, not a validated query plan. ## Cost and access Metered: a lane run costs credits and needs a signed-in SkillSafe account. The in-browser read, both bundled example queries, and a saved run for every lane of both examples are free and need no account. `POST /v1/app-api/estimate` prices a lane without charging anything. ## Programmatic use `https://api.skillsafe.ai/v1/app-api` with a token from `POST /v1/app-api/guest` (body `{"slug": "kql-lens"}`) or SSO. Send `task`, `query`, and optionally `schema`, `platform`, `goal`, `context` and `prescan_facts`. See https://kql-lens.skillsafe.ai/api.html for a worked example per lane in eight languages. ## Source Derived from **@microsoft/azure-kusto** (https://skillsafe.ai/skill/@microsoft/azure-kusto/), an agent skill for querying and analysing data in Azure Data Explorer and Log Analytics with KQL. This is a derived work and is not affiliated with or endorsed by that skill's author, by Microsoft, or by Azure.