Lint Rules
The grafana lexicon’s rules use the GRAF prefix. GRAF0xx rules read your TypeScript source during chant lint. GRAF1xx checks run after a build over the emitted dashboard JSON and provisioning files (alerting included), and over any other output file shaped like a Grafana dashboard or alerting provisioning file.
| Id | Severity | Catches |
|---|---|---|
| GRAF001 | error | a literal dashboard, datasource or library panel uid Grafana rejects, or a variable name $name can’t reference |
| GRAF002 | error | a literal secret in a datasource’s secureJsonData or in a contact point setting Grafana stores encrypted |
| GRAF101 | error / warning | a panel, query, query, ad hoc or group by variable, or annotation using a datasource no Datasource or ExternalDatasource in the build declares, or an ExternalDatasource the build’s deleteDatasources deletes, or a datasource variable whose plugin type none has (error); queries with no datasource at all, or a build that declares no datasource, so references cannot be checked (warning) |
| GRAF102 | error | a query sent to a datasource of another plugin type |
| GRAF103 | error | a $variable in a query, annotation query, title, datasource or repeat that the dashboard doesn’t declare |
| GRAF104 | error | duplicate dashboard, folder or datasource uids, datasource names, panel ids, variable names or refIds |
| GRAF105 | error / warning | a panel outside the 24-column grid (error), two panels overlapping (warning) |
| GRAF106 | error | a dashboard, folder or datasource uid Grafana rejects (a folder’s general included), or a dashboard with no title |
| GRAF107 | error / warning | a value the pinned Grafana schema doesn’t allow (error), a key it doesn’t know (warning) |
| GRAF108 | error | a panel query, query variable, annotation query or alert rule query sent to a Prometheus datasource that is not valid PromQL |
| GRAF109 | error / warning | two dashboard providers loading the same files, or a provider Grafana refuses (error); a dashboard folder a provider ignores, or a nested folder older Grafana flattens (warning) |
| GRAF110 | warning | a panel or row repeated over a variable that only ever holds one value |
| GRAF111 | error | an alert rule whose condition, expression inputs or record.from name no refId of the rule, with duplicate refIds, or with an expression model the pinned schema rejects |
| GRAF112 | error / warning | an alert rule query to a datasource the build does not declare, or whose model is for another plugin (error); a build that declares no datasource, so rule queries cannot be checked (warning) |
| GRAF113 | error / warning | a policy route or rule sending to a contact point, or naming a mute timing, the build does not declare, or a policy matcher that does not parse (error); a build that declares none, so they cannot be checked (warning) |
| GRAF114 | error | an alerting uid, title, group interval, duration or state Grafana rejects, or a rule uid, group, contact point, receiver uid, policy tree, mute timing or template declared twice |
| GRAF115 | warning | a panel unit that is neither a Grafana unit id nor a custom unit |
| GRAF116 | error | a panel query, query variable stream selector, annotation query or alert rule query sent to a Loki datasource that is not valid LogQL |
| GRAF117 | warning | a panel query sent to a Tempo datasource that is not valid TraceQL |
| GRAF118 | warning | a panel query reading a spanmetrics, servicegraph or GenAI metric no collector config in the build emits, or grouping one by a label that is not a declared dimension |
GRAF001
Section titled “GRAF001”A uid is 1-40 letters, digits, - and _. A variable name is letters, digits and _, not starting with a digit. Only literal values in source are checked; GRAF106 checks what was emitted.
GRAF002
Section titled “GRAF002”Write secrets as $__env{NAME}, $__file{/path} or ${NAME}. Grafana expands them when it reads the provisioning file, so the value is in neither the repository nor the build output. A secureJsonData object written as a named const in the same file is followed.
A ContactPoint’s receivers are checked the same way, at the settings Grafana stores encrypted for that integration type (CONTACT_POINT_SECRET_SETTINGS, from Grafana 13.2.2’s /api/alert-notifiers): a Slack url or token, a PagerDuty integrationKey, a webhook’s password, authorization_credentials, hmacConfig.secret and TLS keys, and so on. Receivers, their settings and nested settings written as named consts are followed.
GRAF101
Section titled “GRAF101”Every datasource reference in a dashboard (a panel’s, each query’s, each query, ad hoc and group by variable’s, each annotation’s) must be the uid of a declared Datasource or ExternalDatasource. Grafana’s own pseudo-datasources (-- Mixed --, -- Dashboard --, -- Grafana --) and datasource-variable refs (${ds}) are exempt. A panel with queries and no datasource anywhere is a warning: Grafana sends the queries to its default datasource.
A datasource variable offers the datasources of its pluginType. If no declared Datasource or ExternalDatasource has that type, the variable has nothing the build knows to choose from, and GRAF101 reports it.
This check joins dashboards against the datasources of the same build root (chant #1939). Declare a datasource that exists in Grafana but is provisioned elsewhere (another build root, the UI, another tool) with ExternalDatasource: the check resolves references to it, and nothing is provisioned. When a build declares no datasource at all, each dashboard that references one gets a single warning naming the uids and datasource-variable types it could not check.
A reference to an ExternalDatasource whose name the build’s DatasourceProvisioning lists in deleteDatasources (org 1), and that no Datasource provisions again, is an error: Grafana deletes it before any dashboard can use it.
GRAF102
Section titled “GRAF102”The type in a reference must match the type of the declared or external datasource it names, and a reference through a datasource variable must match that variable’s pluginType. The typed classes prevent this in TypeScript; the check covers plain refs and hand-edited dashboards. Like GRAF101 it only sees the datasources of the same build root (chant #1939), so declare the others with ExternalDatasource.
GRAF103
Section titled “GRAF103”Every $name, ${name}, ${name:format} and [[name]] in a query’s fields, a panel or row title, a datasource uid, a repeat, a query variable’s query or an annotation’s query (target, or Prometheus’s legacy expr) must be a variable on the same dashboard. Grafana’s built-ins ($__interval, $__rate_interval, $__range, anything starting with $__) need no declaration.
GRAF104
Section titled “GRAF104”Dashboard uids, folder uids, datasource uids and datasource names are unique across the build; panel ids and variable names within a dashboard; refIds within a panel. When two dashboards share a uid Grafana keeps one and drops the other without an error. Two folders get one uid when their paths slug the same (Team A and team-a); file provisioning, which gives folders uids of its own, can still load them, but the API applier cannot create both, so declare one as a Folder with its own uid.
GRAF105
Section titled “GRAF105”A panel must satisfy x ≥ 0, y ≥ 0, w ≥ 1, h ≥ 1 and x + w ≤ 24. Two panels overlapping is a warning: Grafana moves one on load, so the dashboard won’t look as declared. Panels inside a collapsed row are compared with each other, not with the rest of the dashboard. Panels without x and y are placed by the dashboard and never overlap each other.
GRAF106
Section titled “GRAF106”Grafana refuses a dashboard, folder or datasource uid outside 1-40 letters, digits, - and _, and a dashboard with no title. It also refuses the folder uid general, which names its root: a dashboard with folder: "General" would get it, so leave folder out to put a dashboard in General.
GRAF107
Section titled “GRAF107”The dashboard is validated against the dashboard schema at GRAFANA_SCHEMA_PIN, with the correction overlay applied. Each panel’s options and fieldConfig.defaults.custom, and each Prometheus, Tempo, Loki, Elasticsearch, CloudWatch, Azure Monitor, Cloud Monitoring, BigQuery or Pyroscope query, are validated against their plugin’s schema with required fields relaxed, since Grafana fills those in. A value the schema doesn’t allow (a wrong type, an enum value it doesn’t list, a missing required field) is an error. A key the schema doesn’t know is a warning: Grafana adds keys in every release, so an unknown key is more often a newer Grafana than a typo. The message names the JSON path and what the schema expected. Dashboards exported from Grafana 12.4 and 13.x pass, including the __inputs, __requires and __elements of an external export. Each library panel model in __elements is validated like a panel on the grid, so a bad one is reported at its path under /__elements/<uid>/model. See Where the types come from.
GRAF108
Section titled “GRAF108”Every panel query, query variable and annotation query whose datasource resolves to a prometheus one is parsed with the grammar the Prometheus web UI’s editor uses (@prometheus-io/lezer-promql, through the prometheus lexicon’s checkPromql). The datasource is resolved the way GRAF101 and GRAF102 resolve it: the query’s own ref, else its panel’s, a datasource variable’s pluginType, or the type on a ref to a datasource the build doesn’t declare. A query whose datasource can’t be told, one with no ref anywhere, is not parsed. LogQL and TraceQL are GRAF116 and GRAF117. Library panels in an export’s __elements are checked too. A query variable in object form (Prometheus writes { qryType, query, refId }) is checked on its query.
Grafana replaces template variables before Prometheus sees the query, so the check does too. $var, ${var}, ${var:format}, [[var]] and the $__ macros become a duration inside [...] or after offset ([$__rate_interval], [$window]), a number for $__range_s, $__interval_ms, $__from and $__to, and a name anywhere else ($metric{...}, by ($label)). Inside a quoted string they are left alone. A query variable’s label_values(selector, label), label_names(selector) and query_result(expr) are checked on the selector or expression; metrics(regex) and bare label_values(label) hold no PromQL.
It is a syntax check: sum(x{cluster="$cluster"} (a missing bracket) and [5 m] fail, rate over an instant vector parses. The message gives the offset in the query as written.
Alert rule queries are parsed too: a query whose datasourceUid is a declared prometheus datasource, or whose model says datasource.type: prometheus, has its expr checked the same way.
GRAF109
Section titled “GRAF109”Read from the dashboard provisioning file and where each dashboard file sits under dashboards/.
- Two providers in the same org whose paths are the same, or one inside the other, are an error. Grafana walks a provider’s path recursively, so both load every dashboard under it; it then logs the duplicate uids and takes database writes away from both providers, so later changes to the files never reach Grafana. Give each provider its own path.
- A provider with
foldersFromFilesStructure: trueand bothfolderandfolderUidis an error: Grafana refuses to start it. With only one of them it is a warning, since Grafana files dashboards by their directories and does not use that folder. - Dashboards that declare a
folderwhen no provider setsfoldersFromFilesStructureare a warning: each provider puts every dashboard it loads in its ownfolder(or General), and the dashboards’ folders are ignored. A declaredDashboardProviderwithfolderorfolderUidturnsfoldersFromFilesStructureoff unless it is set. - A nested folder,
folder: "Platform/Kubernetes", is written asdashboards/Platform/Kubernetes/. Grafana 13.1 and later create a Kubernetes folder inside Platform; Grafana 12.4 and 13.0 use only the last directory and put the dashboard in a top-level Kubernetes folder, so it is a warning. Grafana nests at most 4 levels by default ([folder] max_nested_folder_depth, at most 7), and a provider stops with an error on a deeper directory: the message says so past 4, and past 7 it is an error.
GRAF110
Section titled “GRAF110”Grafana repeats a panel or row once per selected value of the variable its repeat names, and only over a variable that holds a list of values: a query, custom, datasource or group by variable. Repeated over an ad hoc, constant, interval, textbox or switch variable, the panel shows once and Grafana logs an error in the browser console. Repeated over a query, custom or datasource variable with neither multi nor includeAll, it shows once, since only one value can be selected. Both are warnings: the dashboard loads, it just doesn’t repeat. Set multi: true or includeAll: true on the variable, or repeat over another one. A repeat that names no variable of the dashboard is GRAF103.
GRAF111
Section titled “GRAF111”Within one alert rule, the condition, each expression’s inputs (the expression of a reduce, threshold or resample, every $A or ${A} in a math expression, each classic condition’s query.params[0]) and a recording rule’s record.from must name a refId of that rule, and no two queries may share a refId. An alert rule needs a condition; a recording rule needs record.metric. Each server-side expression model is validated against the pinned expr schema, as written, so a missing reducer or an evaluator type Grafana doesn’t know is reported at its field. Grafana refuses such a rule, and one refused rule stops it provisioning every alerting file.
GRAF112
Section titled “GRAF112”Each alert rule query’s datasourceUid must be a datasource the build declares, provisioned (Datasource) or external (ExternalDatasource), and a model that states its datasource type must match the declared type. A recording rule’s targetDatasourceUid must be a declared Prometheus datasource. Like GRAF101 it only sees the build root’s datasources; with none declared it warns once, naming the uids it could not check.
GRAF113
Section titled “GRAF113”The notification policy tree’s receivers (the root’s and every route’s), each rule’s notification_settings.receiver, and every mute_time_intervals and active_time_intervals entry must name a ContactPoint or MuteTiming the build declares. grafana-default-email exists in every Grafana and is always accepted. Object matchers must be [label, op, value] with op =, !=, =~ or !~ and a regex that compiles; matchers strings must parse as Alertmanager matchers. With no contact point or mute timing declared at all, it warns once that it cannot check, since they may exist in Grafana already.
GRAF114
Section titled “GRAF114”What Grafana’s file provisioner refuses: a rule or contact point receiver uid outside 1-40 letters, digits, - and _; a rule with no title or one over 190 characters; a group with no name or folder, or an interval that is not a positive multiple of 10s (Grafana’s evaluation tick); a for or keepFiringFor that is not a duration; a noDataState or execErrState Grafana doesn’t know. A contact point receiver’s settings are checked against the options Grafana lists for its integration: a required option left out is an error, and a key the integration does not take is a warning naming the nearest setting. Integrations without a table are not checked. And what it keeps only one of, per organisation: rule uids, group names within a folder, contact point names, receiver uids, mute timing and template names, and policy trees.
GRAF115
Section titled “GRAF115”A field’s unit is a free string in Grafana’s schema, so GRAF107 accepts anything. Grafana looks the string up in its unit registry; an id it doesn’t have is drawn after the value as literal text, so "byte" renders 5 byte where "bytes" renders 5 B, and "cores" renders 2 cores when you may have wanted "none" or "short". GRAF115 checks each panel’s fieldConfig.defaults.unit, every override property with id unit, a heatmap’s options.yAxis.unit and options.cellValues.unit, and a legacy graph panel’s yaxes[].format, including library panels in an export’s __elements.
The accepted ids are the ones registered in Grafana v13.2.2’s categories.ts, plus the legacy alias farenheit that valueFormats.ts still resolves. They are vendored in src/spec/units.gen.ts; just fetch-units re-extracts them. A custom unit passes whatever follows its prefix, exactly as Grafana parses it:
| Syntax | Renders |
|---|---|
suffix:<text> | the value, then the text |
prefix:<text> | the text, then the value |
si:<scale><unit> | an SI-scaled unit, e.g. si:mF |
count:<unit> | a scaled count, e.g. count:reqs |
currency:<symbol>, currency:financial:<symbol>[:suffix] | a currency amount |
time:<format> | a date, e.g. time:YYYY-MM-DD |
bool:<true>/<false> | text for true and false |
It is a warning because the dashboard still renders. The message names the closest registered id when one is a case change or a couple of letters away ("Bytes", "celcius"), and says to write suffix:<text> if the text was meant.
GRAF116
Section titled “GRAF116”Every panel query, annotation query and alert rule query whose datasource resolves to a loki one, and the stream selector of a Loki query variable (label_values({app="x"}, pod), or the stream of the object form), is parsed with the grammar behind Grafana’s Loki query editor (@grafana/lezer-logql). Queries are routed the way GRAF108 routes PromQL, library panels in __elements included, and template variables are substituted the same way. A variable can stand for more than a name in LogQL ({app="x"} $filters with a textbox holding a whole pipeline stage), so a parse error at or right after a variable is not reported.
It is a syntax check: {app="x" (an unclosed selector), rate({app="x"}[1]m) and a pipeline stage Loki doesn’t have fail. The message gives the offset in the query as written.
GRAF117
Section titled “GRAF117”Every panel query whose datasource resolves to a tempo one and whose queryType is traceql is parsed with the grammar behind Grafana’s Tempo query editor (@grafana/lezer-traceql). A query with no queryType is parsed unless it is a trace id; search, service graph and trace id queries hold no TraceQL. Template variables are substituted as for GRAF108, and an error at a variable is not reported.
GRAF117 is a warning because Grafana’s TraceQL grammar trails Tempo’s own parser: syntax newer than the grammar (query hints such as { } with (sample=true)) is flagged though Tempo runs it. Check such a query against Tempo before changing it.
GRAF118
Section titled “GRAF118”The prometheus lexicon’s PROM301, run over dashboards. Every panel query and query variable that reaches a prometheus datasource, with template variables substituted as for GRAF108, is read against the collector configs in the build: the otel lexicon’s own, rebuilt from the build’s otel entities, and any config in the output or in a ConfigMap. A selector whose name falls under a namespace a spanmetrics, servicegraph, or GenAI sum or signaltometrics connector owns, or under the spanmetrics and servicegraph defaults, must be a name a config emits. A by (...) label over such metrics must be a declared dimension, job, instance, an otel_scope_* label, or le on a _bucket series. GRAF118 is silent when the build has no collector config, and for names outside those namespaces. See the prometheus lexicon’s lint rules for what it reads.