Databricks Genie Agents (formerly Genie Spaces) are domain-specific, no-code chat interfaces that let business users ask natural-language questions about their data and get back SQL queries, result tables, and visualizations. As a Genie Agent grows more sophisticated, with carefully crafted instructions, SQL filter snippets, example queries, and evaluation benchmarks, a new problem emerges: how do you manage all of that configuration reliably across environments?
If your team is editing agent instructions directly in the Genie UI, you have no version history, no code review, and no reliable path to promote changes from dev to production. One wrong update and there's no rollback.
In this post I'll show how to solve that using Declarative Automation Bundles (DABs) to manage a Genie Agent and its underlying Unity Catalog metric view entirely as code.
A production-ready Genie Agent carries a surprising amount of configuration:
Germany in the Country dimension); entity matching requires format assistance to be enabled on the columnWhen all of this lives only in the UI, you get:
The fix: treat the Genie Agent like any other piece of software: version-controlled, reviewed, and deployed through a pipeline.
Declarative Automation Bundles (DABs) is Databricks' infrastructure-as-code framework. It supports jobs, pipelines, dashboards, and as of CLI v1.10, Genie Agents as first-class resources.
The approach has two key pieces:
databricks bundle deploy.The full example is on GitHub: https://github.com/databricks-solutions/databricks-blogposts/tree/main/2026-08-genie-agent-cicd
genie-agent-cicd/
├── databricks.yml # bundle config, targets (dev/prod)
├── prebuild_notebook.py # substitutes {schema} into Genie Agent JSON — runs locally OR in Databricks
│
├── resources/
│ ├── metric_view.job.yml # job: CREATE OR REPLACE metric view
│ └── tpcds_retail.genie_space.yml # Genie Agent resource definition
│
├── src/ # source of truth — uses {schema} placeholders
│ ├── metric-view.yaml # metric view dimensions and measures (edit this; substitution handled by DABs at run time)
│ ├── create_metric_view.py # notebook that runs CREATE OR REPLACE; receives catalog/schema as job parameters
│ └── tpcds_retail.geniespace.json # Genie Agent content (edit this; substitution handled by prebuild_notebook.py)
│
└── build/ # generated by prebuild_notebook.py — gitignored
└── tpcds_retail.geniespace.json
Two files are the source of truth: src/metric-view.yaml defines the semantic layer (dimensions, measures, synonyms), and src/tpcds_retail.geniespace.json holds the agent configuration (instructions, SQL snippets, benchmarks).
A small Python script (prebuild_notebook.py) substitutes ${catalog} and ${schema} placeholders into the Genie Agent JSON before deployment.
A separate notebook (src/create_metric_view.py) handles the metric view: it reads src/metric-view.yaml at runtime and substitutes catalog/schema from job parameters before running the DDL.
The Genie Agent and metric view are both deployed via databricks bundle deploy, with the metric view applied by running the bundle job.
The metric view YAML is human-friendly. The inline source query, joins, dimensions, and measures are all readable and diff well in pull requests. A reviewer can see exactly which UNION ALL branch changed, which join was added, or which synonym was modified.
The .geniespace.json is structured JSON, not a blob. When DABs introduced the genie-space resource type, it made the agent config a proper file rather than a stringified JSON-inside-JSON. Every section (instructions, snippets, benchmarks, column configs) is a first-class JSON object you can edit and review.
databricks.yml is the single source of environment config. Catalog and schema are declared once per target. prebuild_notebook.py reads them and stamps the Genie Agent JSON into build/ before each deploy. For the metric view, catalog and schema are injected at runtime as job parameters — no hardcoded values in committed files, no manual reset when switching between dev and prod.
The separation is clean. The metric view (semantic layer) and the agent config (behavior layer) are in separate files with separate edit workflows. Changing a synonym does not require touching the agent instructions. Adding a benchmark question does not require regenerating SQL.
The walkthrough below uses samples.tpcds_sf1, a TPC-DS retail benchmark dataset that comes pre-loaded in every Databricks workspace. It contains three sales fact tables (store_sales, catalog_sales, web_sales) plus standard dimension tables (date_dim, item, customer). No data setup is required. You can follow every step using this dataset as-is.
The same pattern applies to your own data: replace samples.tpcds_sf1 with your source tables, and replace <your_catalog>.<your_schema> with the catalog and schema where you want the base view and metric view to live.
git clone https://github.com/databricks-solutions/databricks-blogposts.git
cd 2026-08-genie-agent-cicd
The bundle commands require Databricks CLI v1.10 or later. macOS:
brew tap databricks/tap
brew install databricks
Linux / macOS (curl):
curl -fsSL https://raw.githubusercontent.com/databricks/setup-cli/main/install.sh | sh
Windows:
winget install Databricks.DatabricksCLI
Verify the installation:
databricks --version
Each target in databricks.yml maps to a named profile in ~/.databrickscfg. Set up one profile per workspace using OAuth (recommended):
# Dev workspace
databricks auth login --host https://<dev-workspace>.cloud.databricks.com --profile DEFAULT
# Prod workspace
databricks auth login --host https://<prod-workspace>.cloud.databricks.com --profile PROD
Both commands open a browser for OAuth login and write the credentials to ~/.databrickscfg
[DEFAULT]
host = https://<dev-workspace>.cloud.databricks.com
[PROD]
host = https://<prod-workspace>.cloud.databricks.com
Then update databricks.yml to reference the correct profile per target:
targets:
dev:
workspace:
profile: DEFAULT
variables:
catalog: dev_catalog
schema: genie
prod:
workspace:
profile: PROD
variables:
catalog: prod_catalog
schema: genie
A Unity Catalog metric view is a governed semantic layer — a special view type that separates how metrics are defined from how they're queried. Unlike a regular view that pre-aggregates at a fixed grain, a metric view lets callers choose which dimensions to group by at query time while guaranteeing the aggregation math is always correct. This makes it the ideal backing store for a Genie Agent: the agent uses your pre-vetted measure expressions, and the synonyms you define map natural-language terms like “revenue” or “sales” to the right SQL.
You create a metric view with the WITH METRICS LANGUAGE YAML DDL — the YAML spec is embedded inline between $$ markers. The metric view YAML defines dimensions, measures, and their configurations (synonyms, etc.). See the src/metric-view.yaml for detail.
CREATE OR REPLACE VIEW <your_catalog>.<your_schema>.tpcds_retail_sales_metrics
WITH METRICS
LANGUAGE YAML
AS $$
version: 1.1
source: | -- inline UNION ALL of store_sales, catalog_sales, web_sales
SELECT ... FROM samples.tpcds_sf1.store_sales UNION ALL ...
dimensions:
- name: Channel
expr: source.channel
synonyms: [sales channel, division]
measures:
- name: Total Sales
expr: SUM(source.net_paid)
synonyms: [revenue, net sales, sales]
$$
Querying a metric view requires wrapping every measure in MEASURE():
SELECT Channel, MEASURE(`Total Sales`) AS revenue
FROM <your_catalog>.<your_schema>.tpcds_retail_metrics
GROUP BY ALL
Source files use ${catalog} and ${schema} as placeholders, no environment-specific values are hardcoded. Then metric_view.job.yml injects catalog and schema as job parameters at run time via DABs variable substitution.
TPC-DS contains three sales fact tables — store_sales, catalog_sales, and web_sales — all representing the same business event: a sale line. When the analysis treats them as one unified sales process, normalize them with UNION ALL.
The metric view's source: field accepts an inline SQL query, so the UNION ALL lives directly in the YAML — no pre-built base view needed. Each branch is explicitly enumerated (no SELECT *), date roles and customer roles are declared once, and a channel column is added to preserve provenance.
Key design decisions in the union:
If you already have a Genie Agent configured in the UI, the CLI can export it directly into your bundle. First, find the space ID in the browser URL:
https://<workspace>.cloud.databricks.com/genie/spaces/<SPACE_ID>
Then run:
databricks bundle generate genie-space \
--existing-id <SPACE_ID> \
--key tpcds_retail
The --key value (tpcds_retail) becomes the resource key in the generated YAML and the identifier used when re-exporting or referencing the resource later.
This generates two files:
src/tpcds_retail.geniespace.json — the full agent configurationresources/tpcds_retail.genie_space.yml — the DABs resource definition After the initial export, src/tpcds_retail.geniespace.json becomes your source of truth — edit it directly for subsequent changes rather than re-running generate.The .geniespace.json file is structured JSON, not a raw blob, so every section is directly editable:
{
"version": 2,
"instructions": {
"text_instructions": [
{
"id": "...",
"content": ["# Agent Instructions\r\n", "..."]
}
],
"sql_snippets": {
"filters": [...],
"measures": [...],
"expressions": [...]
},
"example_question_sqls": [...]
},
"benchmarks": {
"questions": [...]
},
"data_sources": {
"tables": [
{
"identifier": "...",
"column_configs": [...]
}
]
}
}
Each agent uses its own --key, so multiple agents coexist in the same bundle without conflict:
databricks bundle generate genie-space \
--existing-id <SPACE_ID_B> \
--key finance_agent
Add the new resource to databricks.yml:
include:
- resources/metric_view.job.yml
- resources/tpcds_retail.genie_space.yml
- resources/finance_agent.genie_space.yml
Each agent's files are completely independent — src/finance_agent.geniespace.json and resources/finance_agent.genie_space.yml.
databricks.yml defines targets for each environment. A single --target flag promotes the exact same configuration to a different workspace with a different catalog.
The warehouse_id variable is declared at the top level with no default — each target sets its own lookup by warehouse name, which resolves against that target's workspace at deploy time:
bundle:
name: genie_agent_cicd
engine: direct # required for genie_spaces
variables:
warehouse_id:
description: SQL warehouse used to create the metric view and Genie Agent.
targets:
dev:
default: true
mode: development
workspace:
profile: DEFAULT
variables:
catalog: dev_catalog
schema: genie
warehouse_id:
lookup:
warehouse: Serverless Starter Warehouse
prod:
mode: production
workspace:
profile: PROD
root_path: /Workspace/Users/<user>@databricks.com/.bundle/${bundle.name}/${bundle.target}
variables:
catalog: prod_catalog
schema: genie
warehouse_id:
lookup:
warehouse: <prod_warehouse_name>
Generate build/ for dev and deploy
# The prebuild step substitutes ${catalog}/${schema} into the Genie Agent JSON. It must run before bundle deploy.
python3 prebuild_notebook.py --target dev
databricks bundle deploy --target dev
databricks bundle run metric_view --target dev
# Promote to prod — prebuild stamps prod catalog/schema into build/ first
python3 prebuild_notebook.py --target prod
python3 prebuild_notebook.py --verify prod # CI guard: exits non-zero on mismatch
databricks bundle deploy --target prod
databricks bundle run metric_view --target prod
If you don't want to install the CLI locally, Databricks supports deploying bundles directly from the workspace. This is especially useful for team members who prefer a UI-driven workflow or don't have a local development environment set up.
Prerequisites:
Workflow:
build/. It resolves paths from the notebook's workspace location. That's it. The Genie Agent and metric view now live in the target workspace. Because prebuild_notebook.py writes to build/ each time, there's no risk of committing environment-specific values or having to manually reset between targets.Limitations:
The workspace UI is a good fit for day-to-day iteration within a single environment. For multi-environment promotion and CI/CD pipelines, the CLI path remains the recommended approach.
Edit src/tpcds_retail.geniespace.json — for example, to add a new SQL filter snippet (use ${catalog}.${schema} for any table references):
"sql_snippets": {
"filters": [
{
"id": "...",
"display_name": "Store channel only",
"sql": ["`channel` = 'Store'"],
"synonyms": ["in-store", "brick and mortar"]
}
]
}
Then:
python3 prebuild_notebook.py --target dev
databricks bundle deploy
Edit src/metric-view.yaml, then deploy and re-run the job to apply the DDL:
databricks bundle deploy
databricks bundle run metric_view
If someone made changes directly in the Genie UI (it happens), re-export:
databricks bundle generate genie-space \
--existing-id <SPACE_ID> \
--key tpcds_retail
The full working example is available on GitHub:
https://github.com/databricks-solutions/databricks-blogposts/tree/main/2026-08-genie-agent-cicd
It includes the TPC-DS retail sales Genie Agent and metric view as a ready-to-deploy example. Clone it, swap in your workspace and catalog, import your own Genie Agent with bundle generate genie-space, and you have a CI/CD-ready Genie Agent in minutes.
This post is also published on: https://anhcodes.dev/blog/genie-agent-cicd/
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.