Skip to content

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=declarative to 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.


Every declarative config must satisfy the schema in:

apps/dashboard/src/lib/query-config/declarative/schema.ts

Required fields:

FieldTypeNotes
namestringUnique across the catalog. Used as the lookup key.
sqlstring | VersionedSql[]A plain SQL string, or an array of { since, sql } entries for version-aware queries.
columnsstring[]Column names returned by the query, in display order. Must have at least one entry.

Commonly used optional fields:

FieldTypeNotes
descriptionstringShort human-readable description shown in the dashboard.
columnFormatsRecord<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.
columnDescriptionsRecord<string, string>Tooltip text per column.
defaultParamsRecord<string, string | number | boolean>Default values for {param: Type} placeholders in the SQL.
optionalbooleanWhen true, the dashboard skips gracefully if the target table does not exist (default: false).
tableCheckstring | string[]Table(s) to check for existence before running the query. Useful with optional: true.
refreshIntervalnumberAuto-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.
docsstringHelp text shown when the table is missing (a sentence with links — not required to be a bare URL).
suggestionstringA short suggestion/tip rendered with the view.
columnSizingRecord<string, { size?, minSize?, maxSize? }>Per-column width hints (pixels).
tableBehavior{ enableColumnResizing?, enableSorting?, … }Table interaction toggles.
sortingFnsRecord<string, SortingFn>Named sort function per column. Allowed: sort_column_using_pct, sort_column_using_pct_inverted, sort_column_using_actual_value.
clickhouseSettingsRecord<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 / bulkActionKeystring[] / stringBulk 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: rowClassName via rowStyle rules, and permission via the permission field.


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.


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.


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

  2. Write the config. Export a const typed as DeclarativeQueryConfig. Use sql: [{ since: 'X.Y', sql: '...' }] when querying a system column that was added in a specific ClickHouse version (check docs/clickhouse-schemas/tables/ for column availability). Use a plain sql: '...' string when the query works across all supported versions.

  3. Register it in the catalog index. Import your export in apps/dashboard/src/lib/query-config/declarative/catalog/index.ts and add it to the ALL_DECLARATIVE array. The index enforces unique names at module load time and throws if a duplicate is detected.

  4. Validate locally.

    Terminal window
    # Validate schema + run declarative catalog tests
    bun run test:query-config
    # Biome lint (must pass before PR)
    bun run check

    The test:query-config suite runs the schema validator against every catalog entry and compares serializable fields against the legacy TS configs for migrated checks.

  5. Note the opt-in flag. Your check is live in the catalog but the dashboard only reads from it when CHM_CONFIG_SOURCE=declarative is set in the environment. The default is ts. Document this in your PR if relevant.

  6. Open a PR against main. The CI unit-tests job runs the full bun test suite including the declarative catalog tests.


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.


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.


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:

OperatorMeaningCoercion
gt / gte / lt / ltenumeric comparison against valuecell coerced with Number()
truthy / falsynumeric truthiness (non-zero / zero)cell coerced with Number()
nonempty / emptystring is non-empty / emptycell coerced with String()
all / anyAND / OR of nested conditions

Comparison operators (gt/gte/lt/lte) require a numeric value. className is any Tailwind class string.


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.