Reproducible Grafana Dashboards: Export, Edit, and Import the JSON Model via the API
Turn Grafana dashboards into version-controlled artifacts: export the JSON model via the API, fix data source UIDs, import with overwrite, and verify with a round-trip diff.
26 Aug 2025, 14:10 UTC

Managing Grafana dashboards by clicking through the UI works until you have three environments and a dashboard that silently differs in each one. The fix is to treat the dashboard JSON model as the source of truth: export it, version it in Git, and push it to every instance with the API. This guide walks through that workflow end to end, including the two failure modes that trip people up most often — UID collisions and data source identifier mismatches.
What you end up with
A dashboard definition stored in version control that you can deploy to any Grafana instance (dev, staging, production) with a single API call, with no manual clicking and no configuration drift between environments.
Prerequisites
- Grafana 7.0 or later. The dashboard JSON schema stabilized around 7.0; importing models from newer Grafana versions into older servers can silently drop panel types, legend options, or other fields. Check your version on the login page footer or via
GET /api/health(the version is also inGET /api/frontend/settingsfor authenticated users). - A service account token (Grafana 8.5+) or API key with Editor or Admin role on the target instance. Create one under Administration → Service accounts.
- Basic familiarity with the dashboard JSON structure: top-level fields like
uidandtitle, thepanelsarray, andtemplating.listfor variables. curlandjqinstalled on the machine you run the commands from.
Step 1: Export the dashboard JSON
You can do this in the UI (Dashboard settings → JSON Model, then copy), but for automation use the API. Run this from your workstation or CI runner:
curl -H "Authorization: Bearer $GRAFANA_TOKEN" \
https://grafana.example.com/api/dashboards/uid/abc123 | jq '.dashboard' > dashboard.jsonReplace abc123 with the dashboard's UID (visible in the dashboard URL) and $GRAFANA_TOKEN with your token. The response wraps the model in an envelope with dashboard and meta keys; the jq '.dashboard' filter strips the envelope so you keep only the portable model. Commit dashboard.json to your repository.
Step 2: Edit the model for the target environment
Three fields matter most before you import elsewhere:
uid: the stable identifier. Keep it identical across environments if you want the import to update the same dashboard rather than create a copy. Change it only if you intentionally want a second dashboard.title: adjust if environments need distinguishing names.- Data source references: panels reference data sources by
uid(and historically by name). If the target instance's Prometheus data source has a different UID, every panel will show "No data" after import. This is the single most common import failure.
Find every data source reference with:
jq '[.. | objects | select(has("datasource")) | .datasource] | unique' dashboard.jsonThen either rename the data source UIDs in the JSON to match the target, or — better for multi-environment setups — standardize data source UIDs across all your Grafana instances when you provision the data sources themselves, so dashboards become fully portable with zero edits.
Step 3: Import with the API
Import requires a wrapper object, not the raw model. The overwrite flag lets the import replace an existing dashboard with the same UID:
jq '{dashboard: ., overwrite: true, message: "deploy from CI"}' dashboard.json | \
curl -X POST -H "Authorization: Bearer $GRAFANA_TOKEN" \
-H "Content-Type: application/json" \
-d @- \
https://grafana-target.example.com/api/dashboards/dbA successful response returns JSON with status: "success", the dashboard uid, and a URL slug. A 412 or error mentioning UID/title conflict means another dashboard already uses that UID or title — either set overwrite: true (if the conflict is the dashboard you intend to replace) or change the UID.
Verify the deployment
Don't trust the 200 response alone. Run these checks:
- Round-trip diff. Fetch the dashboard back and compare it to what you sent:
Only fields you intentionally changed (plus server-managed fields likecurl -s -H "Authorization: Bearer $GRAFANA_TOKEN" \ https://grafana-target.example.com/api/dashboards/uid/abc123 | \ jq '.dashboard | del(.version, .id)' > imported.json jq -S . dashboard.json | del(.version, .id) > expected.json 2>/dev/null || true diff <(jq -S 'del(.version,.id)' dashboard.json) <(jq -S . imported.json)versionandid) should differ. - Panel health. Open the dashboard in the target UI and confirm every panel renders data. Panels showing "No data" across the board almost always mean a data source UID mismatch — re-run the
jqquery from Step 2 against the imported model. - Variables. Expand each templating dropdown at the top of the dashboard. Query-based variables that fail to populate usually point at a data source problem, not a JSON problem.
Recovery if the import breaks something
Because the import with overwrite: true replaces the existing dashboard, take a backup first: export the current dashboard from the target instance with the same GET /api/dashboards/uid/<uid> call and save it. To roll back, re-import that backup file with overwrite: true. Grafana also keeps dashboard versions (Dashboard settings → Versions), so you can restore the previous version from the UI if versioning is enabled — but an exported backup is more reliable for automation.
Limitations
- The JSON model does not carry permissions, folder assignment (unless you pass
folderUidin the import wrapper), or alert rules — provision those separately. - Importing a model exported from a much newer Grafana into an older server can silently drop features. Keep your environments on close versions, or export from the oldest version you run.
- For fully declarative setups, consider Grafana's file-based provisioning (
provisioning/dashboards) or the Terraform provider instead of ad-hoc API calls; the JSON model workflow here is the right fit when you want CI-driven pushes without restarting Grafana.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.