Contributing a config / check
A declarative query config is a monitoring check expressed as data — a plain TypeScript object that the dashboard validates against a Zod schema and renders as a table or card view. No JSX, no functions, no component imports.
Current status: the declarative catalog is opt-in. Set
CHM_CONFIG_SOURCE=declarativeto route runtime lookups to it. The default (CHM_CONFIG_SOURCE=ts) still uses the TypeScript query configs. A drop-in community catalog directory (where contributions live outside the repo) is planned but not yet built. For now, contributing a check means adding a file to the repo’s catalog directory and opening a PR.
The contract
Section titled “The contract”Every declarative config must satisfy the schema in:
apps/dashboard/src/lib/query-config/declarative/schema.tsRequired fields:
| Field | Type | Notes |
|---|---|---|
name | string | Unique across the catalog. Used as the lookup key. |
sql | string | VersionedSql[] | A plain SQL string, or an array of { since, sql } entries for version-aware queries. |
columns | string[] | Column names returned by the query, in display order. Must have at least one entry. |
Commonly used optional fields:
| Field | Type | Notes |
|---|---|---|
description | string | Short human-readable description shown in the dashboard. |
columnFormats | Record<string, ColumnFormat> | Display format per column. Allowed values: badge, duration, number, number-short, background-bar, colored-badge, boolean, code, text, none, and others — see schema for the full list. |
columnDescriptions | Record<string, string> | Tooltip text per column. |
defaultParams | Record<string, string | number | boolean> | Default values for {param: Type} placeholders in the SQL. |
optional | boolean | When true, the dashboard skips gracefully if the target table does not exist (default: false). |
tableCheck | string | string[] | Table(s) to check for existence before running the query. Useful with optional: true. |
refreshInterval | number | Auto-refresh interval in milliseconds. |
relatedCharts | (string | [string, Record])[] | Chart names (or [name, params] tuples) to render above the table. |
card | { primary?, badges?, metrics?, hidden? } | Card view layout hints. |
defaultView | 'table' | 'cards' | 'auto' | Default display mode. |
docs | string | Help text shown when the table is missing (a sentence with links — not required to be a bare URL). |
suggestion | string | A short suggestion/tip rendered with the view. |
columnSizing | Record<string, { size?, minSize?, maxSize? }> | Per-column width hints (pixels). |
tableBehavior | { enableColumnResizing?, enableSorting?, … } | Table interaction toggles. |
sortingFns | Record<string, SortingFn> | Named sort function per column. Allowed: sort_column_using_pct, sort_column_using_pct_inverted, sort_column_using_actual_value. |
clickhouseSettings | Record<string, string | number | boolean> | Execution-time ClickHouse settings applied per query (e.g. { allow_introspection_functions: 1 }). |
rowStyle | { rules: [{ when, className }], default? } | Conditional row CSS classes — the declarative replacement for rowClassName. See Conditional row styling. |
permission | { feature, defaultAccess?, operation? } | Feature-permission gate. See Feature permissions. |
bulkActions / bulkActionKey | string[] / string | Bulk row actions and the row key they operate on. |
Fields intentionally absent from the declarative format (runtime-only, cannot be serialized):
expandable, columnIcons, filterSchema.
If your check needs any of these, it must stay as a TypeScript config for now.
Two former exclusions are now expressible as data:
rowClassNameviarowStylerules, andpermissionvia thepermissionfield.
Safety rule: SELECT only
Section titled “Safety rule: SELECT only”All checks must be read-only. The schema validator and the SQL validator in
apps/dashboard/src/lib/api/shared/validators/sql.ts reject DDL and DML
(CREATE, INSERT, DROP, ALTER, TRUNCATE, etc.). Do not set
disableSqlValidation: true on contributed checks.
Worked example
Section titled “Worked example”A minimal check that surfaces the last 100 slow queries. Based on the real query-count-baseline pattern in the anomaly catalog:
import type { DeclarativeQueryConfig } from '../../schema'
export const slowQueriesRecentDeclarative: DeclarativeQueryConfig = { name: 'slow-queries-recent', description: 'Queries that took longer than 5 seconds in the last hour', sql: [ { since: '23.8', sql: ` SELECT query_id, user, query_duration_ms, formatReadableTimeDelta(query_duration_ms / 1000) AS readable_duration, round(memory_usage / 1048576, 1) AS memory_mb, left(query, 200) AS query_snippet, event_time FROM system.query_log WHERE type = 'QueryFinish' AND query_duration_ms > 5000 AND event_time >= now() - INTERVAL 1 HOUR ORDER BY query_duration_ms DESC LIMIT 100 `, }, ], columns: [ 'query_id', 'user', 'query_duration_ms', 'readable_duration', 'memory_mb', 'query_snippet', 'event_time', ], columnFormats: { query_duration_ms: 'number', memory_mb: 'number', query_snippet: 'code', }, columnDescriptions: { query_duration_ms: 'Wall-clock duration in milliseconds', memory_mb: 'Peak memory usage in MiB', }, defaultParams: {}, optional: false,}The field names above (name, sql[].since, sql[].sql, columns,
columnFormats, columnDescriptions, optional) map directly to
declarativeQueryConfigSchema in schema.ts.
Step-by-step: adding a check
Section titled “Step-by-step: adding a check”-
Pick the right domain directory. Existing domains under
apps/dashboard/src/lib/query-config/declarative/catalog/:anomaly,explorer,keeper,logs,merges,more,queries,security,system,tables. Add a new file to the closest domain, or create a new subdirectory if the check doesn’t fit any existing one. -
Write the config. Export a
consttyped asDeclarativeQueryConfig. Usesql: [{ since: 'X.Y', sql: '...' }]when querying a system column that was added in a specific ClickHouse version (checkdocs/clickhouse-schemas/tables/for column availability). Use a plainsql: '...'string when the query works across all supported versions. -
Register it in the catalog index. Import your export in
apps/dashboard/src/lib/query-config/declarative/catalog/index.tsand add it to theALL_DECLARATIVEarray. The index enforces unique names at module load time and throws if a duplicate is detected. -
Validate locally.
Terminal window # Validate schema + run declarative catalog testsbun run test:query-config# Biome lint (must pass before PR)bun run checkThe
test:query-configsuite runs the schema validator against every catalog entry and compares serializable fields against the legacy TS configs for migrated checks. -
Note the opt-in flag. Your check is live in the catalog but the dashboard only reads from it when
CHM_CONFIG_SOURCE=declarativeis set in the environment. The default ists. Document this in your PR if relevant. -
Open a PR against
main. The CIunit-testsjob runs the full bun test suite including the declarative catalog tests.
Version-aware SQL
Section titled “Version-aware SQL”When a system table column was introduced in a specific ClickHouse release,
use the sql array instead of a plain string. The loader picks the entry whose
since version is the highest that is still ≤ the connected cluster’s version.
sql: [ { since: '23.8', sql: `SELECT col1 FROM system.query_log LIMIT 10`, }, { since: '24.1', sql: `SELECT col1, new_col FROM system.query_log LIMIT 10`, description: 'Added new_col (available from 24.1)', },],Version strings must match X.Y or X.Y.Z (e.g. "23.8", "24.1",
"25.6.0"). The description field on each entry is optional but encouraged.
Optional tables
Section titled “Optional tables”If your check queries a table that may not exist (e.g. system.backup_log,
system.zookeeper, system.error_log), set optional: true and provide a
tableCheck:
optional: true,tableCheck: 'system.backup_log',The dashboard will surface a graceful “table not available” message instead of an error.
Conditional row styling
Section titled “Conditional row styling”To highlight rows based on their data (e.g. errored or stuck rows), use
rowStyle instead of a rowClassName function. It is an ordered list of
{ when, className } rules; the first matching rule’s className wins, and
default (or nothing) applies when no rule matches.
rowStyle: { rules: [ // first match wins — put the most severe condition first { when: { column: 'is_stuck', op: 'truthy' }, className: 'bg-red-50 dark:bg-red-950/20' }, { when: { all: [ { column: 'is_done', op: 'falsy' }, { column: 'elapsed', op: 'gt', value: 300 }, ], }, className: 'bg-amber-50 dark:bg-amber-950/20', }, ], default: '',},Condition operators:
| Operator | Meaning | Coercion |
|---|---|---|
gt / gte / lt / lte | numeric comparison against value | cell coerced with Number() |
truthy / falsy | numeric truthiness (non-zero / zero) | cell coerced with Number() |
nonempty / empty | string is non-empty / empty | cell coerced with String() |
all / any | AND / OR of nested conditions | — |
Comparison operators (gt/gte/lt/lte) require a numeric value.
className is any Tailwind class string.
Feature permissions
Section titled “Feature permissions”To gate a view behind a feature, set permission. This is plain data (no
import needed) mirroring the app’s FeaturePermission:
permission: { feature: 'queries' },// or, with explicit access/operation:permission: { feature: 'metrics', defaultAccess: 'authenticated', operation: 'read' },feature must be one of the known feature ids (overview, agent, insights,
health, queries, tables, metrics, dashboard, security, logs,
settings, cluster, operations, actions, mcp, peerdb, docs,
about). defaultAccess is public | authenticated; operation is
read | write. Both are optional.