Writing board SQL
Every widget is one SQL query. Sarvam’s analytics store is ClickHouse — column-oriented and tuned for aggregation over large call datasets, so some functions and conventions differ from PostgreSQL or MySQL.
This page covers what the API enforces. For dialect patterns and worked query shapes, see SQL best practices.
Start from the schema
Call Get query schema before writing anything. It returns the tables available to this workspace with their columns and ClickHouse types:
Table names are unqualified — use them exactly as returned, with no database prefix. Referencing anything outside this catalog fails with FORBIDDEN_TABLE.
Do not write org_id or workspace_id predicates. Tenant scoping is applied server-side on every query and cannot be bypassed or opted out of. Adding your own scoping clause is at best redundant; results are additionally verified before they are returned, and a response that would cross tenants is refused with TENANT_ISOLATION_BREACH.
Read-only, single statement
Widget SQL must be exactly one SELECT. CTEs, subqueries, joins, and unions are all fine as long as they only read.
These are rejected at save time, not at run time:
SETTINGS, FORMAT, and INTO OUTFILE are rejected anywhere in the statement, including inside a CTE, a subquery, or one branch of a union.
A plain LIMIT n is allowed and can narrow a result, but never widens it: on preview and widget runs it is clamped to 1000.
Filter tokens
Tokens let one query serve many views without being rewritten.
With no agent_id supplied, that query runs across every agent. With one, it narrows to that agent. start_date is required either way.
Two things worth knowing:
- Your SQL is validated twice — once with every optional block kept and once with all of them removed. A query that only parses when a filter happens to be present is rejected when you save it, not at run time when the filter is absent. A dangling
WHEREleft by a removed block is the usual cause. [[ ]]is only special when it contains a token. ClickHouse nested-array literals like[[1,2],[3,4]]pass through untouched.
Declaring filters is optional
Writing the token into your SQL is enough. When you create or update a widget, every {{token}} in its SQL that the board does not already have a filter for gets one created automatically, labelled with the token name, typed string, with no default.
The filters array on Create widget exists only to give those filters better metadata up front:
Send it when you want a readable label, a non-string type, a default value, or dropdown options — filter_type is one of number, string, date, or dropdown, and a dropdown must also carry dropdown_options. Omit it and you can set the same things later with Update filter.
Auto-creation never overwrites a filter that already exists, so a token shared by several widgets keeps whatever metadata it was given the first time, and re-saving a widget will not undo your edits.
A filter whose default_value is null and which is used outside an optional block is required — the is_required field on List filters tells you which ones a run must supply. This is why a widget saved with no filters array at all produces required, untyped filters: nothing has given them a default yet.
Supply values at run time keyed by token name, with the declared type alongside the raw value:
Query budget
Each run executes under fixed server-side limits:
Run tab takes no row_limit — the per-widget cap there is fixed and not caller-settable.
Error codes
SQL and execution failures return detail as an object with a code:
A CLICKHOUSE_ERROR means your SQL passed validation but failed on execution, so it is almost always a column name or a type problem. PARSE_ERROR means it never reached the database.