You export a Grafana dashboard's JSON and commit it so the same file can be deployed to dev, staging and production. Which parts of the JSON model determine whether it works unchanged in all three, and what must be removed or parameterised first?
answer
- uid = stable identity; id = local autoincrement, strip it
- version/iteration/panel ids = diff noise, normalise in CI
- schemaVersion migrates on load, rewrites on save
- Panels reference {type, uid} — pin UIDs or use a datasource variable
- Export-for-sharing `__inputs`/${DS_…} are NOT filled by file provisioning
basics
~20 sKeep a stable uid, drop the numeric id, and stop hardcoding per-instance data source UIDs in panels — either pin the same data source UID in every environment or drive panels from a data-source template variable. Also expect schemaVersion migration and version churn to add diff noise.
solid answer
~60 sThe fields that decide portability: - **`uid`** — the stable, human-chosen identity. Set it, keep it; links and provisioning updates key off it. - **`id`** — the database-local auto-increment. It must be null/absent in a committed file; carrying another instance's id causes conflicts. - **`version` / `iteration`** — bump on every save and are pure diff noise; normalise them in CI. - **`schemaVersion`** — Grafana migrates older dashboards on load and writes them back at the current schema, so an exported file may differ structurally from the one you committed. - **Panel `datasource`** — the real portability trap. Modern JSON references a data source as `{type, uid}`, and a UID generated in one instance does not exist in another. Two clean fixes: **pin deterministic UIDs** in datasource provisioning so every environment has `prometheus-main`, or use a **data-source template variable** and reference `${DS}` in panels. - **`__inputs` / `${DS_…}`** — produced by *Export for sharing externally*. Those placeholders are filled by the import flow; **file provisioning does not fill them**, so such a file loads broken. Also check annotations, library panels and links, which likewise resolve by uid.
code
text · 10 lines{
"uid": "platform-api-overview", // KEEP, stable, human-chosen
"id": null, // STRIP (per-instance autoincrement)
"version": 0, // NORMALISE in CI (pure churn)
"schemaVersion": 39, // migrated on load, rewritten on save
"panels": [
{ "datasource": { "type": "prometheus", "uid": "prometheus-main" } }
// pinned UID (same in every env) -- or -- "${DS}" from a datasource variable
]
}go deeper
Know that uid is the stable identity, that id must go, and that panels point at a data source which may not exist in another instance.
Explain the pinned-UID and datasource-variable fixes and why an export-for-sharing file with __inputs does not work under file provisioning.
Add schemaVersion migration effects, diff-noise normalisation, and CI checks that assert uid presence, no unresolved placeholders and known data source UIDs.
Argue for generating dashboards from a higher-level definition so that identity, data source binding and review quality are structural rather than conventions people remember.
## Identity: uid versus id A dashboard has two identifiers. **`id`** is the primary key in that particular Grafana's database — an auto-increment, meaningless anywhere else. **`uid`** is a short string you choose, stable across instances, and it is what URLs, links and provisioning use to decide whether an incoming file is an update or a new dashboard. Rules that follow: always set `uid` explicitly in a committed file, never let it be regenerated by a copy-paste round trip, and make sure `id` is absent or null. A file carrying a foreign `id` invites conflicts with whatever occupies that row in the target instance. If the uid changes — because someone re-created the dashboard through the UI — you get a *second* dashboard rather than an update, and every saved link, alert annotation and bookmark points at the old one. ## Churn fields `version` increments on every save; `iteration` is an editing artefact; the panel `id` numbering churns as panels are added and removed. None of this affects behaviour, all of it pollutes diffs and makes review useless. Teams that are serious about dashboards-as-code normalise these in CI (strip or zero them) so that a pull request shows only the change that matters. This is also the strongest practical argument for *generating* dashboards from a higher-level definition rather than hand-committing exported JSON. ## schemaVersion and migrations `schemaVersion` records which internal dashboard schema the JSON conforms to. Grafana migrates older schemas forward when it loads them, and when a dashboard is saved it is written at the current schema. Consequences: a file committed years ago still works, but exporting it after an upgrade produces a structurally different file (renamed fields, restructured panel options). Expect large, semantically empty diffs after a Grafana upgrade, and decide deliberately whether to re-export and commit the migrated form or leave the old form and let Grafana migrate on load. ## The data source reference — the actual trap Older dashboards referenced data sources by **name**, which was portable if names matched. Modern Grafana references them as an object `{ "type": "prometheus", "uid": "P1A2B3C4" }`. A UID auto-generated when someone clicked a data source into existence is unique to that instance, so the committed file points at nothing in staging or production, and every panel renders "data source not found". Three ways out, best first: 1. **Pin the UID in datasource provisioning.** Give the data source an explicit `uid: prometheus-main` in every environment's provisioning file. The dashboard JSON then references a UID that genuinely exists everywhere. This is the simplest and most robust option and it also makes derived-field and exemplar links portable, since those resolve by UID too. 2. **Use a data source template variable.** Declare a variable of type *datasource*, and reference `${DS}` in every panel. Users can then switch environments inside the dashboard, which is desirable for fleet-wide dashboards and undesirable when you want a dashboard hard-bound to one source. 3. **`__inputs` placeholders.** *Export for sharing externally* rewrites data source references to `${DS_PROMETHEUS}` and adds an `__inputs` block describing what must be supplied. The import flow (UI or the import API) prompts for and substitutes them. **File provisioning does not** — it loads the JSON as-is, so a shared-export file dropped into a provisioning directory produces a dashboard whose panels reference an unresolved placeholder. This mismatch between the export intended for sharing and the export intended for provisioning is one of the most common real-world failures, and knowing it is a strong signal in an interview. ## Everything else that resolves by uid - **Annotation queries** carry their own data source reference — same problem, same fixes. - **Library panels** are referenced by uid and must exist in the target instance; provisioning the dashboard does not bring them along. - **Dashboard links and panel drill-through links** point at other dashboards by uid, so a link to a dashboard that is not deployed in that environment simply 404s. - **Template variable definitions** may embed a data source reference each. ## The workflow that makes this manageable Edit in the UI, then export; strip `id`, normalise `version`/`iteration`; keep `uid` stable; ensure every data source reference is either a pinned UID or a variable; commit; let provisioning deliver. Validate in CI: parse the JSON, assert a uid is present, assert no unresolved `${DS_` placeholders, assert every referenced data source UID is one of the pinned set. Those three checks catch nearly every portability failure before it reaches an environment.
- When would you prefer a data source template variable over pinning the same UID in every environment?Prefer the variable when one dashboard must serve several sources the user chooses between at view time — per-cluster or per-region Prometheus instances inside a single Grafana, for example. Prefer pinned UIDs when each environment has its own Grafana and the dashboard should be hard-bound to that environment's source, because it removes a click, avoids a wrong-source screenshot during an incident, and keeps the JSON simpler. They also compose: pin the UIDs and use a variable only where genuine choice exists.
- Why does a dashboard exported with the share-externally option often break when dropped into a provisioning directory?That export rewrites data source references to `${DS_…}` placeholders and adds an `__inputs` block declaring what must be supplied. Those inputs are resolved by the import flow — the UI importer or the import API — which asks for a concrete data source. File provisioning performs no such substitution: it loads the JSON literally, so the panels keep an unresolved placeholder and render as having no data source. For provisioning you want the plain JSON export with real, pinned UIDs or a datasource variable.
saying these in an interview costs you the question
- Committing the numeric id, or letting the uid change between exports so updates create duplicates
- Hardcoding a data source UID that was auto-generated in one instance
- Assuming an export-for-sharing file works under file provisioning because it imports fine in the UI
- Reviewing dashboard pull requests without normalising version/iteration churn, so real changes are invisible
- Forgetting that annotations, library panels and dashboard links also resolve by uid in the target instance