ObservabilityXXVIII · Grafana VariablesGrafanaVariables
Variable Types
What you'll learn
- Name the seven variable kinds Grafana 11 offers and the role of each
- Choose the right kind for a given use: query for label dimensions, custom for static lists, interval for step knobs, textbox for free-form filters
- Configure the fields-hub (includeAll, multi, allValue, skipUrlSync, refresh, hide) correctly for each kind
- Predict the refresh behaviour of each kind when the time-range picker changes
- Recognise the production failure shape of a constant used as a filter and of an interval variable with no All-value
Prerequisites
Verified against Prometheus 2.55.x · Alertmanager 0.28.x · node_exporter 1.8.x · blackbox_exporter 0.26.x · Grafana 11.x · Loki 3.x · Tempo current · OpenTelemetry Collector 0.110.x · Grafana Alloy current · Docker Engine 28.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-13
The on-call engineer paged the wrong team. The dashboard they
were looking at was for production, not staging, but the
operator had no idea which environment the URL pointed at.
The variable at the top of the dashboard said prod but the
value was a free-text field. The operator typed production,
missed, and got the wrong data.
This is what the variable-type taxonomy exists to prevent.
Grafana 11 has seven kinds of variable, and each one
constrains the values the viewer can pick. A query variable
returns what the data source returns; a custom variable
returns what the author typed; a constant variable is a
single fixed value; a datasource variable is the name of a
data source; an interval variable is a time step; a
textbox variable is whatever the viewer types; and a
system variable is set by Grafana itself.
What it is
A variable kind is the discriminator that determines how
a variable’s value list is computed. It is set at variable
creation and visible in Settings > Variables > Type. The
seven kinds:
+----------------+-------------------------------------------------+
| Kind | Value list source |
+----------------+-------------------------------------------------+
| query | Run an expression against a data source; |
| | return the resulting label values. |
+----------------+-------------------------------------------------+
| custom | A static, comma-separated list defined by |
| | the author. No data source query. |
+----------------+-------------------------------------------------+
| constant | A single value defined by the author. Hidden |
| | by convention; the value is the variable. |
+----------------+-------------------------------------------------+
| datasource | List of every data source of a given type |
| | configured in the Grafana instance. |
+----------------+-------------------------------------------------+
| interval | A list of time steps (1m, 5m, 15m, 1h, 6h, |
| | 12h, 1d, 7d). Used as $__interval step. |
+----------------+-------------------------------------------------+
| textbox | Free-form text typed by the viewer. No |
| | validation. |
+----------------+-------------------------------------------------+
| system | Grafana-supplied values: $__from, $__to, |
| | $__interval, $__range, $__org, $__user. |
+----------------+-------------------------------------------------+
The kind is not a style choice; it constrains the operator’s
behaviour at view time. A constant variable cannot be
changed from the dropdown; a textbox variable accepts
anything; a query variable refreshes from the data source.
Why a sysadmin cares
Each kind encodes a different operational guarantee. Picking the right one is the difference between a dashboard that self-corrects and one that lets an operator type a typo into production.
- Drift-free filtering. A
queryvariable onlabel_values(up, cluster)always reflects what the data source knows. The dashboard cannot drift from reality while the data source is healthy. - Stable, named values. A
customvariable for the list of business services guarantees that the operator picks from a known list, not whatever they remember the name to be. - Immutable per-dashboard constants. A
constantvariable pins a value (the dashboard name, the region, the team). The viewer cannot override it; URL pinning has no effect. - Multi-cluster fan-out. A
datasourcevariable lets a panel switch between ten Prometheus instances without editing panel JSON. - Step-appropriate rate windows. An
intervalvariable on$__intervalgives the right denominator for arate()query; anintervalon$__rate_intervalgives the right step with sub-queries and rules.
The wrong choice shows up as a typoed query that returns a blank panel, or as a fixed constant that drifts as the world changes.
The fields-hub
Every kind shares a common fields block visible in the variable editor:
+----------------+-------------------------------------------------+
| Field | Purpose |
+----------------+-------------------------------------------------+
| name | The identifier used in $name interpolation. |
| | Must be unique per dashboard. |
+----------------+-------------------------------------------------+
| label | The dropdown label the viewer sees. |
+----------------+-------------------------------------------------+
| description | Tooltip and runbook excerpt. |
+----------------+-------------------------------------------------+
| hide | Hide the variable from the dropdown; still |
| | available for $name interpolation. |
+----------------+-------------------------------------------------+
| skipUrlSync | Do not write this variable into the URL. |
| | Useful for tab-specific settings. |
+----------------+-------------------------------------------------+
| includeAll | Show an "All" pseudo-value alongside the data. |
+----------------+-------------------------------------------------+
| allValue | The string the "All" pseudo-value expands to |
| | when interpolated. Typically ".*" for regex. |
+----------------+-------------------------------------------------+
| multi | Allow selecting more than one value. |
+----------------+-------------------------------------------------+
| refresh | 0 (never), 1 (on dashboard load), 2 (on time- |
| | range change). |
+----------------+-------------------------------------------------+
| current | Persisted selection; survives reloads. |
+----------------+-------------------------------------------------+
The fields-hub is what turns a variable into a working piece of UI. Skipping it produces a variable that works for the author and surprises everyone else.
Regen on time change
The refresh field has three values:
0— the variable’s value list is computed once at dashboard load and never refreshed. Acustomvariable never refreshes; aconstantvariable has no value list to refresh; aqueryvariable withrefresh: 0is a cached value.1— refresh on every dashboard load. The default forqueryvariables. Cost is amortised across panels.2— refresh on every time-range change. This is the rare setting that produces aqueryvariable whose value list shrinks when the time range narrows. Useful forlabel_values(up[1h], cluster)to keep the dropdown to recently-active clusters.
The system kind is special: its values are not refreshed
at all. $__interval is computed from the time range and
panel width; $__from and $__to are the time-range picker.
The All-value and the multi-value
Two pseudo-values appear in every kind that has a list:
- All. The first row of the dropdown when
includeAll: true. Selecting All writesvalue="$__all"to the URL. When interpolated, it expands toallValue(default empty string, recommended.*forqueryvariables that use=~). - Multi. When
multi: true, the viewer can pick more than one value. The URL value isvalue1,value2,.... When interpolated into a regex, the values becomevalue1|value2|....
A correct multi query variable has a panel-side
cluster=~"$cluster" to match the comma-list. A multi
without =~ (using =) is the most common multi-value
bug; only one value matches.
How it works
The seven kinds resolve values through different paths:
+----------------+
| query | data source call --> label list --> sort/dedup
+----------------+
+----------------+
| custom | author-defined list (read from JSON)
+----------------+
+----------------+
| constant | author-defined single value (read from JSON)
+----------------+
+----------------+
| datasource | grafana-server in-memory list of
| | /api/datasources filtered by type
+----------------+
+----------------+
| interval | grafana-server hard-coded list
+----------------+
+----------------+
| textbox | free-form viewer input (no validation)
+----------------+
+----------------+
| system | computed per render from request context
+----------------+
The query kind is the only one that makes an external call.
The cost is bounded by the refresh setting and by the
chain dependencies.
How to configure it
Below is a portable, idiomatic templating.list showing
the four kinds you will use most often. The fifth and sixth
(interval, textbox) get their own snippets.
{
"templating": {
"list": [
{
"name": "cluster",
"label": "Cluster",
"type": "query",
"datasource": { "type": "prometheus", "uid": "prom-prod" },
"query": "label_values(up{job=\"kube-state\"}, cluster)",
"refresh": 1,
"includeAll": true,
"allValue": ".*",
"multi": true,
"sort": 1,
"current": { "selected": true, "text": "All", "value": "$__all" }
},
{
"name": "env",
"label": "Environment",
"type": "custom",
"query": "prod,staging,dev",
"current": { "selected": true, "text": "prod", "value": "prod" },
"includeAll": false,
"multi": false
},
{
"name": "team",
"label": "Team",
"type": "custom",
"query": "platform,payments,frontend",
"current": { "selected": false, "text": "platform", "value": "platform" }
},
{
"name": "region",
"label": "Region",
"type": "constant",
"query": "eu-west-1",
"current": { "selected": true, "text": "eu-west-1", "value": "eu-west-1" },
"hide": 0
},
{
"name": "ds",
"label": "Datasource",
"type": "datasource",
"query": "prometheus",
"current": { "selected": true, "text": "prom-prod", "value": "prom-prod" }
}
]
}
}
For a time-step knob and a free-form filter:
{
"name": "step",
"label": "Step",
"type": "interval",
"query": "1m,5m,15m,1h,6h,12h,1d,7d",
"current": { "selected": true, "text": "15m", "value": "15m" }
},
{
"name": "search",
"label": "Search",
"type": "textbox",
"current": { "selected": true, "text": "", "value": "" },
"options": { "current": { "selected": true, "text": "", "value": "" } }
}
Three details worth highlighting:
queryof thedatasourcevariable is the type of data source, not the UID. The dropdown lists every data source of that type configured in Grafana.constanthas no UI affordance for changing the value; the value is thequeryfield. Aconstantis for immutable per-dashboard facts (region, cluster, team).textboxis the only kind without a value list. The viewer is typing into a string; validation is the panel query’s job.
How to validate it
Three checks: one on the JSON, one on the wire, one in the URL.
Severity: READ-ONLY.
# 1. The JSON declares the expected kind and the expected
# query field for that kind.
curl -s -u admin:$ADMIN \
https://grafana.example.com/api/dashboards/uid/svc-overview \
| jq '.dashboard.templating.list[]
| {name, type, query, refresh, includeAll, allValue}'
{
"name": "cluster",
"type": "query",
"query": "label_values(up{job=\"kube-state\"}, cluster)",
"refresh": 1,
"includeAll": true,
"allValue": ".*"
}
{
"name": "env",
"type": "custom",
"query": "prod,staging,dev",
"refresh": 0,
"includeAll": false,
"allValue": null
}
{
"name": "region",
"type": "constant",
"query": "eu-west-1",
"refresh": 0,
"includeAll": false,
"allValue": null
}
{
"name": "ds",
"type": "datasource",
"query": "prometheus",
"refresh": 0,
"includeAll": false,
"allValue": null
}
# 2. The query variable returns a value set that matches
# what the dashboard expects. A label that no longer
# exists in Prometheus returns an empty dropdown.
curl -G -s http://prometheus:9090/api/v1/query \
--data-urlencode 'query=label_values(up{job="kube-state"}, cluster)' \
--data-urlencode 'time='$(date +%s) \
| jq '.data.result | map(.metric.cluster) | length'
# 4
# 3. The dropdown lists each value. Open the dashboard,
# click the cluster dropdown, and confirm four cluster
# names are present. The All-value is the first row.
xdg-open "https://grafana.example.com/d/svc-overview"
How it can fail
Six failure shapes appear repeatedly with the variable-type taxonomy:
textboxused as a filter. The viewer typesprod,stagingand the panel query doesenv="$env", matching the literal string. The panel shows nothing. Symptom: the panel is empty whenever the viewer picks more than one value.constantused as a runtime filter. The author intends the value to be selectable butconstanthides the dropdown. Symptom: the dropdown is absent; the value cannot change; the URL has novar-region.queryvariable with a stale expression. The label is renamed in Prometheus but the variable query still references the old label. Symptom: the dropdown is empty; the panel cannot render; the dashboard shows a “Templating init failed” badge.datasourcevariable pinned to a deleted data source. A data source is removed via provisioning but a panel still references$ds. Symptom: every panel shows “Data source not found”.intervalwithout an explicitquery. Grafana 11 uses a default step list whenqueryis empty. The default is fine for normal cases; for a panel that wants5s,30s,1m,5m,15m,1h, the author must set it explicitly. Symptom:$__intervalis too coarse for rate calculations on sub-minute data.multiwithout a regex matcher. Amulti: truevariable withcluster="$cluster"matches exactly one value. Symptom: selecting two clusters blanks the panel; the legend shows “No data”.
How to troubleshoot it
The diagnostic order:
- Inspect the variable. Settings > Variables. The
Typedropdown must match the use. - Click Run query. A failed query is the obvious
failure shape; an empty list with no error is the
subtle one. Cross-check the
queryfield against the data source’s own query interface. - Confirm the refresh policy.
refresh: 2on aqueryvariable that depends on a time-windowed expression can produce different lists at different time ranges; verify by switching the time-range picker. - Check the chain. A
queryvariable that depends on anotherqueryvariable may resolve differently if the parent variable’s value list has changed. - Inspect the panel’s data source. A
datasourcevariable that points to a missing UID is a silent failure. The dropdown lists the right data sources; the panel UID has drifted.
Security implications
queryvariables run with the data source’s permissions. The query does not carry the viewer’s identity; it runs as the data-source plugin. This is normally fine; be aware that a dashboard author can drive a query that the viewer would not be authorised to author directly.- **
textboxvariables are unvalidated input. Atextboxinterpolated into PromQL is escaped by Grafana’s query builder, but the meaning of the text is not validated. A viewer can type.*to match every value, which is not a security exploit but is an operational surprise. datasourcevariables list every data source of the type the viewer can see. A viewer with limited data-source access still sees only the data sources they have access to. This is enforced by Grafana’s RBAC on/api/datasources.
Performance implications
queryvariables dominate load time. Eachrefresh: 1query is one round trip to the data source; eachrefresh: 2query is one per time-range change.customandconstantvariables are essentially free. No data source call; no I/O.datasourcevariables are a single in-memory call to Grafana’s data source registry. Effectively free.intervalandsystemvariables are computed locally. Effectively free.
The lesson on variable query cost examines query cost in
detail.
Production guidance
- Default new variables to
querywhen the value set is data-driven; default tocustomwhen the value set is small and known. - Use
constantonly for immutable per-dashboard facts. Usetextboxonly for free-form search; never for a filter that controls what panels render. - Set
refresh: 1onqueryvariables unless arefresh: 2knob is operationally required. The cost ofrefresh: 2is hidden in dashboard load latency. - Set
multi: trueplusincludeAll: trueplusallValue: ".*"onqueryvariables that use=~. This is the production default for filtering dimensions. - Set
skipUrlSync: trueon variables that are per-tab-only (e.g., a “compare against staging” toggle on a production dashboard). The URL stays clean and shared links do not pin the toggle.
Verification
You should now be able to answer:
- Name the seven variable kinds and the value-list source of each.
- Which kind is appropriate for an immutable per-dashboard region tag, and which is appropriate for a data-driven list of clusters?
- What is the difference between
includeAll: trueandmulti: true? - When does a
queryvariable refresh, and what doesrefresh: 2mean? - What is the failure shape of
multi: truecombined with=rather than=~?
Quiz
Knowledge check · 8 questions
Q1. Which variable kind runs an expression against a data source to produce the value list?
Q2. Which variable kind pins an immutable value per dashboard and does not appear in the URL?
Q3. A custom variable refreshes on every dashboard load.
Q4. Which variable kind is the right choice for an explicit list of business services authored once and stable for years?
Q5. Name two variable kinds whose value list Grafana computes locally without a data source call.
Q6. Which of these variable kinds do NOT make a data source call when resolving?
Q7. A panel wants to switch between several Prometheus instances by configuration. Which variable kind fits?
Q8. What does the All pseudo-value expand to in a query variable with allValue: ".*" and multi: true?
Passing score: 75%. Answers are checked in this browser.