Every Looker team eventually hits the same asymmetry. LookML lives in Git — branches, pull requests, advanced deploy mode, a clean promotion path from a developer's sandbox to production. User-created content does not. Dashboards, Looks, folders, boards, schedules, alerts and user attributes live in Looker's internal database, and the only supported way to move them is the API.
That matters the moment you run more than one instance: a dev or QA instance next to production, a separate instance per region, or a per-customer instance in an embedded deployment. Somebody rebuilds the same dashboard three times by hand, the numbers drift, and nobody can say which copy is authoritative.
This tutorial covers the tooling that closes the gap: gzr (Gazer) for single artefacts, Looker Deployer for repeatable multi-object promotion, and the folder/model remapping details that decide whether an import lands cleanly or explodes.
What is and is not covered by Git
| Artefact | Versioned in Git? | How it moves between instances |
|---|---|---|
| Views, models, explores, LookML dashboards | Yes | Normal Git deploy |
| User-defined dashboards | No | gzr dashboard export/import |
| Looks | No | gzr look export/import |
| Folders (Spaces) and their trees | No | gzr folder export/import |
| Boards (Homepages) | No | gzr board export/import |
| Schedules / alerts | No | API 4.0 or gzr plans |
| Roles, groups, model sets, user attributes | No | SDK / Terraform |
| Connections | No | SDK / Terraform |
The first architectural decision follows from this table. Anything the business cannot afford to lose should be a LookML dashboard, versioned in Git (see our post on LookML dashboards as code). Content promotion tooling is for the layer above that: the analyst-built dashboards you still need to keep in sync across instances.
Installing gzr
gzr is a Ruby CLI that wraps the Looker API for content operations.
gem install gazer
gzr --version
Authenticate with an API key from Admin > Users > Edit > API Keys. gzr reads a .netrc-style file or explicit flags; the least surprising approach in a pipeline is environment-driven flags:
export LOOKERSDK_BASE_URL="https://dev.looker.example.com:19999"
export LOOKERSDK_CLIENT_ID="..."
export LOOKERSDK_CLIENT_SECRET="..."
gzr --host dev.looker.example.com --port 19999 \
--client-id "$LOOKERSDK_CLIENT_ID" \
--client-secret "$LOOKERSDK_CLIENT_SECRET" \
user me
Use a dedicated service user with an admin role for imports. Content import creates and reassigns objects; a limited role will fail halfway and leave you with partial state.
Exporting a dashboard
Find the dashboard id from its URL (/dashboards/482), then:
gzr dashboard cat 482 --dir ./content/dev > /dev/null
# writes ./content/dev/Executive_Revenue_Overview_482.json
The JSON contains the tiles, their queries (model, explore, fields, filters, sorts, pivots), layout, dashboard filters and theme. Open it. Two things are worth noticing before you import anywhere:
- Each tile's query names a model — that model must exist on the target instance with the same name.
- The file references a folder by id, and folder ids differ between instances.
Importing into another instance
Import takes a target folder id on the destination:
gzr dashboard import ./content/dev/Executive_Revenue_Overview_482.json 17 \
--host prod.looker.example.com \
--client-id "$PROD_ID" --client-secret "$PROD_SECRET"
Useful flags:
--force— overwrite a dashboard with the same title in that folder instead of creating a duplicate. Without it you will accumulate Executive Revenue Overview, … (copy), … (copy 2).--plans— carry schedule plans across with the dashboard.
To move a whole folder tree, including sub-folders, Looks and dashboards:
gzr folder export 17 --dir ./content/dev/finance
gzr folder import ./content/dev/finance 9 --host prod.looker.example.com
Remapping models and folders
The most common import failure is a model name mismatch — dev uses ecommerce_dev, production uses ecommerce. gzr accepts a mapping file so you do not hand-edit JSON:
[
{ "from": "ecommerce_dev", "to": "ecommerce" },
{ "from": "marketing_dev", "to": "marketing" }
]
gzr dashboard import dash.json 17 --mapping ./mapping.json
For folders, resolve ids at runtime rather than hard-coding them. Folder ids are instance-specific and change when someone rebuilds a tree:
FOLDER_ID=$(gzr folder ls --fields id,name --plain \
| awk -F'\t' '$2=="Finance" {print $1}')
Looker Deployer for repeatable promotion
gzr is fine for one-off moves. Once you are promoting a set of content on every release, use Looker Deployer (ldeploy), a Python wrapper that reads a YAML spec and drives gzr-equivalent operations in bulk.
pip install looker-deployer
looker.ini:
[dev]
base_url=https://dev.looker.example.com:19999
client_id=...
client_secret=...
[prod]
base_url=https://prod.looker.example.com:19999
client_id=...
client_secret=...
deploy.yaml:
content:
- name: finance_pack
source: dev
targets:
- prod
folders:
- "Shared/Finance"
exclude:
- "Shared/Finance/Scratch"
ldeploy content --config deploy.yaml --env dev --target prod --debug
Deployer also handles boards and can promote content by folder path rather than id, which is exactly the indirection you want in a pipeline.
Wiring it into CI
A workable promotion pipeline looks like this:
- Export nightly from production into Git. Run
gzr folder exportfor your curated folders and commit the JSON. This is not really version control of Looker content — ids and timestamps churn — but it is a recoverable backup and it makes accidental deletions visible in a diff. - Promote on release. After the LookML deploy to production succeeds, run the Deployer job for the content pack. Content that references fields which do not exist yet will import but render broken, so LookML first, content second, always.
- Validate after import. Call the content validator through the API and fail the job on new errors:
curl -s -H "Authorization: token $TOKEN" \
"$BASE_URL/api/4.0/content_validation" \
| jq '.content_with_errors | length'
- Store credentials as pipeline secrets, never in
looker.iniin the repo. Rotate the service user's key on the same schedule as your other API credentials.
Folder hygiene, or why imports get ugly
Content promotion punishes messy instances. Before you automate anything:
- Curate a promotion boundary. One top-level shared folder (
Shared/Certified) that is import-managed and read-only for analysts; everything else is personal or scratch and never promoted. Enforce it with folder permissions, not with a wiki page. - Prune before you copy. Use System Activity (
content_usage) to find dashboards nobody has opened in 90 days and delete them rather than migrating them forever. - Ban personal-folder dependencies. A dashboard in a shared folder that references a Look in someone's personal folder will not survive promotion — or that person's departure.
- Watch schedules. Importing with
--plansinto production means live emails start firing to real recipients. In QA, import without plans, or rewrite recipients first. - Mind the ownership. Imported content is owned by the service user. Decide deliberately whether that is what you want for auditability, and document it.
Common failure modes
| Symptom | Cause | Fix |
|---|---|---|
Model not found on import | Model naming differs per instance | --mapping file |
| Tiles render "field does not exist" | Content promoted before LookML deploy | Reorder pipeline |
| Duplicate dashboards after each run | Missing --force | Add --force, keep titles stable |
| Import succeeds, dashboard is empty | Source export ran against a dev branch with unmerged LookML | Export from a deployed state |
| Permission errors halfway through | Non-admin service user | Grant admin to the deploy user |
| Schedules fire from QA | Imported with --plans | Strip plans for non-prod targets |
Where this leaves you
Content promotion is not a substitute for putting your important assets in LookML — it is the tooling for the layer that will always live outside Git. The teams that do it well share three habits: a small, explicitly certified set of promotable content; LookML deployed before content, every time; and a validator check that fails the build rather than a Slack message someone reads on Monday.
If you are running multiple Looker instances and promoting dashboards by hand, our developers do this as a standard engagement: audit the folder structure, define the promotion boundary, and hand back a working pipeline. Get in touch to talk it through.