<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[DBSQL SME Engineering - Medium]]></title>
        <description><![CDATA[Publication for content from the DBSQL SME group and the surrounding community on DBSQL best practices, new design patterns, creative solutions, and real world user stories all in one place. - Medium]]></description>
        <link>https://medium.com/dbsql-sme-engineering?source=rss----a346361fdb66---4</link>
        <image>
            <url>https://cdn-images-1.medium.com/proxy/1*TGH72Nnw24QL3iV9IOm4VA.png</url>
            <title>DBSQL SME Engineering - Medium</title>
            <link>https://medium.com/dbsql-sme-engineering?source=rss----a346361fdb66---4</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sat, 14 Nov 2026 01:56:11 GMT</lastBuildDate>
        <atom:link href="https://medium.com/feed/dbsql-sme-engineering" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[Who’s Actually Using Your Databricks Dashboards (and What Do They Cost)?]]></title>
            <link>https://medium.com/dbsql-sme-engineering/whos-actually-using-your-databricks-dashboards-and-what-do-they-cost-80bc0224b827?source=rss----a346361fdb66---4</link>
            <guid isPermaLink="false">https://medium.com/p/80bc0224b827</guid>
            <category><![CDATA[databricks]]></category>
            <category><![CDATA[business-intelligence]]></category>
            <category><![CDATA[analytics]]></category>
            <category><![CDATA[dashboard]]></category>
            <dc:creator><![CDATA[Josh Rosenberg]]></dc:creator>
            <pubDate>Fri, 17 Jul 2026 18:06:47 GMT</pubDate>
            <atom:updated>2026-07-21T16:03:22.552Z</atom:updated>
            <content:encoded><![CDATA[<h4>A free, importable AI/BI dashboard that turns Databricks system tables into usage, performance, and cost analytics for every dashboard in your account</h4><p><strong>By Josh Rosenberg (Sr. Solutions Engineer @ Databricks) &amp; Cassi Sullivan (Sr. Solutions Engineer @ Databricks)</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*oXSqf3OIXBFdMz--T5Sv2g.png" /><figcaption>Usage &amp; Cost Dashboard</figcaption></figure><p>We spend a lot of energy building dashboards. We agonize over the right chart type, argue about color palettes, and rewrite the same aggregation four different ways to shave 200 milliseconds off a load. Then we publish, share the link, and move on.</p><p>And then… we mostly stop paying attention.</p><p>Six months later, nobody can answer the simple questions. Which dashboards do people actually open? Who are the power users? Which one has quietly become the most expensive object in the workspace because it re-scans a billion rows every time someone breathes near it? Are we paying to keep three near-identical “exec overview” dashboards alive when only one gets traffic?</p><p>Those aren’t dashboard-building questions. They’re dashboard <em>operations</em> questions, and Databricks already collects everything you need to answer them. It’s sitting in the system tables. You just have to assemble it.</p><p>So we built an <a href="https://www.databricks.com/product/business-intelligence">AI/BI</a> dashboard that does exactly that, and we’re sharing it so you can import it into your own account in a couple of minutes. This post walks through what it shows, how it works under the hood, how to set it up, and — importantly — the caveats you should understand before you present any of these numbers to a VP.</p><h3>What it actually shows</h3><p>The dashboard is organized into two analytical pages plus a shared filter panel, all driven by a single date range and a set of cross-cutting filters (viewer, dashboard ID, warehouse).</p><p><strong>The Overview page</strong> answers the adoption question. At a glance you get total dashboard views, unique viewers, the count of dashboards that saw any activity, and total queries fired. Below the headline counters, it breaks down daily query volume over time, ranks your most-used dashboards, surfaces your top viewers, and — the one that tends to start conversations — estimated cost by dashboard. There’s also a detailed query-level table for when you need to drill into specifics.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*a_og_FbjASct5th7QU3FRA.png" /><figcaption>Overview tab</figcaption></figure><p><strong>The Performance &amp; Attributed Costs page</strong> answers the efficiency question. Average query duration, cache hit rate, and total DBUs consumed sit up top. From there it trends cost and query duration over time, plots cache hits versus misses (a fast, honest read on how much your dashboards benefit from result caching), and slices cost and DBU consumption by user, by SKU, and by warehouse. The by-warehouse breakouts are useful for spotting a dashboard that’s pointed at an oversized or poorly-sized warehouse.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*HlW5p3GOAcAa2aBBIEZ64Q.png" /><figcaption>Performance &amp; Attributed Costs Tab</figcaption></figure><p><strong>The Global Filters pane</strong> is the control panel. Everything is parameterized, so you can zoom from “the whole account, last 90 days” down to “this one dashboard, this one warehouse, last week” without editing a single query.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*iiU7gmoP10I8x5kB__TG-Q.png" /><figcaption>Global FIlters Pane</figcaption></figure><p>The design goal was that someone should be able to open it cold and, within about thirty seconds, know whether their dashboard estate is healthy, loved, and reasonably priced.</p><h3>How it works under the hood</h3><p>The whole thing is built on <a href="https://www.databricks.com/product/unity-catalog">Unity Catalog</a> <a href="https://docs.databricks.com/aws/en/admin/system-tables/">system tables </a>— the read-only, account-wide operational logs Databricks maintains in the system catalog. No pipelines, no jobs, no copies of data. The dashboard queries the source directly. Four datasets do the work:</p><ul><li><strong>Views come from the audit log.</strong> The system.access.audit table records dashboard interactions under service_name = ‘dashboards’. We filter to the getDashboard, getPublishedDashboard, and executeDashboardWidget actions and pull the viewer’s email and the dashboard ID out of the request parameters. A left join to system.access.workspaces_latest swaps opaque workspace IDs for human-readable names.</li><li><strong>Performance comes from query history.</strong> system.query.history carries a query_source struct, and for any statement triggered by an AI/BI dashboard, query_source.dashboard_id is populated. That single field is the key that lets you attribute duration, rows read, cache behavior, and execution status back to a specific dashboard.</li><li><strong>Cost is the interesting part.</strong> Query history tells you <em>which</em> statements a dashboard ran; system.billing.attributed_usage tells you the DBUs each of those statements consumed, joined on the statement ID (usage_metadata.dbsql_statement_id = statement_id). To turn DBUs into dollars, we join system.billing.account_prices for any negotiated contract pricing on the SKU — if no contract price exists (as is the case for most accounts), we fall back to system.billing.list_prices, which always carries the public list rate. The result is an estimated cost per query that rolls up cleanly to per-dashboard, per-user, and per-warehouse totals. Note that attributed_usage lags by approximately 2 days, so cost figures for the most recent days may be incomplete until billing attribution settles.</li><li><strong>Daily activity is a pre-aggregation.</strong> A fourth dataset groups query history by day, dashboard, and warehouse to produce the trend lines efficiently — query counts, unique users, average duration, bytes read, and a cache-hit/miss split.</li></ul><p>That’s the entire architecture. Because it reads live from system tables, there’s nothing to schedule and nothing to maintain. When Databricks writes new audit and billing records, your dashboard reflects them on the next refresh.</p><h3>Getting it running</h3><p>The dashboard is importable directly from the repo (see below). Grab the .lvdash.json, <a href="https://docs.databricks.com/aws/en/dashboards/automate/import-export">import it as a new AI/BI dashboard</a> in your workspace, point it at a SQL warehouse, and refresh.</p><p>There’s one prerequisite that trips people up, and it’s not the import — it’s permissions. System tables have to be <a href="https://docs.databricks.com/aws/en/admin/system-tables/#enable"><strong>enabled by an account admin</strong></a>, and the schemas this dashboard reads (access, query, and billing) each have to be turned on. Whoever runs the dashboard also needs SELECT on those system schemas. If you import it and every tile comes back empty or throws a permissions error, that’s almost always the cause. Enabling system tables is a one-time account-level action; your platform or admin team can do it in a few minutes.</p><p>Once it’s importing cleanly, set the date range on the Global Filters page to something reasonable (we default to the last 30–90 days), pick a warehouse, and let it populate. From there it’s just filtering.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/305/1*uXSd-TygfkWv4XMa2sKQNg.png" /><figcaption>Setting the default date range</figcaption></figure><h3>Possible Upgrades</h3><p>If you want to take the dashboard a step further, there are four upgrades worth calling out.</p><ul><li><strong>How to find the dashboard ID: </strong>The fastest path is to grab it from the dashboard URL. For AI/BI dashboards, the dashboard ID is the value in the /sql/dashboardsv3/{id} URL, and if you are starting from the workspace file instead, workspace/get-status returns the dashboard resource_id, which is also the identifier used by the <a href="https://docs.databricks.com/api/workspace/lakeview/list">Dashboard API</a> and the published dashboard URL.</li><li><strong>How to automatically include the dashboard name:</strong> The main dashboard access audit events log dashboard_id, but not the human-readable dashboard name. A practical improvement is to resolve that ID through the <a href="https://docs.databricks.com/api/workspace/lakeview/get">dashboard API</a>, whose response includes display_name, so your reporting layer can show readable dashboard names without relying on a manual mapping file.</li><li><strong>Pull it via API into your own table:</strong> If you need longer retention or a cleaner reporting layer, snapshot the dashboard metadata into your own table on a schedule instead of depending only on the live retention window of system tables. The same dashboard_id can be used with the Dashboard API to pull dashboard details such as display_name, path, and warehouse_id, which gives you a lightweight dimension table you can reuse across joins and historical analysis.</li><li><strong>Add extra joins to show warehouse name explicitly: </strong>Query history records the warehouse used for a statement through compute.warehouse_id, while the warehouses system table exposes the readable warehouse_name. Joining on warehouse_id, and taking the latest warehouse snapshot for each ID, lets you replace opaque IDs with explicit warehouse names and makes cost and performance breakdowns much easier to interpret.</li></ul><h3>Where it earns its keep</h3><p>A few concrete ways we’ve seen this pay off:</p><ul><li><em>Rationalizing the estate.</em> Sort dashboards by activity and the long tail of zero-view dashboards becomes obvious. Those are candidates to archive — less clutter, fewer stale numbers floating around, and in some cases real savings.</li><li><em>Cost attribution and chargeback.</em> “Cost by dashboard” and “cost by user” give you a defensible, data-backed starting point for conversations about who’s driving warehouse spend, instead of guessing.</li><li><em>Performance triage.</em> The cache hit rate and average-duration trends make it easy to spot a dashboard that’s slow because it’s not benefiting from caching, or one whose performance has quietly degraded as its underlying tables grew.</li><li><em>Adoption tracking.</em> If you just shipped a dashboard to a new team, the viewer and query-volume trends tell you within days whether it landed or whether you need to go do some enablement.</li></ul><h3>Caveats — read these before you quote any numbers</h3><p>This is the part I’d want a colleague to read most carefully, because system-table data is powerful but it has sharp edges. Treat the dashboard as a strong directional signal, not a billing-grade source of truth.</p><p><strong>“Views” are an approximation.</strong> A single person opening one dashboard generates multiple audit events — an open event plus a widget-execution event for each tile on the page. Counting those actions gives you an excellent <em>engagement</em> signal, but it is not a literal count of unique human page loads, and it will run higher than a naive “one open = one view” intuition. If you need stricter semantics, narrow the action list (for example, count only getDashboard/getPublishedDashboard) and calibrate to your own environment.</p><p><strong>Cost is a list-price estimate.</strong> The dollar figures are computed from account_prices, which reflect list pricing. They do <strong>not</strong> reflect your organization’s actual negotiated or effective rate, discounts, or commit-based pricing. Use them to compare dashboards <em>against each other</em> and to spot outliers — not as a substitute for your real invoice.</p><p><strong>Cost attribution is cleanest on serverless SQL.</strong> Statement-level DBU attribution via attributed_usage lines up best with serverless SQL warehouses, where usage is tied to individual statements. On classic or pro SQL warehouses, billing is driven by cluster uptime rather than per-statement execution, so per-dashboard cost attribution can be partial or absent. Know which warehouse type your dashboards run on before you read too much into the cost tiles.</p><p><strong>System tables aren’t real-time.</strong> There’s ingestion latency between an event happening and it landing in the audit and billing tables — typically on the order of hours, and billing records can lag longer. Expect “today” to look sparse; judge trends on data that’s had time to settle.</p><p><strong>Retention is finite.</strong> System tables retain history for a bounded window, and the dashboard can only show you what’s still retained. If you need longer-horizon analysis, snapshot the data into your own table on a schedule.</p><p><strong>Query history is still in Public Preview.</strong> The system.query.history table — the backbone of the performance and cost datasets — is a Public Preview feature at the time of writing. Its schema can evolve, so keep an eye on the release notes if a field stops behaving the way you expect.</p><p>None of these should stop you from using the dashboard. They’re the difference between someone confidently presenting garbage and someone presenting a well-understood estimate with the right footnotes. The second person is the one who gets trusted with the next question.</p><h3>Grab it and make it yours</h3><p>Everything here is importable and fully editable. If your organization tags warehouses by team, add a tag breakdown. If you care about published-link traffic specifically, tighten the audit filter. If you want an alert when a dashboard’s cost crosses a threshold, wire one up on top of the cost dataset.</p><p>The point isn’t this exact set of tiles — it’s that the data to run your dashboards like a product, rather than shipping and forgetting them, is already in your account. This just connects the wires.</p><p><strong>Repo and import instructions: </strong><a href="https://github.com/Rosenberg-Josh/databricks-dashboard-usage-analytics"><strong>Repo Link</strong></a></p><p>If you end up extending it, we’d love to hear what you added.</p><p>*If there are any system tables referenced here that are not currently accesible, you can reach out to your account team for support.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=80bc0224b827" width="1" height="1" alt=""><hr><p><a href="https://medium.com/dbsql-sme-engineering/whos-actually-using-your-databricks-dashboards-and-what-do-they-cost-80bc0224b827">Who’s Actually Using Your Databricks Dashboards (and What Do They Cost)?</a> was originally published in <a href="https://medium.com/dbsql-sme-engineering">DBSQL SME Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Databricks Genie Code. Full practice guide and Examples]]></title>
            <link>https://medium.com/dbsql-sme-engineering/databricks-genie-code-full-practice-guide-f3bcb13596f2?source=rss----a346361fdb66---4</link>
            <guid isPermaLink="false">https://medium.com/p/f3bcb13596f2</guid>
            <category><![CDATA[ai-assistant]]></category>
            <category><![CDATA[data-engineering]]></category>
            <category><![CDATA[data-engineer]]></category>
            <category><![CDATA[databricks]]></category>
            <category><![CDATA[databricks-genie]]></category>
            <dc:creator><![CDATA[Maksim Pachkouski Databricks MVP]]></dc:creator>
            <pubDate>Wed, 25 Mar 2026 22:38:48 GMT</pubDate>
            <atom:updated>2026-08-15T13:03:09.400Z</atom:updated>
            <content:encoded><![CDATA[<p>In <strong>March 2026,</strong> Databricks officially replaced <strong>Databricks Assistant</strong> with <strong>Genie Code</strong>. This may seem like a name change, but it&#39;s actually a new assistant that runs in agent mode. The transition was fairly gradual, with <strong>Assistant</strong> gradually adding new features until it became <strong>Genie.</strong></p><p><a href="https://medium.com/dbsql-sme-engineering/genie-code-databricks-agentic-ai-the-price-of-intelligence-32a7bc477cba">In my previous article, I discussed the cost of <strong>Genie Code</strong></a><strong>.</strong> In<strong> </strong>this article, we&#39;ll explore the key differences between <strong>Assistant</strong> and <strong>Genie Code</strong>. If you want to use <strong>Genie Code,</strong> you&#39;ll also need to understand the basics of working with agents, as it can now not only assist you, but also edit and delete tables, scripts, and data with your permission.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*vlz_f_n8yXdj3JXnVyBpPg.jpeg" /></figure><p>Databricks is keeping up with the times and has decided to abandon the simple Assistant-style prompts in favor of Agentic AI. Let&#39;s break down the key differences:</p><h4>1. Operating mode:</h4><ul><li><strong>Databricks Assistant</strong>: Works on a question-and-answer basis. You ask it to write an SQL query, explain a piece of PySpark code, or find an error in a specific notebook cell, and it generates text or code and awaits your next command. It uses local context, specifically what&#39;s in your current notebook and the current chat history.</li><li><strong>Genie Code</strong>: This is an agent-based system. You can assign it a high-level task, and it will automatically break it down into steps, find the necessary tables, write the code, execute it, and verify the result. It is deeply integrated into <strong>the Unity Catalog</strong>. It automatically understands all your company data: table semantics, columns, table lineages, popular assets, and corporate access policies. You don&#39;t need to explain to it where the data is. It finds it automatically.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*feYVeP-BzJomr-UpTstB5Q.png" /></figure><h4>2. Hints and support:</h4><ul><li><strong>Assistant</strong>: Helps only during the development phase, writing and debugging code in real time.</li><li><strong>Genie Code</strong>: Runs as a proactive agent in the background. It can monitor Lakeflow pipelines and ML models, automatically analyze logs during crashes, perform basic troubleshooting, and configure resource allocation:</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*2VxCyv3JCaWdme2ewA12qg.png" /></figure><h4>3. Advanced Agent capabilities:</h4><p><strong>Genie Code is</strong> a fully configurable multi-agent. For each task, it dynamically routes requests, selecting the most appropriate model, be it a leading commercial LLM, open-source, or your custom model deployed directly in Databricks. Furthermore, via the Model Context Protocol (MCP), it can interact with external systems like Jira or GitHub. You can also configure general rules in the form of <a href="https://docs.databricks.com/aws/en/genie-code/instructions">custom instructions</a> or <a href="https://docs.databricks.com/aws/en/genie-code/skills">skills</a> for specific tasks, immersing the agent in context:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/551/1*mrAnbMqMvWNZvH9inB_GEQ.png" /></figure><h4>Basics of working with agents</h4><p>Genie Code takes over more complex engineering processes: from independent exploratory data analysis and model training to building and maintaining ETL pipelines. But now you need to understand the nuances and fundamentals of working with agents.</p><p><strong>1. Context isolation.</strong> For each new task or fundamentally different data set, it is <strong>essential to start a new session (chat):</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/555/1*r-BeyJQwofH2Ay2U3RMHEg.png" /></figure><p>The agent builds a decision tree that remembers the action plan and local context. If, after creating an ETL pipeline for the finance department, you ask it to analyze web server logs in the same window, its context will be confused. It may start applying previous logic or attempt to connect unrelated data, leading to failures and hallucinations.</p><p><strong>2. Confirming actions for the Agent.</strong> By default, the Agent will ask permission for every action, but you can grant <strong>&quot;Always Allow&quot; for all changes.</strong> Be careful in a production environment! It can permanently delete something, and finding such actions can be quite difficult. I recommend reviewing each action and confirming or rejecting it:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/569/1*bwNS5UORc8GX8m3hkpJ0_Q.png" /></figure><p><strong>3. Session management and interruptions.</strong> The agent&#39;s lifecycle in interactive mode is tied to an active communication session. When a page is reloaded or the network is interrupted, the connection is lost. The system stops the agent to prevent uncontrolled resource consumption, as it doesn&#39;t know whether the user will return. Wait for the agent to complete generation or an intermediate response without refreshing the tab.</p><blockquote>If the agent is running and you switch to another browser tab, it is paused, switch back and it continues running.</blockquote><p><strong>4. Task Setting: Goal + Limits.</strong> The agent should be given not step-by-step instructions (like the Assistant), but a final goal and strict limitations.</p><ul><li><strong>Set limits during the development phase:</strong> <em>&quot;Build the display, but first limit the selection to 1000 rows to test the logic</em>.&quot; This will prevent situations where the agent tries to sift through petabytes of data interactively.</li><li><strong>Specify security rules:</strong> Explicitly state if the agent is not allowed to modify data (for example: <em>“Do not DROP/UPDATE existing tables, save results only to the sandbox schema”</em> ).</li></ul><p><strong>5. In-process correction.</strong> Agents can get stuck in an infinite loop (e.g., write code with an error, get an error, try to fix it the same way, get another error).</p><ul><li>Follow the detailed logs of his actions.</li><li>If you see that the agent has gone down the wrong path or is stuck in a loop, don&#39;t wait - forcefully stop the generation and direct it to the text: <em>&quot;This table is empty, look for data in another directory</em>. &quot;</li></ul><p><em>Author:</em> <strong>Maksim Pachkouski</strong> [Databricks MVP &amp; Databricks Solutions Architect Champion]. Follow my posts on <a href="https://medium.com/@protmaks">Medium</a> and <a href="https://www.linkedin.com/in/protmaks/">LinkedIn</a> to stay up-to-date on the latest news in Data Engineering and learn about best practices.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=f3bcb13596f2" width="1" height="1" alt=""><hr><p><a href="https://medium.com/dbsql-sme-engineering/databricks-genie-code-full-practice-guide-f3bcb13596f2">Databricks Genie Code. Full practice guide and Examples</a> was originally published in <a href="https://medium.com/dbsql-sme-engineering">DBSQL SME Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Embedding Genie API for a Multi‑Tenant Application]]></title>
            <link>https://medium.com/dbsql-sme-engineering/embedding-genie-api-for-a-multi-tenant-application-d307bfbfc89b?source=rss----a346361fdb66---4</link>
            <guid isPermaLink="false">https://medium.com/p/d307bfbfc89b</guid>
            <category><![CDATA[embedded-analytics]]></category>
            <category><![CDATA[databricks]]></category>
            <category><![CDATA[databricks-dashboard]]></category>
            <dc:creator><![CDATA[Josh Rosenberg]]></dc:creator>
            <pubDate>Sat, 21 Mar 2026 04:49:02 GMT</pubDate>
            <atom:updated>2026-03-31T16:40:09.517Z</atom:updated>
            <content:encoded><![CDATA[<p><em>Implementation Guide for Attribute-Based Access Control (ABAC) for a Multi-Tenant Architecture for AI/BI Genie</em></p><p>By Josh Rosenberg (Sr. Solutions Engineer @ Databricks) &amp; Josh Bae (Delivery Solutions Architect @ Databricks)</p><h3><strong>Summary</strong></h3><ul><li>Embed the Genie Conversation API directly inside multi-tenant apps to deliver stateful analytics conversations backed by a curated Genie space.</li><li>Authenticate requests through your application using OAuth M2M with service principals, not individual Databricks user accounts or external workspace provisioning.</li><li>Securely personalize data with Unity Catalog ABAC policies and implement row-level security guardrails.</li></ul><h3>Introduction</h3><p>Since the launch of AI/BI Genie spaces, we have seen enterprise-scale adoption of self-service analytics using natural language for many Databricks users. While this served customers well for internal reporting and operational visibility, the lack of access for external audiences to the UI-native experience on Databricks made it difficult to share this capability more broadly.</p><p>However, with the <a href="https://www.databricks.com/blog/genie-conversation-apis-public-preview">Public Preview release</a> of the AI/BI Genie Conversation API across AWS, Azure, and GCP, users could now serve Genie-level insights from any application, even external or custom-built ones. The API provided a seamless way for business users to “talk to their data” from any surface, but it raised an immediate governance issue: how would the app ensure that only the data that is permitted for a particular viewer could be returned?</p><p>It is a problem we have seen many of our customers wrestle with, so this blog offers guidance on designing your multi-tenant Genie space apps with robust per-tenant guardrails via service principals (using OAuth M2M authentication) and Unity Catalog ABAC (attribute-based access control) policies.</p><h3>Reference Architecture</h3><p>This section outlines the high-level request flow and required infrastructure components for the integration.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7QYdSa6IPik554bTO0Eztw.png" /></figure><p>At a high level, the runtime flow looks like this:</p><ol><li>Your application receives a question from a user in a multi-tenant app.</li><li>The app determines which tenant the request belongs to.</li><li>The app retrieves that tenant’s Databricks service principal credentials from a secure vault.</li><li>The app uses the OAuth client credentials flow to get an access token.</li><li>The app calls the Genie Conversation API using that tenant-specific identity.</li><li>Genie generates SQL against curated Unity Catalog tables (in a Genie space).</li><li>ABAC policies evaluate current_user() during query execution.</li><li>Only the rows and columns that the identity is allowed to access are returned.</li></ol><p>That basic runtime architecture is what makes the pattern powerful: every request runs as a tenant-specific principal, so identity becomes the control point for access.</p><h3>Why this pattern works</h3><p>The major design choice here is to use a dedicated <em>service principal (SP) per tenant</em> (which we will elaborate on in later sections). That way, your application no longer has to be the primary enforcement layer for data isolation. Instead, Databricks governance via Unity Catalog handles it at query time.</p><p>When a request runs, Unity Catalog evaluates policies using the current caller, and because each tenant has its own SP, you can write rules directly against that identity. This dramatically simplifies the mental model. Rather than sprinkling tenant filters throughout your app and hoping they are always applied correctly, you let the Databricks Platform evaluate access where it matters most: when the SQL actually runs. The tradeoff is operational: you now have many identities and secrets to manage, so automation is essential.</p><h3>Multi-Tenancy Design</h3><p>Tenant isolation hinges on how you structure service principals and how Unity Catalog ABAC evaluates row filters and column masks at query time. Let’s clarify these terms to better articulate how they will serve the implementation in the proposed solution later in this blog.</p><p><strong><em>What’s a service principal (SP)?</em></strong></p><p>Databricks recommends using SPs as specialized identities for accessing Databricks resources because they avoid relying on individual user credentials. It provides a programmatic way to grant/restrict access to the Genie space(s) without defining how each individual user will query the underlying data. Since all requests run under the tenant SP, filters/masks apply consistently without per-user identity.</p><p><strong><em>What’s an attribute-based access control (ABAC) policy?</em></strong></p><p>ABAC in Unity Catalog is a governance model that lets you centrally express access rules using attributes and have them enforced automatically at query time. In practice, ABAC policies typically take the form of row filters (to limit which rows are visible) and column masks (to redact sensitive fields), and those policies can be inherited down the object hierarchy (catalog → schema → table). Under our recommended design, applications can authenticate via SPs and rely on ABAC to enforce tenant boundaries at the data layer, rather than implementing security solely in application code.</p><p>We will cover authoring ABAC policies in a later section, but you can read more at <em>Unity Catalog attribute-based access control (ABAC).</em> (<a href="https://docs.databricks.com/aws/en/data-governance/unity-catalog/abac/">AWS</a> | <a href="https://learn.microsoft.com/en-us/azure/databricks/data-governance/unity-catalog/abac/">Azure</a> | <a href="https://docs.databricks.com/gcp/en/data-governance/unity-catalog/abac/">GCP</a>)</p><h3>Implementation Guide</h3><h4>Prerequisites</h4><p>Before implementing this pattern, make sure you have the following in place:</p><ul><li>Be an account admin and/or workspace admin (SP creation, generating OAuth secrets, etc.)</li><li>Have access to the following:<br>- A Databricks workspace with the Databricks SQL entitlement<br>- At least CAN EDIT permissions on the Genie space<br>- CAN USE access on at least one pro or serverless SQL warehouse<br>- SELECT privileges on the data used in the space<br>- A control-plane process or service for onboarding tenants<br>- A secure secret store or vault to manage SP credentials</li></ul><p>Additionally, to create OAuth secrets for SPs, you should also account for several ABAC-related constraints when designing this architecture. ABAC in Unity Catalog requires <em>Serverless compute</em> (<a href="https://docs.databricks.com/aws/en/compute/serverless/">AWS</a> | <a href="https://learn.microsoft.com/en-us/azure/databricks/compute/serverless/">Azure</a> | <a href="https://docs.databricks.com/gcp/en/compute/serverless/notebooks">GCP</a>) or <em>Standard compute running on Databricks Runtime (DBR) 16.4+</em>, and there are policy limits to consider, such as <strong>one row filter per table per user</strong> and <strong>one mask per column per user</strong>. Those constraints should help inform how you design your governance and policy layer from the outset, avoiding conflicts or unnecessary complexity later.</p><p>For best practices, limitations, and example user-defined functions (UDFs), see <em>UDFs for ABAC policies best practices</em> (<a href="https://docs.databricks.com/aws/en/data-governance/unity-catalog/abac/udf-best-practices">AWS</a> | <a href="https://learn.microsoft.com/en-us/azure/databricks/data-governance/unity-catalog/abac/udf-best-practices">Azure</a> | <a href="https://docs.databricks.com/gcp/en/data-governance/unity-catalog/abac/udf-best-practices">GCP</a>)</p><h4>Step 1: Provision a service principal for each tenant</h4><p>The first step is to treat tenant identity as a first-class component of your onboarding process.</p><p>When a new tenant is created, your provisioning system should create (or retrieve) a dedicated SP for that tenant, assign it to the appropriate workspace, and treat that identity as the credential your application will use whenever it needs to execute requests on the tenant’s behalf. Your app should not create identities dynamically at runtime. Instead, identity provisioning should live in a separate onboarding or control-plane process responsible for identity lifecycle management.</p><p>Maintaining this separation is important. Your application runtime should consume tenant credentials rather than manage tenant identities directly. Centralizing identity provisioning within a control plane ensures clearer lifecycle management, stronger security practices, and a more maintainable multi-tenant architecture.</p><p>You can leverage the Account SCIM REST API to do this.</p><p>To execute the HTTP requests, you will need your Databricks account ID, the account console URL (such as <a href="https://accounts.cloud.databricks.com/">https://accounts.cloud.databricks.com</a> for AWS), and an account-level access token. You will also need the exact SCIM ID of the group where you plan to assign the new service principal.</p><p><em>Note: The code can also be run with WorkspaceClient since workspace admins can also add SPs to the Databricks account.</em></p><pre>import os<br>import requests<br><br># Retrieve configuration from environment variables<br>ACCOUNT_ID = os.environ[&quot;DATABRICKS_ACCOUNT_ID&quot;]<br>ACCOUNT_HOST = &quot;https://accounts.cloud.databricks.com&quot;<br>TOKEN = os.environ[&quot;DATABRICKS_ACCOUNT_TOKEN&quot;]  # admin token<br><br># Define a function that creates a new service principal<br>def create_service_principal(display_name: str):<br><br>    # Build the account-level API endpoint for creating a service principal<br>    url = (<br>        f&quot;{ACCOUNT_HOST}/api/2.0/accounts/{ACCOUNT_ID}&quot;<br>        f&quot;/scim/v2/ServicePrincipals&quot;<br>    )<br><br>    # Send a POST request to Databricks to create the service principal<br>    response = requests.post(<br>        url,<br>        headers={<br>            &quot;Authorization&quot;: f&quot;Bearer {TOKEN}&quot;,<br>            &quot;Content-Type&quot;: &quot;application/scim+json&quot;<br>        },<br>        json={<br>            &quot;schemas&quot;: [&quot;urn:ietf:params:scim:schemas:core:2.0:ServicePrincipal&quot;],<br>            &quot;displayName&quot;: display_name<br>        },<br>        timeout=30,<br>    )<br><br>    # Raise an exception if Databricks returns an HTTP error status<br>    response.raise_for_status()<br>    return response.json()</pre><p>Not included above, but Databricks recommends assigning each SP in Unity Catalog to <em>groups</em>. They simplify access to workspaces, data, and other securable objects.</p><p>Check out <em>Manage service principals</em> (<a href="https://docs.databricks.com/aws/en/admin/users-groups/manage-service-principals">AWS</a> | <a href="https://learn.microsoft.com/en-us/azure/databricks/admin/users-groups/manage-service-principals">Azure</a> | <a href="https://docs.databricks.com/gcp/en/admin/users-groups/manage-service-principals">GCP</a>) for more explanations on how to manage SPs for your Databricks account and workspaces.</p><h4>Step 2: Generate and store the OAuth secret</h4><p>Once the tenant’s SP has been created and registered in Unity Catalog, the next step is to generate an OAuth secret that your application can use as the client_secret in the OAuth client-credentials flow.</p><p>You can create this secret programmatically using the Databricks API.</p><p><em>Note: A service principal can have up to five OAuth secrets, each with a configurable lifetime (up to two years).</em></p><pre>import os<br>import requests<br><br># Retrieve configuration from environment variables<br>ACCOUNT_ID = os.environ[&quot;DATABRICKS_ACCOUNT_ID&quot;]<br>ACCOUNT_HOST = &quot;https://accounts.cloud.databricks.com&quot;<br>TOKEN = os.environ[&quot;DATABRICKS_ACCOUNT_TOKEN&quot;]  # admin token<br><br># Define a function that creates an OAuth secret for a given SP ID<br>def create_oauth_secret(service_principal_id: str):<br><br>    # Build the account-level API endpoint for creating a SP secret<br>    url = (<br>        f&quot;{ACCOUNT_HOST}/api/2.0/accounts/{ACCOUNT_ID}&quot;<br>        f&quot;/servicePrincipals/{service_principal_id}/credentials/secrets&quot;<br>    )<br><br>    # Send a POST request to Databricks to create the OAuth secret<br>    response = requests.post(<br>        url,<br>        headers={&quot;Authorization&quot;: f&quot;Bearer {TOKEN}&quot;},<br>        json={&quot;lifetime&quot;: &quot;15552000s&quot;},  # 180 days<br>        timeout=30,<br>    )<br><br>    # Raise an exception if Databricks returns an HTTP error status<br>    response.raise_for_status()<br><br>    return response.json()</pre><p>One important detail is that the OAuth secret string value is displayed only once in the API response and cannot be retrieved again. As soon as it is shown, you should immediately store it in a secure secrets manager and never persist it in plaintext outside the vault. In practice, you will store the tenant’s client_id, client_secret, secret_id, and expires_at so you can authenticate at runtime and rotate secrets safely later.</p><p>Check out <em>Authorize service principal access to Databricks with OAuth </em>(<a href="https://docs.databricks.com/aws/en/dev-tools/auth/oauth-m2m">AWS</a> | <a href="https://learn.microsoft.com/en-us/azure/databricks/dev-tools/auth/oauth-m2m">Azure</a> | <a href="https://docs.databricks.com/gcp/en/dev-tools/auth/oauth-m2m">GCP</a>) for more information and instructions on how to do this via UI.</p><h4>Step 3: Grant the minimum permissions during provisioning</h4><p>After the SP and its associated OAuth secret are created, the next step is to grant only the permissions required for the tenant to use Genie and query the data it is authorized to access.</p><p>To use a Genie space, the SP must have two things:</p><ul><li>The consumer access or Databricks SQL entitlement</li><li>At least SELECT privileges on all of the Unity Catalog data objects used in the space</li></ul><p><em>Note: Queries run using the compute credentials embedded by the author who configured the warehouse. End users do not need direct warehouse permissions.</em></p><p>This should be part of your automated provisioning flow, not a manual admin step. A Python example using the Databricks SDK for Genie permissions looks like the following:</p><pre>import requests<br><br>WORKSPACE_HOST = &quot;https://&lt;your-workspace-instance&gt;.cloud.databricks.com&quot;<br>WORKSPACE_TOKEN = os.environ[&quot;DATABRICKS_WORKSPACE_TOKEN&quot;] <br><br>def provision_genie_client_permissions(<br>    host: str,<br>    token: str,<br>    sp_app_id: str,      # client_id of the tenant SP<br>    genie_space_id: str,<br>):<br>    headers = {&quot;Authorization&quot;: f&quot;Bearer {TOKEN}&quot;}<br><br>    # Allow the service principal to run the Genie space<br>    requests.patch(<br>        f&quot;{WORKSPACE_HOST}/api/2.0/permissions/genie/{genie_space_id}&quot;,<br>        headers=headers,<br>        json={<br>            &quot;access_control_list&quot;: [{<br>                &quot;service_principal_name&quot;: sp_app_id,<br>                &quot;permission_level&quot;: &quot;CAN_RUN&quot;<br>            }]<br>        },<br>        timeout=15<br>    ).raise_for_status()</pre><p>To grant SELECT permissions on the data, you can use SQL:</p><pre>GRANT USE CATALOG ON CATALOG main TO `&lt;service_principal&gt;`;<br>GRANT USE SCHEMA ON SCHEMA main.sales TO `&lt;service_principal&gt;`;<br>GRANT SELECT ON ALL TABLES IN SCHEMA main.sales TO `&lt;service_principal&gt;`</pre><p>At this point, the SP has sufficient permissions to function (to query the underlying tables of the Genie space). However, we still have not defined the guardrails that control what data it can actually see. This is where ABAC policies come into play, enforcing which rows and columns are visible to the SP at query time.</p><h4>Step 4: Enforce tenant isolation with ABAC</h4><p>With identities and permissions in place, you can now enforce the actual tenant boundary. Because each tenant has its own SP, you can write ABAC policies that evaluate the current identity directly.</p><p>A common pattern is to maintain a mapping table between SPs and tenant IDs, then use a function that checks whether the current caller is authorized for a given row. For example:</p><pre>CREATE OR REPLACE FUNCTION main.sec.filter_by_tenant(tenant_id STRING)<br>RETURNS BOOLEAN<br>RETURN EXISTS (<br>  SELECT 1<br>  FROM main.sec.tenant_acl t<br>  WHERE t.tenant_id = tenant_id<br>    AND t.principal = current_user()<br>);</pre><p>You can then apply that function as a row filter:</p><pre>ALTER TABLE main.sales.customer_orders<br>SET ROW FILTER main.sec.filter_by_tenant ON (tenant_id);</pre><p>At that point, queries are automatically scoped at execution time. It does not matter whether the SQL was written by hand or generated by Genie — if the current SP is not allowed to see a row, that row will not be returned.</p><p>This is one of the strongest aspects of the pattern: enforcement stays close to the data rather than in the application layer.</p><p>Column masking follows the same idea. If you have fields such as SSNs, account numbers, or other sensitive attributes, you can apply a masking policy so those values are exposed only when the SP is explicitly authorized.</p><h4>Step 5: Curate the Genie space for quality answers</h4><p>Once the security model for your tenants is in place, you can focus on improving the quality of responses in your Genie space. A Genie space is only as good as the data model and business context it understands, so it is worth spending time curating it.</p><p>Focus on adding clear table and column descriptions, defining joins and key measures, and including business-friendly synonyms or metadata so Genie can interpret questions accurately. Provide a small set of high-quality example queries — typically five to ten — to demonstrate common questions and correct SQL patterns. Keep the space focused on a small number of well-understood tables, and use a Serverless SQL Warehouse when available for better performance and scalability.</p><p>See <em>Curate an effective Genie space</em> (<a href="https://docs.databricks.com/aws/en/genie/best-practices">AWS</a> | <a href="https://learn.microsoft.com/en-us/azure/databricks/genie/best-practices">Azure</a> | <a href="https://docs.databricks.com/gcp/en/genie/best-practices">GCP</a>) for more best practices defining a new space.</p><h4>Step 6: Authenticate per tenant at runtime</h4><p>With provisioning complete, your runtime path becomes straightforward. When the application receives a request for a given tenant, it loads that tenant’s stored credentials and exchanges them for a Databricks access token using the OAuth client-credentials flow.</p><p>A simple example looks like this:</p><pre>import requests<br><br>ACCOUNT_HOST = &quot;https://accounts.cloud.databricks.com&quot;<br>ACCOUNT_ID = &quot;&lt;your_account_id&gt;&quot;<br>CLIENT_ID = &quot;&lt;tenant_client_id&gt;&quot;<br>CLIENT_SECRET = &quot;&lt;tenant_secret&gt;&quot;<br><br>resp = requests.post(<br>    f&quot;{ACCOUNT_HOST}/oidc/accounts/{ACCOUNT_ID}/v1/token&quot;,<br>    auth=(CLIENT_ID, CLIENT_SECRET),<br>    data={&quot;grant_type&quot;: &quot;client_credentials&quot;, &quot;scope&quot;: &quot;all-apis&quot;},<br>    timeout=15<br>)<br><br>resp.raise_for_status()<br>token = resp.json()[&quot;access_token&quot;]</pre><p>These tokens are short-lived (roughly one hour), so your application should cache and refresh them automatically rather than requesting a new token for every call. If you are using the Databricks SDK, token handling is typically abstracted for you.</p><p>At this point, you have everything you need for secure tenant-scoped execution: a tenant-specific SP, a short-lived access token, and ABAC policies that evaluate that identity when the query runs.</p><h4>Step 7: Call the Genie conversation API</h4><p>You can now call Genie. The API is stateful, so the typical pattern is to start a conversation, receive a conversation ID and message ID, and then poll until the message completes and results are available.</p><p>The simplified flow looks like this:</p><pre>import time<br>import requests<br><br>WORKSPACE_HOST = &quot;https://&lt;your-workspace-instance&gt;.cloud.databricks.com&quot;<br>WORKSPACE_TOKEN = os.environ[&quot;DATABRICKS_WORKSPACE_TOKEN&quot;]  <br>SPACE_ID = &quot;&lt;your_genie_space_id&gt;&quot;<br><br>headers = {&quot;Authorization&quot;: f&quot;Bearer {WORKSPACE_TOKEN}&quot;}<br><br># 1. Start the Genie conversation<br>start = requests.post(<br>    f&quot;{WORKSPACE_HOST}/api/2.0/genie/spaces/{SPACE_ID}/start-conversation&quot;,<br>    json={&quot;content&quot;: &quot;Top customers by revenue last month&quot;},<br>    headers=headers,<br>    timeout=15<br>)<br>start.raise_for_status()<br><br># Extract conversation and message IDs <br>conversation_id = start.json()[&quot;conversation&quot;][&quot;id&quot;]<br>message_id = start_data[&quot;message&quot;][&quot;id&quot;]<br><br><br># 2. Poll for the execution to finish<br>for _ in range(60):<br>    poll = requests.get(<br>        f&quot;{WORKSPACE_HOST}/api/2.0/genie/spaces/{SPACE_ID}/conversations/&quot;<br>        f&quot;{conversation_id}/messages/{message_id}&quot;,<br>        headers=headers,<br>        timeout=15<br>    )<br>    poll.raise_for_status()<br><br>    status = poll.json()[&quot;status&quot;]<br><br>    if status == &quot;COMPLETED&quot;:<br>        break<br><br>    time.sleep(2)</pre><p>In production, you will want to add robust retry logic, exponential backoff, timeout handling, and monitoring around failures or API limits. But conceptually, this is the full runtime loop: authenticate as the tenant, call Genie, let Genie generate SQL, and rely on ABAC to enforce tenant boundaries during execution.</p><p>Check out <em>Best practices for using the Genie API</em> (<a href="https://docs.databricks.com/aws/en/genie/conversation-api#best-practices-for-using-the-genie-api">AWS</a> | <a href="https://learn.microsoft.com/en-us/azure/databricks/genie/conversation-api">Azure</a> | <a href="https://docs.databricks.com/gcp/en/genie/conversation-api#best-practices-for-using-the-genie-api">GCP</a>) to maintain performance and reliability when using the Genie API.</p><h3>Operating this pattern in production</h3><p>Once your core flow is live, the heavy lifting shifts from initial implementation to ongoing operations. To keep your system secure, efficient, and easy to maintain, focus on four key areas.</p><h4>1. Secret lifecycle management</h4><p>Secrets are never a one-time setup step. They require regular rotation, with a reasonable rule of thumb being every 60 to 180 days. Fortunately, Databricks supports multiple active secrets per SP, enabling zero-downtime rotation. The overlap between old and new secrets is what keeps your production environment safe during the transition.</p><p>A step-by-step flow for rotating your secrets:</p><ul><li>Create a new secret and store it securely in your vault</li><li>Update your application to use this newly generated secret</li><li>Verify that all traffic has successfully switched over</li><li>Revoke the old secret only after confirming the new one works</li></ul><h4>2. Clean deprovisioning</h4><p>When a tenant is offboarded, you should carefully reverse the initial setup steps. Keeping this teardown process as part of your standard control-plane lifecycle makes the entire system much easier to reason about over time. Be sure to revoke active secrets, remove permissions, and optionally delete the SP entirely.</p><h4>3. Observability and Genie space hygiene</h4><p>As your platform usage grows, you need deep visibility into how the system is behaving. Keep a close eye on query patterns, performance bottlenecks, conversation volume, and audit signals tied to policy enforcement. You also need to maintain good space hygiene by deleting old records, especially since spaces often have strict conversation limits.</p><p><em>Note: As of the date of this publication, a Genie space has a limit of 10,000 conversations.</em></p><h4>4. Evolving Your ABAC Model</h4><p>Finally, expect your ABAC model to evolve as your data model changes. New tables, new sensitive fields, and new use cases will almost certainly require updates to your filters and masks. That is normal. The advantage of this architecture is that these changes occur in the governed policy and metadata layers, rather than by rewriting application filtering logic.</p><h3>Operating this pattern in production</h3><p>What makes this pattern highly effective is its clean separation of responsibilities, decoupling application logic from data governance. Service principals establish clear tenant identities, while OAuth provides a secure mechanism for your application to assume those identities at runtime. At the data layer, Unity Catalog ABAC policies dynamically enforce row and column-level access rules, freeing your application from managing complex, error-prone tenant filters. By combining these robust governance guardrails with the Genie Conversation API, you can confidently scale self-service analytics and securely deliver a rich, natural language data experience to external users directly within your application. Ultimately, this approach yields a multi-tenant architecture that is fundamentally easier to secure, scale, and maintain than pushing tenant filters deep into application code.</p><h3>Resources</h3><ul><li>Genie Conversation <a href="https://docs.databricks.com/api/workspace/genie">APIs</a>/<a href="https://databricks-sdk-py.readthedocs.io/en/latest/workspace/dashboards/genie.html">SDK</a></li><li>For more on embedding Genie into your custom web apps with Genie Conversation APIs, check out <a href="https://www.databricks.com/blog/access-genie-everywhere">Use Genie Everywhere with Enterprise OAuth</a>.</li><li>For more on building a Genie space from scratch to production-ready through benchmark evaluation and systematic optimization, check out <a href="https://www.databricks.com/blog/how-build-production-ready-genie-spaces-and-build-trust-along-way">How to Build Production-Ready Genie Spaces, and Build Trust Along the Way</a>.</li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=d307bfbfc89b" width="1" height="1" alt=""><hr><p><a href="https://medium.com/dbsql-sme-engineering/embedding-genie-api-for-a-multi-tenant-application-d307bfbfc89b">Embedding Genie API for a Multi‑Tenant Application</a> was originally published in <a href="https://medium.com/dbsql-sme-engineering">DBSQL SME Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Databricks Genie Code Pricing: The Real Cost of Agentic AI]]></title>
            <link>https://medium.com/dbsql-sme-engineering/genie-code-databricks-agentic-ai-the-price-of-intelligence-32a7bc477cba?source=rss----a346361fdb66---4</link>
            <guid isPermaLink="false">https://medium.com/p/32a7bc477cba</guid>
            <category><![CDATA[cloud-pricing]]></category>
            <category><![CDATA[agentic-ai]]></category>
            <category><![CDATA[databricks-genie-code]]></category>
            <category><![CDATA[genie]]></category>
            <category><![CDATA[databricks]]></category>
            <dc:creator><![CDATA[Maksim Pachkouski Databricks MVP]]></dc:creator>
            <pubDate>Tue, 17 Mar 2026 21:57:28 GMT</pubDate>
            <atom:updated>2026-08-15T13:03:31.336Z</atom:updated>
            <content:encoded><![CDATA[<p>In <strong>March 2026,</strong> Databricks officially replaced <strong>Databricks Assistant</strong> with <strong>Genie Code</strong>. This may seem like a name change, but it&#39;s actually a new assistant that runs in agent mode.</p><p>New features, new costs. In this article, we&#39;ll explore <strong>Genie Code</strong> pricing. We&#39;ll also look at when it can be free and when it can exceed the stated price.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*YqVRm3oSSgKqTz9C_aW5Eg.jpeg" /></figure><p>The main difference between cloud solutions is that enabling a new feature often incurs additional costs. I<a href="https://medium.com/@protmaks">&#39;ve already written several articles about the cost of Databricks</a>. And I was curious about the cost of using Genie Code.</p><p>Unfortunately, there&#39;s currently no convenient expense monitoring dashboard that would show exactly where money is being spent. There are only a few dashboards, but they provide a very high-level overview. However, if you&#39;re interested, you can find everything you need in Databricks Logs or system tables.</p><p>I ran some experiments with running Genie Code, and here&#39;s what the billing showed.</p><h4>Experiment 1: Running Genie Code to write a text article.</h4><p>I tried actively using Genie Code for 10 minutes without searching the Unity catalog or running queries. I asked him to write an article. It was clear he&#39;d been poring over some Databricks documentation and wrote a 14,955-character article:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*keaLukb8k2uj0gOtfP1-fg.png" /></figure><p>From the logs (system.access.assistant_events), it is clear that during this time, it managed to create 15 events, and all of this was almost free:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/849/1*4QqPBeWsfcL5Moa2QK8qqA.png" /></figure><h4>Experiment 2: Running Genie Code to write a script.</h4><p>During the second experiment, I asked him to write a script, and he came up with a plan:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*feYVeP-BzJomr-UpTstB5Q.png" /></figure><p>I also performed selections from tables and analyzed the results:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/834/1*OH6EBtzEtLYCENXCRy-VvA.png" /></figure><p>The experiment lasted 30 minutes, including adjustments, and, according to the logs, 79 events occurred.</p><p>In Databricks, all costs are displayed as DBU per hour, and we calculate the cost based on the pricing. When Genie starts, a separate cluster of the <strong>PREMIUM_ALL_PURPOSE_SERVERLESS_COMPUTE_*</strong> type is launched, and its cost is currently approximately <strong>$0.75 per DBU</strong>, depending on the region and cloud provider.</p><p>But in the end, the amount was approximately equal to 2 DBU:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/768/1*PjwAaxvnH48e-m2_LeKW0Q.png" /></figure><p>After analyzing the detailed logs (system.access.audit), which contained over 900 events during this period, I found an interesting line:</p><pre>{<br>  &quot;virtual_cluster_size&quot;:&quot;VirtualMedium&quot;,<br>  &quot;billing_info&quot;:<br>    {&quot;context&quot;:<br>      {<br>        &quot;notebook_name&quot;:&quot;/Users/myuser@gmail.com/.conversations/___DBCONVERSATION___(): Databricks ...&quot;,<br>        &quot;notebook_id&quot;:&quot;935626676034490&quot;<br>      }<br>     },<br>     &quot;custom_tags&quot;:{&quot;NotebookId&quot;:&quot;935626676034490&quot;},<br>     &quot;cluster_creator&quot;:&quot;REPL_LAUNCHER&quot;,<br>     &quot;kind&quot;:&quot;SERVERLESS_REPL_VM&quot;,<br>     &quot;autotermination_minutes&quot;:&quot;15&quot;,<br>     &quot;idempotency_token&quot;:&quot;serverless-***********************&quot;,<br>     &quot;platform_channel&quot;:&quot;CLIENT-4&quot;<br>}</pre><p>Now we see that Genie Code spun up a separate <strong>VirtualMedium-</strong>sized cluster with <strong>an autotermination_minutes</strong> setting of <strong>15 minutes</strong> to fulfill queries.</p><h4><strong>The tracking problem.</strong></h4><p>When I was running the experiment, I didn&#39;t run anything on Databricks that day except Genie Code to find its billing.</p><p>Currently, billing shows that the cluster was launched by the user themselves, not Genie Code. Unfortunately, the cost breakdown doesn’t include any additional labels or tags.<br>If your company uses clusters with tags and cost centers, you’ll need to add more users to accurately distribute them among teams.</p><blockquote>Quite a big change coming to Databricks Genie. Starting <strong>July 8, 2026</strong>, <a href="https://medium.com/p/1d40bf8a4aad">Genie product usage beyond the free monthly allowance will move to pay-as-you-go billing</a>. The usage is billed based on underlying LLM usage in DBUs, and budgets can be set up to track/control spend.</blockquote><p>The transition from Assistant to Genie Code is a huge leap in productivity. Business users gain independence from data engineers for basic analytics, and engineers are freed from writing routine ad-hoc queries.</p><p>However, when an Agent appears instead of copying code from the Assistant, and can radically change your script, you need to be able to work with version control. Fortunately, Databricks already has built-in versioning for Notebooks, but it&#39;s best to connect to GitHub and commit successful scripts. Also, remember that if you give the Agent complete freedom, it can delete data in tables, jobs, and so on, or create a script that wastes your budget. It&#39;s a handy assistant that requires control.</p><p><em>Author:</em> <strong>Maksim Pachkouski</strong> [Databricks MVP &amp; Databricks Solutions Architect Champion]. Follow my posts on <a href="https://medium.com/@protmaks">Medium</a> and <a href="https://www.linkedin.com/in/protmaks/">LinkedIn</a> to stay up-to-date on the latest news in Data Engineering and learn about best practices.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=32a7bc477cba" width="1" height="1" alt=""><hr><p><a href="https://medium.com/dbsql-sme-engineering/genie-code-databricks-agentic-ai-the-price-of-intelligence-32a7bc477cba">Databricks Genie Code Pricing: The Real Cost of Agentic AI</a> was originally published in <a href="https://medium.com/dbsql-sme-engineering">DBSQL SME Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Unlock Deep Observability & Cost Tracking for Databricks Genie]]></title>
            <link>https://medium.com/dbsql-sme-engineering/unlock-deep-observability-cost-tracking-for-databricks-genie-6c1b2a732d58?source=rss----a346361fdb66---4</link>
            <guid isPermaLink="false">https://medium.com/p/6c1b2a732d58</guid>
            <category><![CDATA[genie]]></category>
            <category><![CDATA[bi]]></category>
            <category><![CDATA[business-intelligence]]></category>
            <category><![CDATA[databricks]]></category>
            <category><![CDATA[bi-on-databricks]]></category>
            <dc:creator><![CDATA[Krishnan Gopakumar]]></dc:creator>
            <pubDate>Tue, 17 Feb 2026 16:35:43 GMT</pubDate>
            <atom:updated>2026-02-17T16:35:41.909Z</atom:updated>
            <content:encoded><![CDATA[<p>Author: Krishnan Gopakumar</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*LqjBAn8rLGi9sql9FsI5-A.png" /></figure><p>Databricks Genie is transforming how teams interact with data using natural language. But as adoption grows, so does the need for visibility: <em>How are users interacting with your Genie spaces? What is the cost per query? Where are the friction points?</em></p><p>This blog (along with the accompanying <strong>Databricks Notebook</strong>) provides an approach to building a scalable, end-to-end observability workflow that answers these questions and much more. By leveraging <strong>Genie REST APIs</strong> and <strong>Databricks System Tables</strong>, this solution creates two powerful datasets that give you full transparency into your AI/BI environment. These tables can then be a part of your observability toolkit on Databricks!</p><h3>How to use this blog</h3><p>Consider this blog your launchpad for Genie observability techniques. It covers the essential logic you need to account for when building this workflow. The accompanying notebook contains all the code required to start experimenting immediately.</p><p><strong><em>Pro Tip:</em></strong><em> Leverage </em><strong><em>Databricks Assistant</em></strong><em> to reverse engineer the code provided in the notebook to make it truly your own!</em></p><p>For Genie Space Administrators, these tables should be more than logs. They are a roadmap for evolving your data strategy. By analysing the <em>genie_observability_main_table</em> and <em>dbsql_cost_per_query_table</em>, you can drive continuous improvement in three key areas:</p><h4>1. Refine Your Semantic Layer</h4><p>User questions are the best indicator of what is missing from your data model. Use these insights to close the gap between user intent and available data:</p><ul><li><strong>Curate with Precision:</strong> Identify questions where Genie struggled or failed. Use this feedback to add missing tables, define better join relationships, or provide specific <a href="https://docs.databricks.com/aws/en/genie/trusted-assets"><strong>SQL examples</strong></a> to guide Genie’s reasoning.</li><li><strong>Standardise Metrics:</strong> Spot recurring questions about specific KPIs (e.g., “churn rate” or “daily active users”). Centralise, promote and govern these common definitions into <a href="https://docs.databricks.com/aws/en/metric-views/"><strong>Databricks Metric Views</strong></a>, ensuring consistent logic across the entire organisation.</li></ul><h4>2. Optimise Engineering Patterns (“Shift Left”)</h4><p>Observability helps you spot performance bottlenecks early. If you notice Genie frequently generating complex, expensive joins to answer simple questions, it might be time to <strong>“shift left”</strong>:</p><ul><li>Move semantic modeling logic upstream in your ETL pipeline.</li><li>Pre-aggregate data or denormalise tables in the Silver/Gold layer to improve query performance and reduce latency for end users.</li></ul><h4>3. Master Operational Efficiency</h4><p>With granular cost attribution, you can conduct more accurate cost analysis for chargebacks and usage monitoring:</p><ul><li><strong>Attribute Costs:</strong> Accurately charge back compute costs to specific departments or Genie spaces based on actual DBU consumption.</li><li><strong>Manage Capacity:</strong> Identify peak usage times to ensure your SQL Warehouses are sized correctly, balancing performance with cost.</li></ul><h3>What this blog achieves</h3><p>Throughout this post, we will demonstrate how to create two tables that are critical to Genie observability.</p><h4>1. genie_observability_main_table</h4><ul><li><strong>What it tracks:</strong> Every message sent to your Genie spaces, including user questions, Genie’s natural language responses, and the generated SQL.</li><li><strong>Why it matters:</strong> It captures critical metadata like user feedback (thumbs up/down), error messages, and SQL statement IDs, etc, enabling you to pinpoint quality issues and improve Genie’s accuracy.</li></ul><h4>2. dbsql_cost_per_query_table</h4><ul><li><strong>What it tracks:</strong> Granular cost attribution for every SQL query executed across your workspaces.</li><li><strong>Why it matters:</strong> This allows you to attribute costs to Genie Spaces, by filtering out SQL queries and their costs that originate from a given Genie Space. You can now see the exact <strong>DBU consumption</strong> and <strong>USD cost</strong> for every question asked, simplifying chargebacks and ROI analysis.</li></ul><h3>Pre-requisites:</h3><ul><li>This notebook/workflow is targeted towards a Genie space manager.</li><li>Entitlements: You must have the Databricks SQL workspace entitlement. See <a href="https://docs.databricks.com/aws/en/security/auth/entitlements">Manage entitlements</a>.</li><li>Compute: CAN USE access on at least one pro or serverless SQL warehouse.</li><li>Data access: SELECT privileges on the data used in the space.</li><li>Genie space ACLs: At least CAN EDIT permissions on the Genie space. Genie space creators automatically have CAN MANAGE permissions on spaces they create. See <a href="https://docs.databricks.com/aws/en/security/auth/access-control/#genie-space-acls">Genie space ACLs</a>.</li><li>You should have enough permissions to create tables in a schema within a catalog of your choice</li><li>Access to system tables is governed by Unity Catalog. Account admins have access to system tables by default. To allow a user to query system tables, an admin must grant that user USE and SELECT permissions on the system schemas.</li></ul><h3>Part 1: Capturing the Conversation (The API Layer)</h3><p>To build the genie_observability_main_table, we need to extract the raw conversation history. We simplify this process by using the databricks-sdk for Python. The SDK handles authentication, pagination, and data typing automatically, making the extraction code cleaner and more robust than raw API calls. <strong>Note: </strong>The Databricks notebook also has an implementation that uses the python requests library.</p><p>Here is a snippet of the logic used to fetch and parse the messages using the WorkspaceClient:</p><pre>from databricks.sdk import WorkspaceClient<br><br>def get_genie_observability_table(space_id: str):<br>    w = WorkspaceClient()<br>    <br>    # Get space metadata<br>    space = w.genie.get_space(space_id=space_id)<br>    space_name = space.title<br>    <br>    records = []<br>    <br>    # 1. List all conversations (SDK handles pagination automatically)<br>    # We set include_all=True to fetch conversations from all users<br>    conversations = w.genie.list_conversations(space_id=space_id, include_all=True)<br>    <br>    for conv in conversations:<br>        # 2. List all messages within the conversation<br>        messages = w.genie.list_conversation_messages(<br>            space_id=space_id, <br>            conversation_id=conv.conversation_id<br>        )<br>        <br>        for msg in messages:<br>            # 3. Flatten the message object into a row for our table<br>            # (Helper function _extract_message_data parses attachments &amp; feedback)<br>            record = _extract_message_data(msg, space_id, space_name, w)<br>            records.append(record)<br>            <br>    # Convert list of records to Spark DataFrame<br>    schema = _get_schema()<br>    return spark.createDataFrame(records, schema=schema)</pre><h3>Part 2: Attributing the Cost (The System Tables Layer)</h3><p>Genie spaces incur costs based on the underlying compute (Databricks SQL Warehouse) used to execute the SQL generated from natural language queries. Because Databricks SQL bills for warehouse uptime rather than per-query execution, the “cost per query” is a derived approximation.</p><p>Accurately attributing this cost is complex, as multiple queries often run concurrently on a shared warehouse. To address this, we employ a <strong>“Time-Weighted” attribution strategy</strong>. This method calculates the total cost of a warehouse during a specific window and distributes it based on each query’s execution duration relative to the warehouse’s total active workload.</p><p>Link to the full SQL Script for calculating the Cost per Query is available <a href="https://github.com/databrickslabs/sandbox/blob/main/dbsql/cost_per_query/PrPr/DBSQL%20Cost%20Per%20Query%20MV%20(PrPr).sql">here</a>.</p><p>We run this script to create the dbsql_cost_per_query_table table.</p><p>To isolate costs for a specific Genie space, we utilise the query_source_id column in the <em>dbsql_cost_query_table</em> table. For queries originating from Genie, this ID corresponds directly to the unique space_id of the Genie space.</p><p>We attribute the final costs by performing an INNER JOIN between the cost table and the Genie space table (on query_source_id = space_id), followed by a SELECT DISTINCT operation to produce the final, accurate dataset.</p><h3>Visualising Insights: Building the Dashboard &amp; Meta-Genie</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*serZQqRDJJyJ8DYdNwPh1A.png" /></figure><p>Once the notebook execution is complete, your two main tables are populated and ready to drive actionable insights. You can immediately leverage these to power a structured Databricks AI/BI Dashboard, or even create a “Meta-Genie” Space — pointing a new Genie space at these tables to ask questions about your Genie usage in natural language (<em>Genie-ception</em>).</p><p>To get started with a Dashboard, create a new AI/BI Dashboard and use the following SQL snippets to define your datasets. You can also view the dashboard JSON <a href="https://github.com/Dynosphere/aibi-adoption-dashboard/blob/main/src/dashboards/lh_adoption_dashboard.lvdash.json">here</a>.</p><h4>Dataset 1: Engagement &amp; Adoption</h4><p>Use this dataset to visualise how end users are interacting with your Genie spaces. Key metrics to track include the number of unique users, conversation volume, and message distribution over time.</p><pre>USE CATALOG IDENTIFIER(:catalog);<br>USE SCHEMA IDENTIFIER(:schema);<br>SELECT * FROM genie_observability_main_table<br>WHERE created_datetime &gt;= :param.min<br>AND created_datetime &lt;= :param.max<br>AND (<br>:space_id = &#39;ALL&#39;<br>OR space_id = :space_id<br>)<br>ORDER BY<br>created_datetime DESC</pre><h4>Dataset 2: Cost Analysis</h4><p>Use this dataset to visualise cost per user, cost per space, expensive query identification, and general ROI metrics.</p><pre>USE CATALOG IDENTIFIER(:catalog);<br>USE SCHEMA IDENTIFIER(:schema);<br>SELECT DISTINCT<br>c.*<br>FROM dbsql_cost_per_query_table c<br>INNER JOIN genie_observability_main_table o<br>ON c.query_source_id = o.space_id<br>WHERE c.start_time &gt;= :param.min<br>AND c.start_time &lt;= :param.max<br>AND (<br>:genie_space_id = &#39;ALL&#39;<br>OR o.space_id = :genie_space_id<br>);</pre><h3>Get Started</h3><p>Ready to implement this in your environment? You can find the full source code, including the Python extraction logic and the complete SQL cost modelling in a Databricks Notebook format in our repository.</p><p><strong>Link to Notebook: </strong><a href="https://github.com/Dynosphere/aibi-adoption-dashboard/blob/main/src/standalone_resources/genie_observability/genie_observability_deep_dive.ipynb"><strong>LINK</strong></a></p><h3><strong>Explore Wider Observability</strong></h3><p>Genie is just one part of the Databricks Data Intelligence Platform. If you are interested in broader observability tooling, including Apps monitoring, Model Serving endpoint monitoring and more, explore our full Observability repository here: <a href="https://github.com/Dynosphere/aibi-adoption-dashboard/tree/main">LINK</a></p><h4>Repo Contributors: <a href="https://www.linkedin.com/in/samuel-le-corre/">Sam Le Corre</a> and <a href="https://www.linkedin.com/in/mike-dobing-88b5651b/">Mike Dobing</a></h4><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=6c1b2a732d58" width="1" height="1" alt=""><hr><p><a href="https://medium.com/dbsql-sme-engineering/unlock-deep-observability-cost-tracking-for-databricks-genie-6c1b2a732d58">Unlock Deep Observability &amp; Cost Tracking for Databricks Genie</a> was originally published in <a href="https://medium.com/dbsql-sme-engineering">DBSQL SME Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Level of Detail Expressions in AI/BI: A Practical Guide]]></title>
            <link>https://medium.com/dbsql-sme-engineering/level-of-detail-expressions-in-ai-bi-a-practical-guide-fec6e11c9deb?source=rss----a346361fdb66---4</link>
            <guid isPermaLink="false">https://medium.com/p/fec6e11c9deb</guid>
            <category><![CDATA[business-intelligence]]></category>
            <category><![CDATA[databricks]]></category>
            <category><![CDATA[data]]></category>
            <category><![CDATA[dashboard]]></category>
            <category><![CDATA[databricks-sql]]></category>
            <dc:creator><![CDATA[Justin Kolpak]]></dc:creator>
            <pubDate>Thu, 29 Jan 2026 14:23:36 GMT</pubDate>
            <atom:updated>2026-01-29T14:23:35.506Z</atom:updated>
            <content:encoded><![CDATA[<h3>Level of Detail Expressions in Databricks AI/BI: A Practical Guide</h3><h3>Introduction</h3><p><a href="https://docs.databricks.com/aws/en/dashboards/custom-calculations/level-of-detail">Level of detail expressions</a> are now available in Databricks AI/BI Dashboards, which unlock a whole array of interesting analytical questions we can now answer directly within AI/BI.</p><blockquote>Level of detail expressions let you specify a granularity at which aggregations are calculated which is different than the level of detail of your visualizations.</blockquote><p>For me personally, this is one of those concepts that sounds really simple at first blush, but once you get into the details, dealing with calculations and visualizations that simultaneously involve multiple levels of granularity can get pretty tricky. Because of that, I wanted to walk through a basic, concrete example in a good amount of detail, to demonstrate the mechanics behind these expressions and ultimately build an intuition for when and how to use them properly.</p><p>To demonstrate, we will build a basic visualization based on the samples.bakehouse.sales_transactions table — this is a sample dataset that’s available within every Databricks workspace. The dashboard artifacts are available on <a href="https://github.com/justinkolpak-databricks/databricks-dwh-resources/tree/main/level-of-detail-expressions">this GitHub repo</a> — feel free to try it on your own!</p><blockquote>Note: This demo shows how to build level of detail expressions as <a href="https://docs.databricks.com/aws/en/dashboards/custom-calculations/">custom calculations</a>, which are local to an AI/BI dashboard. If these expressions need to be reused across multiple dashboards and use cases (which in a real-life context, they probably do!), the expressions can be defined in a <a href="https://docs.databricks.com/aws/en/metric-views/">Unity Catalog Metric View</a> once available.</blockquote><p>For the purposes of this blog, I’ll use a small sample of the dataset, just to make the math easier to follow. Here’s the dataset we’ll use for the following examples:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/630/1*OdgYa0Z7i_gazdoJD_6WDA.png" /><figcaption>Sample Dataset</figcaption></figure><h3>Setting the stage</h3><p>Objective: I’m the VP of Operations at the Bakehouse company, a popular cookie franchise. I’m interested in doing an analysis on how the different franchises perform relative to one another, so I work with my team to come up with metrics to measure performance. Initially, I want to focus on Average Sale Prices of the product and how they compare across franchises:</p><ul><li>Average Sales Price (ASP) for a single franchise</li><li>Average Sales Price (ASP) across all franchises (global)</li><li>Difference between ASP for a single franchise and all franchises: Provides a relative measure of how a franchise’s ASP compares to the average</li></ul><p>I see that these metrics require dealing with 2 different levels of granularity: at the franchise level and at the global level (all franchises). For me, that’s an indication that I should consider using a level of detail expression (from now on, I’ll abbreviate to LOD expression), since they’re designed for working with multiple levels of granularity (or, levels of detail; hence the name 🙂)</p><p>There are two ways we can go about this, which align with the Fixed and Coarse level-of-detail calculations available in AI/BI.</p><h3>Primer on important concepts:</h3><p>Before we get deep into the nuances of LOD concepts, let’s take a quick aside to explain three important, foundational concepts:</p><h4>Fields vs. Measures:</h4><p>There are generally two types of calculations in AI/BI (and any other BI tool, for that matter):</p><ul><li>Fields: Row-level transformations. Conceptually, these can be calculated whenever the source dataset is refreshed.</li><li>Measures: Calculations that aggregate something to a different level of granularity (e.g. AVG, SUM, etc.). Conceptually, these can only be calculated at runtime, since the level of granularity that the aggregation needs to consider will depend on the visualization grouping or filters from the dashboard.</li></ul><h4>Visualization groupings vs. filters</h4><ul><li>When I use the term <strong>“visualization groupings”</strong>, I’m referring to the fields that are used in the visualization, which tell the visualization the granularity at which to calculate the Measures.</li><li><strong>Filters</strong>, in this case, refers to a simple Filter that would apply to a visualization to control the subset of data the visualization can display.</li></ul><p>Let me show you a concrete example to illustrate how these work. Here are two pivot tables, both calculating the Average of the totalPrice field (we’ll call this <strong>Average Sales Price</strong>):</p><p>Notice that in both visuals, the ASP of Biscotti is $4. They both effectively filter to product = “Biscotti”, but they achieve this differently:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ZJQI8haYul6HVUwHkfKvBg.png" /><figcaption>Table 1</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*TkqULOsGbAqjuB8NZfNpAw.png" /><figcaption>Table 2</figcaption></figure><p>Table 1 (Visualization Grouping):</p><ul><li>The product field is added to the Rows of the pivot table</li><li>Each “row” in the table represents a different product</li><li>For the Biscotti row specifically, only Biscotti transactions are aggregated</li><li>The filtering happens implicitly based on which row we’re looking at</li></ul><p>Table 2 (Filter):</p><ul><li>No fields are added to the Rows (no visualization grouping)</li><li>A dashboard filter is applied: product = “Biscotti”</li><li>All records in the underlying dataset are filtered to just Biscotti transactions</li><li>Then the aggregation is applied to what remains</li></ul><p><strong>Key takeaway: </strong>From a calculation perspective, visualization groupings and filters operate the same way. They both control which subset of data gets aggregated. The difference is:</p><ul><li><strong>Visualization groupings</strong> apply filtering per row in the visual (e.g., each product gets its own filtered aggregation)</li><li><strong>Filters</strong> apply filtering to all rows in the visual at once (e.g., the entire visual only sees Biscotti data)</li></ul><p>Since they function equivalently for calculation purposes, I’ll use filters in the examples below to keep things simple.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*BgUJVlJ0g9EzxNPK.jpg" /><figcaption>Biscotti (Source: allrecipes.com)</figcaption></figure><h4>Mental model for how visualization groupings &amp; filters impact measures</h4><p>The section above reveals a helpful mental model to think about how filters (and visualization groupings) impact a measure calculation:</p><p><strong>Step 1 — Filter the data:</strong> The filter condition is applied to the underlying dataset, including any calculated fields. This can be:</p><ul><li>The whole dataset (in the context of a dashboard filter), or</li><li>A subset relevant to a specific row (in the context of a visualization grouping)</li></ul><p><strong>Step 2 — Aggregate:</strong> With the filtered dataset in hand, apply the Measure’s aggregation logic (AVG, SUM, etc.)</p><p>Keep this two-step model in mind as we explore how LOD expressions work with our sample dataset.</p><h3>Fixed Level-of-Detail Expressions:</h3><h4>Defining the expressions</h4><p>We’ll start out by creating a Pivot Table, since it’s easier to see the values and how they’re calculated. I know it’s boring, but trust me, it will make it easier to see how the calculations work. Once we build a Pivot Table, we can use these expressions in any visual type we see fit.</p><p>Let’s get started:</p><ol><li>First, let’s create the franchise-level Average Sales Price measure that we’ll compare against this global average. Create a Measure called <em>asp</em> (short for Average Sales Price) with the syntax:</li></ol><pre>AVG(totalPrice)</pre><p>Create a pivot table with <em>franchiseID</em> on the Rows, and <em>asp</em> on the Values.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Rho5-62iR7oGnibu5kPicQ.png" /></figure><p>How it’s calculated: This is super straightforward. For each franchiseID in the dataset, the visualization layer calculates the average of the totalPrice field.</p><p>2. Next, we’ll calculate the Average Sales Price across all franchises, as a <em>fixed level-of-detail expression:</em></p><p>Fixed level-of-detail expressions are <a href="https://docs.databricks.com/aws/en/dashboards/custom-calculations/level-of-detail#what-are-level-of-detail-expressions">defined</a> as expression which:</p><blockquote>Aggregate over pre-defined set of dimensions specified in the expression itself, ignoring all other dimensions in the visualization.</blockquote><p>Let’s create the following fixed LOD expression to define the average sales price across all franchises.</p><p>Create a custom calculation called <em>asp_all_franchises_fixed</em>. This will create a “Field” (as opposed to a “Measure” in the Dataset), which means it can be calculated for each row in the dataset (whereas “Measures” are aggregated in the context of the visual).</p><pre>AVG(totalPrice) OVER ()</pre><p>Here’s what the dataset looks like with the new calculated field:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*JdvPQLcTfOgSSiegEmJEmQ.png" /></figure><p>Intuitively, this tells us that the Average Sales Price across all franchises is $4.50.</p><p>3. Finally, let’s put it all together with the comparison between the Average Sales Price for <em>this franchise vs. </em>the global average.</p><p>Let’s create a custom calculation called <em>asp_diff_fixed</em> with the following syntax:</p><pre>MEASURE(asp) - ANY_VALUE(asp_all_franchises_fixed)</pre><p>This custom calculation produces a Measure, which means it is an aggregated value which is calculated at runtime, in the context of the visualization’s dimensions and any relevant filters. Because the <em>asp_all_franchises_fixed</em> field is <em>fixed</em> across all rows in this dataset, we can use the ANY_VALUE function to allow this custom calculation to be used in a measure calculation (see <a href="https://docs.databricks.com/aws/en/dashboards/custom-calculations/level-of-detail#example-percent-of-total">docs</a> for reference).</p><p>Intuitively, this tells us the difference between the average sales price for <em>this franchise</em> ($4, for Franchise 1) and the average sales price for <em>all franchises </em>($4.50)<em>, </em>resulting in a value of -$0.50, meaning Franchise 1’s ASP is $0.50 below the global average.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*qPN-yKopdwg8EuYx9Q2LNg.png" /></figure><h4>Adding Filters</h4><p>Ok, great. This is super helpful and shows that we can show different levels of granularity in the same dataset and visualization (the franchise grain and the global grain). But let’s take it a step further and add additional filters to our visualization and observe how the calculations adapt to the filter selections. We’ll add a filter to the visualization for the “Product” field.</p><p>What happens when we apply a filter for the “Biscotti” product? Let’s use the mental model above to think through this. Filters follow the two-step process of: 1) filter the underlying data and 2) aggregate the filtered data.</p><p>Concretely, let’s see how that works when we apply the filter for “Biscotti”.</p><ol><li>After filtering the dataset for product = ‘Biscotti’, we get this:</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*TIrbLj7h-8w-lsI6eHlTrQ.png" /></figure><p>2. Now, with this filtered dataset, we’ll re-apply the aggregations:</p><p><strong>Average Sales Price:</strong></p><ul><li>Franchise 1: $3</li><li>Franchise 2: $5</li></ul><p><strong>Diff. Average Sales Price</strong> (using calculation MEASURE(asp) — ANY_VALUE(asp_all_franchises_fixed))</p><ul><li>Franchise 1: $3 — $4.50 = $-1.50</li><li>Franchise 2: $5 — $4.50 = $0.50</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*0ZLjEf5e1jgwTD-98hC0ag.png" /></figure><p>Intuitively, this means that Franchise 1 has an ASP for the Biscotti product that’s $1.50 less than the global ASP for Biscotti, while Franchise 2 is $0.50 more than the global ASP.</p><p>Notice that in this calculation, the global ASP stays constant across all the various circumstances: It’s $4.50 when we’re looking at all transactions unfiltered, it’s $4.50 when we’re looking at just Biscotti, it’s $4.50 when we’re looking at both Franchise 1 and Franchise 2. Conceptually, this is why it’s a <em>fixed</em> level-of-detail calculation. It’s <em>fixed</em> on the dimensions that we’ve predefined in the expression, which in this case, is no dimension because we defined it as a global average (with the syntax: AVG(totalPrice) OVER ()). However, we could just as easily fix the calculation on any other dimension that is relevant for our analysis (more on this in the next section).</p><h4>Takeaway</h4><p>And that’s it! We’ve built out the Fixed LOD calculation to calculate the difference between the franchise-level ASP and the global-level ASP. We observed that the expression was <strong><em>calculated independently of the visualization groupings and filters in the dashboard</em></strong>: no matter what fields or filters we added, the global ASP across all franchises was always $4.50. In many use cases, this may be exactly the behavior you want to see.</p><p>However, in other cases, we may need the LOD expression to adapt to the visualization groupings or filters. As an example, let’s say we’re interested in answering the question:</p><p>“What is the Average Sales Price <em>of the Biscotti product</em> across all franchises?” With the fixed calculation, this doesn’t quite work. Since we’ve <em>fixed</em> the expression at the global level, we can only calculate that expression at the global granularity. To <strong><em>fix</em></strong> this problem (pun intended), we can turn to the other type of LOD expressions: Coarse.</p><h3>Coarse Level-of-Detail Expressions</h3><h4>Why do we need another type of expression?</h4><p>Considering the scenario above, where we’re trying to answer the question “What is the Average Sales Price <em>of the Biscotti product</em> across all franchises?”, we could certainly add a new fixed LOD expression that calculates the ASP for each product (e.g. with an expression like AVG(totalPrice) OVER (PARTITION BY product)).</p><p>But then, let’s say we want to ask an even more granular question like “What is the Average Sales Price of the Biscotti product across all franchises <em>on Tuesdays</em>?” Now, we’re adding another dimension to the equation (the date field). Again, we can certainly handle this by creating another expression, which calculates the ASP by product and by day of the week.</p><p>But let’s hold on just a darn minute.</p><p>We already have 3 expressions to handle 3 very similar (but slightly different) questions. As this dashboard gets into the hands of end users, I anticipate that there will be a lot of different variations on these questions and interesting ways to slice and dice this data. For each of those requests, I don’t want to need to create a new expression <em>every time. </em>What if there was a way to dynamically re-generate the LOD expressions to adapt to the visualization groupings and filters in the dashboard? That’s exactly the motivation for when to use a coarse level-of-detail expression (I prefer to think of these as “dynamic” LOD expressions).</p><h3>Defining the expressions</h3><p>Let’s go ahead and build one, we’ll follow the same three steps as above to calculate the different components of this Pivot Table:</p><ol><li>Average Sales Price: This will be the same as in the <em>Fixed</em> section</li><li>Average Sales Price for all franchises: Create a custom calculation called <em>asp_all_franchises</em> with the following syntax:</li></ol><pre>AVG(totalPrice) AGGREGATE OVER (ORDER BY franchiseId ALL)</pre><p>Whoa — we’ve got some new syntax! <a href="https://docs.databricks.com/aws/en/dashboards/custom-calculations/#aggregate-window-functions">AGGREGATE OVER</a> is the keyword to indicate that this is a <em>coarser LOD </em>expression (for more details on syntax, refer to the linked docs). This generates a <em>measure</em>, so it will be aggregated at query-time based on the context of the visualization.</p><p>Intuitively, what this means is that we want to calculate the Average Sales Price for all dimensions in the visualization grouping or filter, EXCEPT for franchiseId, where we want to ignore that filter and consider ALL values of franchiseId.</p><p>3. Diff. of Average Sales Prices: Putting it all together, we’ll create a simple custom calculation called <em>asp_diff</em> with the syntax:</p><pre>MEASURE(asp) - MEASURE(asp_all_franchises)</pre><p>Let’s build a quick Pivot Table with these 3 fields and see how it observes compared to the fixed LOD expressions.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*MIhJJXKSMN0dnaIA1DjY0g.png" /></figure><p>Without any additional visualization groupings or filters, this works exactly the same way as it did for the Fixed LOD expression. Let’s add that filter for product and walk through how the difference of ASP measure will be calculated now with a filter on product = ‘Biscotti’. Again, measures are conceptually evaluated using a two step process: 1) Filter the dataset and 2) Aggregate the measures.</p><ol><li>When the dataset is filtered to Biscotti, we get the same dataset as before:</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/890/1*M1NlMOonmAFWFgcvvCqTPQ.png" /></figure><p>2. Re-apply the aggregations on the filtered dataset:</p><p><strong>Average Sales Price </strong>(same as before)</p><ul><li>Franchise 1: $3</li><li>Franchise 2: $5</li></ul><p><strong>Average Sales Price All Franchises </strong>(Coarse LOD): With the filtered dataset, we’ll get the Average of the totalPrice field across all franchises (in other words, we’ll ignore any filters on the franchiseId field, but will respect all other filters). In this case:</p><ul><li>Franchise 1: $4</li><li>Franchise 2: $4</li></ul><p><strong>Diff. Average Sales Price:</strong></p><ul><li>Franchise 1: $3 — $4 = $-1</li><li>Franchise 2: $5— $4 = $1</li></ul><p>This means that the ASP for the Biscotti product across all franchises is $4, so Franchise 1 sells it for $1 less than the average and Franchise 2 sells it for $1 more than the average.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*SiKg5Vwg-wMXotwKxomLJg.png" /></figure><h3>Takeaway</h3><p>We’re getting a different result than with the fixed LOD expression because here, we’re recalculating the expression based on the context of the visual, which in this case includes the product filter. We could add multiple other dimensions (and combinations of dimensions) to this visual, and the expression will always recompute within the context of those dimensions. This is pretty powerful!</p><p>The point is not that one method (Fixed vs. Coarse) is better than the other: they are both valid approaches, it really just depends on the question we’re trying to answer.</p><p>Comparing against a global, fixed ASP makes sense if we’re doing a high-level analysis/benchmarking exercise to understand the general trend of the business. Comparing against a dynamic <em>(or coarser)</em> average, which adjusts to product dimension selections is helpful if we’re focused on understanding trends within a specific product line, where the global average isn’t useful, but understanding the ASP for that specific product across franchises is. Different types of analysis, but both are completely valid and useful in their own right. Use the right type of expression for the job!</p><h3>Summary</h3><p>We’ve walked through the motivation and the mechanics for how the <em>fixed</em> and <em>coarse</em> LOD expressions work in AI/BI. Next time you’re building a dashboard and need to represent various levels of granularity in the same visualization or dataset, keep these two methods in mind.</p><p>Use Fixed when the dimensions are pre-determined and do not need to be re-calculated in the context of the visualization. Use Coarse when the context of the visualization matters a lot, so the expressions should be recomputed to reflect that context.</p><p><strong>Mental model</strong>: If a user adds a filter or groups by a new dimension, should my comparison point change or stay the same?</p><p>If you have any questions, reach out to your Databricks Account team or email me at <a href="/service/mailto:justin.kolpak@databricks.com">justin.kolpak@databricks.com</a>.</p><p>Ok, this blog made me hungry, I’m going to go eat a Biscotti! Thanks for reading.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=fec6e11c9deb" width="1" height="1" alt=""><hr><p><a href="https://medium.com/dbsql-sme-engineering/level-of-detail-expressions-in-ai-bi-a-practical-guide-fec6e11c9deb">Level of Detail Expressions in AI/BI: A Practical Guide</a> was originally published in <a href="https://medium.com/dbsql-sme-engineering">DBSQL SME Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Set Access Request Approvers in Databricks from  Excel via API]]></title>
            <link>https://medium.com/dbsql-sme-engineering/set-access-request-approvers-in-databricks-from-excel-via-api-83008cdb6ea9?source=rss----a346361fdb66---4</link>
            <guid isPermaLink="false">https://medium.com/p/83008cdb6ea9</guid>
            <category><![CDATA[data-governance]]></category>
            <category><![CDATA[data-engineering]]></category>
            <category><![CDATA[azure-databricks]]></category>
            <category><![CDATA[databricks]]></category>
            <category><![CDATA[api]]></category>
            <dc:creator><![CDATA[Maksim Pachkouski Databricks MVP]]></dc:creator>
            <pubDate>Tue, 20 Jan 2026 16:41:38 GMT</pubDate>
            <atom:updated>2026-08-15T13:04:50.350Z</atom:updated>
            <content:encoded><![CDATA[<h3>Set Access Request Approvers in Databricks from Excel via API</h3><p>Manually setting up access permissions for hundreds of tables is a tedious task. Databricks recently introduced the <a href="https://www.databricks.com/blog/announcing-public-preview-request-access-unity-catalog"><strong>Manage Access Request Destinations</strong> (Public Preview)</a> feature, which significantly simplifies the approval process. <a href="https://medium.com/dbsql-sme-engineering/databricks-access-requests-export-configured-approvers-c57f16772c60">In part one, I covered how to audit and export current approvers.</a> Now, let&#39;s move on: in this article, I&#39;ll show you how to close the automation loop—take a ready-made list from Excel and mass-assign approvers to schemas and tables in Unity Catalog via the API.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*6YZwmcuDHn8VBE7u7LyoAA.jpeg" /></figure><p>Typically, the Data Governance department maps tables and responsible parties using external registries (Collibra, Excel, etc.). To avoid routine manual configuration, we need a synchronization script.</p><p>As an example, we&#39;ll implement uploading from Excel. This is a general-purpose scenario, and suitable for most teams. This is what the automated workflow looks like:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*HbvvxjLMN55Piqamd7yvqQ.jpeg" /></figure><h3>Importing approvers from Excel</h3><p>We&#39;re implementing a scenario in which a script reads mappings of schemas and approvers from an Excel file and applies them to Databricks.</p><p>We&#39;ll use email for notifications (the API allows up to 5 emails per object). Although the Manage access request destinations feature allows you to send requests to Slack or Teams, for simplicity of demonstration, we&#39;ll use email.</p><p>Let&#39;s assume we have an Excel file where a list of schemes and their owner approvers is listed on one tab:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/839/1*B3FLj4DHqaMHOE-GbgBJwQ.png" /></figure><p>Another tab contains a list of tables and also the owner approvers, since the entire &quot;<strong>qa.new_schema&quot;</strong> schema may be owned by<strong> &quot;MissisClaus@gmail.com&quot;</strong>, while a table within the same schema may be owned by<strong> &quot;elf1@gmail.com&quot;:</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/840/1*QqM_1GE4tcbVNyJoJpwqew.png" /></figure><p>A <a href="https://docs.databricks.com/gcp/en/ingestion/sharepoint">connector from Databricks to SharePoint was recently released</a>, and I&#39;d suggest using this method to load an Excel file into Databricks. To simplify the example, I&#39;ll simply upload it to Volume.</p><p>As I wrote in the previous article, the API SDK needs to be updated:</p><pre>%pip install -U databricks-sdk<br>dbutils.library.restartPython()</pre><p>As always, first we make the necessary imports:</p><pre>import pytz, requests, pyarrow as pa, json<br>from databricks.sdk import WorkspaceClient<br>from datetime import datetime, timezone<br>from pprint import pprint<br>from pyspark.sql import Row<br>from collections import defaultdict<br>from pyspark.sql.functions import *<br>from pyspark.sql.types import *<br>from databricks.sdk.service.catalog import (AccessRequestDestinations, Securable, NotificationDestination, SecurableType, DestinationType)<br><br>wc = WorkspaceClient()</pre><p>Below, I will provide a code example if you want to install the approver for a specific table and test how it works (<em>YOU CAN SKIP IT</em>):</p><pre>table_full_name = &quot;qa.new_schema.new4&quot; ### your table<br><br>payload = AccessRequestDestinations(<br>    securable=Securable(type=SecurableType.TABLE, full_name=table_full_name),<br>    destinations=[<br>        NotificationDestination(<br>            destination_type=DestinationType.EMAIL,<br>            destination_id=&quot;data-approvers@company.com&quot;, ### your email<br>        )<br>    ],<br>)<br><br>wc.rfa.update_access_request_destinations(payload, update_mask=&quot;destinations&quot;)</pre><p>To load the data, I&#39;ll use Databricks&#39; built-in capabilities for reading Excel directly into a Spark DataFrame. In our script, we&#39;ll separate the logic: we&#39;ll calculate the chart sheet separately from the table sheet.</p><p><em>The choice of tool here is not critical—if you prefer, this step can be implemented using Pandas or any other library.</em></p><pre>df_excel_schemes = spark.read.option(&quot;headerRows&quot;, 1).option(&quot;dataAddress&quot;, &quot;schemes&quot;).excel(&#39;/Volumes/dev/edl_test/volume_test/data_approvers_tst.xlsx&#39;)<br>df_excel_schemes.createOrReplaceTempView(&quot;EXCEL_SCHEMES&quot;)</pre><pre>df_excel_tables = spark.read.option(&quot;headerRows&quot;, 1).option(&quot;dataAddress&quot;, &quot;tables&quot;).excel(&#39;/Volumes/dev/edl_test/volume_test/data_approvers_tst.xlsx&#39;)<br>df_excel_tables.createOrReplaceTempView(&quot;EXCEL_TABLES&quot;)</pre><p>The Unity Catalog uses a three-level namespace: <strong>catalog.schema.table</strong>. This allows us to automatically determine the object type without explicitly querying Excel. Let’s write a small utility function that classifies an object (Catalog, Schema, or Table) simply by counting the number of dots in its full name:</p><pre>def infer_securable_type_by_dots(full_name: str) -&gt; SecurableType:<br>    dots = full_name.count(&quot;.&quot;)<br><br>    if dots == 2:<br>        return SecurableType.TABLE<br>    if dots == 1:<br>        return SecurableType.SCHEMA<br>    if dots == 0:<br>        return SecurableType.CATALOG<br><br>    raise ValueError(f&quot;Error &#39;{full_name}&#39; (dots: {dots})&quot;)</pre><p>In a real production environment, no one will likely install an approver for the entire catalog, so we’ll focus on tables and schemas.</p><p>We’ll create a function that takes a row from a DataFrame, clears the list of email addresses of empty values, and sends a request via the Databricks SDK. To ensure the process doesn’t stop due to a single invalid entry, we’ll add exception handling and save all errors in a separate list for the final report.</p><pre>error_rows = []<br>success_counts = {<br>    &quot;schema&quot;: 0,<br>    &quot;table&quot;: 0<br>}<br><br>def set_approvers_from_row(row):<br>    securable_type = infer_securable_type_by_dots(row.TABLE)<br>    print(f&quot;Processing: {row.TABLE}, inferred type: {securable_type}&quot;)<br><br>    if securable_type == SecurableType.SCHEMA:<br>        level = &quot;schema&quot;<br>    elif securable_type == SecurableType.TABLE:<br>        level = &quot;table&quot;<br>    else:<br>        level = &quot;catalog&quot;<br><br>    emails = [<br>        row.EMAIL_1,<br>        row.EMAIL_2,<br>        row.EMAIL_3,<br>        row.EMAIL_4,<br>        row.EMAIL_5,<br>    ]<br>    emails = [e.strip() for e in emails if e and e.strip()]<br><br>    if not emails:<br>        error_rows.append(Row(FULL_NAME=row.TABLE, LEVEL=level, ERROR=&quot;No EMAILs provided&quot;))<br>        return<br><br>    payload = AccessRequestDestinations(<br>        securable=Securable(type=securable_type, full_name=row.TABLE),<br>        destinations=[NotificationDestination(destination_type=DestinationType.EMAIL, destination_id=e) for e in emails],<br>    )<br><br>    try:<br>        wc.rfa.update_access_request_destinations(payload, update_mask=&quot;destinations&quot;)<br>        success_counts[level] += 1<br><br>    except Exception as e:<br>        error_rows.append(Row(FULL_NAME=row.TABLE, LEVEL=level, ERROR=str(e)))</pre><p>Now let’s create the final lists for the schemas and tables. At this stage, we do two things:</p><ul><li>We trim the fields of spaces (using the trim function), as Excel files often contain unnecessary indentation.</li><li>We collect the data in local memory (.collect) to pass it to the Databricks API client.</li></ul><pre>schemas = df_excel_schemes.select(<br>    trim(&quot;SCHEMA&quot;).alias(&quot;SCHEMA&quot;),<br>    trim(&quot;EMAIL_1&quot;).alias(&quot;EMAIL_1&quot;),<br>    trim(&quot;EMAIL_2&quot;).alias(&quot;EMAIL_2&quot;),<br>    trim(&quot;EMAIL_3&quot;).alias(&quot;EMAIL_3&quot;),<br>    trim(&quot;EMAIL_4&quot;).alias(&quot;EMAIL_4&quot;),<br>    trim(&quot;EMAIL_5&quot;).alias(&quot;EMAIL_5&quot;),<br>).collect()<br><br>tables = df_excel_tables.select(<br>    trim(&quot;TABLE&quot;).alias(&quot;TABLE&quot;),<br>    trim(&quot;EMAIL_1&quot;).alias(&quot;EMAIL_1&quot;),<br>    trim(&quot;EMAIL_2&quot;).alias(&quot;EMAIL_2&quot;),<br>    trim(&quot;EMAIL_3&quot;).alias(&quot;EMAIL_3&quot;),<br>    trim(&quot;EMAIL_4&quot;).alias(&quot;EMAIL_4&quot;),<br>    trim(&quot;EMAIL_5&quot;).alias(&quot;EMAIL_5&quot;),<br>).collect()</pre><p>For each row in our lists, the previously written function is called. This is when the actual API calls and access settings are updated.</p><pre>for row in schemas:<br>    set_approvers_from_row(row)<br><br>for row in tables:<br>    set_approvers_from_row(row)</pre><p>Finally, we display the work statistics. The script will show the number of successfully updated schemas and tables. If any errors occurred, we&#39;ll collect them in a separate DataFrame and display them in a table for easy analysis of incidents.</p><pre>print(&quot;===== SUMMARY =====&quot;)<br>print(f&quot;Schemas successfully updated : {success_counts[&#39;schema&#39;]}&quot;)<br>print(f&quot;Tables successfully updated  : {success_counts[&#39;table&#39;]}&quot;)<br>print(f&quot;Total failures               : {df_errors.count()}\n&quot;)<br>print(&quot;===== ERROR DETAILS:&quot;)<br><br>df_errors = spark.createDataFrame(error_rows)<br>df_errors.display()</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/894/1*TtmqBPw8-6rPzS0sUKyQ4g.png" /></figure><p>I&#39;ve demonstrated the basic automation logic. Of course, in production, you&#39;ll need to replace <strong>display()</strong> and add audit logs to the database table or send alerts to Slack when an update fails.</p><ul><li><a href="https://github.com/protmaks/Databricks/blob/main/API%20%26%20SDK/Databricks%20Access%C2%A0Requests/Set%20Access%20Request%20Approvers%20in%20Databricks%20from%20%20Excel%20via%C2%A0API.py"><strong><em>Link to the Notebook with source code</em></strong></a></li></ul><p><em>Author:</em> <strong>Maksim Pachkouski</strong> [Databricks MVP &amp; Databricks Solutions Architect Champion]. Follow my posts on <a href="https://medium.com/@protmaks">Medium</a> and <a href="https://www.linkedin.com/in/protmaks/">LinkedIn</a> to stay up-to-date on the latest news in Data Engineering and learn about best practices.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=83008cdb6ea9" width="1" height="1" alt=""><hr><p><a href="https://medium.com/dbsql-sme-engineering/set-access-request-approvers-in-databricks-from-excel-via-api-83008cdb6ea9">Set Access Request Approvers in Databricks from  Excel via API</a> was originally published in <a href="https://medium.com/dbsql-sme-engineering">DBSQL SME Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Databricks Access Requests: Export Configured Approvers (Destinations) with the API in Unity…]]></title>
            <link>https://medium.com/dbsql-sme-engineering/databricks-access-requests-export-configured-approvers-c57f16772c60?source=rss----a346361fdb66---4</link>
            <guid isPermaLink="false">https://medium.com/p/c57f16772c60</guid>
            <category><![CDATA[databricks]]></category>
            <category><![CDATA[databricks-sql]]></category>
            <category><![CDATA[azure-databricks]]></category>
            <category><![CDATA[unity-catalog]]></category>
            <category><![CDATA[data-engineering]]></category>
            <dc:creator><![CDATA[Maksim Pachkouski Databricks MVP]]></dc:creator>
            <pubDate>Mon, 12 Jan 2026 21:25:35 GMT</pubDate>
            <atom:updated>2026-08-15T13:05:08.888Z</atom:updated>
            <content:encoded><![CDATA[<h3>Databricks Access Requests: Export Configured Approvers (Destinations) with the API in Unity Catalog</h3><p>Databricks recently added the <a href="https://docs.databricks.com/aws/en/data-governance/unity-catalog/manage-privileges/access-request-destinations"><strong>Manage access request destinations feature (Public Preview)</strong></a>, but the documentation and YouTube video talk about managing it through the interface. This is a visual demonstration for purposes, but for real production and automation, we need a different solution — and that’s what I’ll tell you about.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*4R2llSjbh2OowM9l8XRoSw.jpeg" /></figure><p>To understand the value of the new approach, consider the legacy process used in most companies. If a user needs access to a table, they create a request in Jira, list the tables they need to access, specify the group they want to access, and attach an approval from the owner. The administrator then writes a script and grants permissions.</p><p>Below is an example of what the processes look like before and after, as well as how much time each action typically requires. As we can see, the new process consumes half as much time and resources:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*kYln_PUhOOoYgWKsthwmxg.jpeg" /></figure><h3>Manage access request destinations</h3><p>Take a quick look at how this new feature works in the Databricks interface. The administrator assigns an owner to a catalog, schema, or table:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*cbphPbMF-D_WbPp8TLs8kA.jpeg" /></figure><p>In the interface, you can specify up to 5 emails (resource owners), and this can also be not only email, but also Slack, Teams, and other applications:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/657/1*87Uu9s4dW-_iEuUsBF-pSQ.png" /></figure><p>If an owner is configured for a directory, schema, or table, the settings are inherited down the hierarchy to the table. Now the user can request access. It’s best to request access for a group, for your entire team. Below is an example:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*LdKv6jh8CzkkxOCj3pM6jA.jpeg" /></figure><p>A notification is sent via email, and the owner grants access to read or modify the table in a couple of clicks:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/483/1*rLbTfFv0N5KQ_NJgvjNUhQ.png" /></figure><p>I’ll explain how to create a script to extract a list of tables and view their assigned owners. In the next part, I’ll show you how to download a list of table owners and tables from Excel and install them in Databricks via the API. <a href="https://medium.com/@protmaks">Subscribe to my blog on Medium to stay up to date</a>.</p><h3>How to extract current approvers via API?</h3><p>To manage requests through code, we’ll need the rfa (Request for Access) API. Your current version of the Databricks SDK likely doesn’t have it yet, as the feature is quite new. So, first, update the package:</p><pre>%pip install -U databricks-sdk<br>dbutils.library.restartPython()</pre><p>Next, we make the necessary imports:</p><pre>import pytz, requests, pyarrow as pa, json<br>from datetime import datetime, timezone<br>from pprint import pprint<br>from collections import defaultdict<br>from pyspark.sql import Row<br>from pyspark.sql.functions import *<br>from pyspark.sql.types import *<br>from databricks.sdk import WorkspaceClient<br>from databricks.sdk.service.catalog import (AccessRequestDestinations, Securable, NotificationDestination, SecurableType, DestinationType)<br><br>wc = WorkspaceClient()</pre><p>Using the API, we extract a list of directories, schemas, and tables. Inheritance occurs hierarchically, and it’s difficult to determine at what level permissions are set, so we collect the effective_level. We also exclude system tables, as they are of no interest to us.</p><pre>EXCLUDE_CATALOGS = {&quot;samples&quot;, &quot;system&quot;}<br>EXCLUDE_SCHEMAS = {&quot;information_schema&quot;}<br>rows = []<br><br>def get_rfa(securable_type: str, full_name: str) -&gt; dict:<br>    return wc.rfa.get_access_request_destinations(securable_type, full_name).as_dict()<br><br>def norm_destinations(rfa: dict):<br>    ds = (rfa or {}).get(&quot;destinations&quot;) or []<br>    return sorted({(d.get(&quot;destination_type&quot;), d.get(&quot;destination_id&quot;)) for d in ds})<br><br>def pick_level(dt, ds, dc):<br>    if not dc and not ds and not dt:<br>        return None<br>    if dt and dt != ds:<br>        return &quot;table&quot;<br>    if ds and ds != dc:<br>        return &quot;schema&quot;<br>    if dc:<br>        return &quot;catalog&quot;<br>    return None<br><br>for cat in wc.catalogs.list():<br>    if cat.name in EXCLUDE_CATALOGS:<br>        continue<br><br>    rfa_catalog = get_rfa(&quot;catalog&quot;, cat.name)<br>    dc = norm_destinations(rfa_catalog)<br><br>    for sch in wc.schemas.list(catalog_name=cat.name):<br>        if sch.name in EXCLUDE_SCHEMAS:<br>            continue<br><br>        schema_full = sch.full_name<br>        rfa_schema = get_rfa(&quot;schema&quot;, schema_full)<br>        ds = norm_destinations(rfa_schema)<br><br>        for tbl in wc.tables.list(catalog_name=cat.name, schema_name=sch.name):<br>            table_full = tbl.full_name<br>            rfa_table = get_rfa(&quot;table&quot;, table_full)<br>            dt = norm_destinations(rfa_table)<br><br>            effective_level = pick_level(dt, ds, dc)<br><br>            if effective_level == &quot;table&quot;:<br>                effective = rfa_table<br>            elif effective_level == &quot;schema&quot;:<br>                effective = rfa_schema<br>            elif effective_level == &quot;catalog&quot;:<br>                effective = rfa_catalog<br>            else:<br>                effective = None<br><br>            rows.append(Row(<br>                catalog=cat.name,<br>                schema=schema_full,<br>                table=table_full,<br>                effective_level=effective_level,<br>                effective_access_request_destinations_json=(json.dumps(effective, ensure_ascii=False) if effective is not None else None)<br>            ))<br><br>df_rfa = spark.createDataFrame(rows)<br>df_rfa.createOrReplaceTempView(&quot;TABLE_ACCESS_DESTINATIONS&quot;)</pre><p>Using effective_level, we can see at what level the access query settings were set for each table:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*5dDnIUmWAd6N9EI9N9AUYQ.png" /></figure><p>Let’s assume we work for a large enterprise company, so we only allow email, and we don’t check other parameters. Let’s break out the five emails into separate columns.</p><pre>df_dest_emails = spark.sql(&quot;&quot;&quot;<br>    SELECT<br>        catalog,<br>        schema,<br>        table,<br>        effective_level,<br>        get(emails, 0) AS EMAIL_1,<br>        get(emails, 1) AS EMAIL_2,<br>        get(emails, 2) AS EMAIL_3,<br>        get(emails, 3) AS EMAIL_4,<br>        get(emails, 4) AS EMAIL_5<br>    FROM (<br>        SELECT<br>            *,<br>            TRANSFORM(<br>                FILTER(<br>                    FROM_JSON(<br>                        effective_access_request_destinations_json,<br>                        &#39;STRUCT&lt;destinations:ARRAY&lt;STRUCT&lt;destination_id:STRING,destination_type:STRING&gt;&gt;&gt;&#39;<br>                    ).destinations,<br>                    d -&gt; d.destination_type = &#39;EMAIL&#39;<br>                ),<br>                d -&gt; d.destination_id<br>            ) AS emails<br>        FROM TABLE_ACCESS_DESTINATIONS<br>    )<br>&quot;&quot;&quot;)<br><br>df_dest_emails.createOrReplaceTempView(&quot;TABLE_ACCESS_DESTINATIONS_EMAILS&quot;)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*knPlhmX0j4UY85kjWwT0kA.png" /></figure><p>Next, we need to get a list of all tables. For this, we’ll use the Databricks system table.</p><pre>df_all_tables = spark.sql(&quot;&quot;&quot;<br>    SELECT<br>        CONCAT_WS(&#39;.&#39;, table_catalog, table_schema, table_name) AS table,<br>        table_owner,<br>        table_type   <br>    FROM system.information_schema.tables<br>    WHERE<br>        table_catalog NOT IN (&#39;samples&#39;, &#39;system&#39;)<br>        AND table_schema &lt;&gt; &#39;information_schema&#39;<br>    ORDER BY table<br>&quot;&quot;&quot;)<br><br>df_all_tables.createOrReplaceTempView(&quot;ALL_TABLES&quot;)</pre><p>All that remains is to make a join and find tables where the approver is not specified:</p><pre>%sql<br>SELECT<br>  AT.table as TABLE,<br>  TAD.effective_level as LEVEL,<br>  TAD.EMAIL_1,<br>  TAD.EMAIL_2,<br>  TAD.EMAIL_3,<br>  TAD.EMAIL_4,<br>  TAD.EMAIL_5<br>FROM ALL_TABLES as AT<br>LEFT JOIN TABLE_ACCESS_DESTINATIONS_EMAILS as TAD<br>  ON AT.table = TAD.table<br>ORDER BY table</pre><p>From the results, we see that there are rows where the approver columns are empty:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*cIcwhoKzyTv_bLXZebH6GA.png" /></figure><ul><li><a href="https://github.com/protmaks/Databricks/blob/main/API%20%26%20SDK/Databricks%20Access%C2%A0Requests/Databricks%20Access%20Requests%20-%20Export%20Configured%20Approvers.py"><strong><em>Link to the full Notebook code</em></strong></a></li></ul><p><strong>Why do we need a configuration export?</strong><br>Downloading current settings is useful in several scenarios:</p><ol><li><strong>Migration to IaC (Infrastructure as Code)</strong>. If access was configured manually for some time, exporting will help obtain the current system state (a “snapshot”) and translate it into automated scripts.</li><li><strong>Security audit. </strong>For example, to ensure that users are requesting access only to Views, and not to raw tables.</li><li><strong>Updating responsible persons. </strong>If an approver leaves, exporting will allow you to quickly find all tables where that employee is listed as the approver and assign a replacement.</li></ol><p>Often, data on responsible parties is stored not in Databricks, but in Data Governance registries (Collibra, Excel). In the next post, I’ll show you how to automate the transfer of this information: we’ll learn how to download a configuration file from Excel and apply it to tables via the API.</p><p><em>Author:</em> <strong>Maksim Pachkouski</strong> [Databricks MVP &amp; Databricks Solutions Architect Champion]. Follow my posts on <a href="https://medium.com/@protmaks">Medium</a> and <a href="https://www.linkedin.com/in/protmaks/">LinkedIn</a> to stay up-to-date on the latest news in Data Engineering and learn about best practices.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=c57f16772c60" width="1" height="1" alt=""><hr><p><a href="https://medium.com/dbsql-sme-engineering/databricks-access-requests-export-configured-approvers-c57f16772c60">Databricks Access Requests: Export Configured Approvers (Destinations) with the API in Unity…</a> was originally published in <a href="https://medium.com/dbsql-sme-engineering">DBSQL SME Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Governed Schema Evolution on Databricks]]></title>
            <link>https://medium.com/dbsql-sme-engineering/governed-schema-evolution-on-databricks-6d1dbf5e43a3?source=rss----a346361fdb66---4</link>
            <guid isPermaLink="false">https://medium.com/p/6d1dbf5e43a3</guid>
            <category><![CDATA[liquibase]]></category>
            <category><![CDATA[data-governance]]></category>
            <category><![CDATA[schema-management]]></category>
            <category><![CDATA[ci-cd-pipeline]]></category>
            <category><![CDATA[databricks]]></category>
            <dc:creator><![CDATA[Manuel Beuttler]]></dc:creator>
            <pubDate>Fri, 09 Jan 2026 16:01:57 GMT</pubDate>
            <atom:updated>2026-01-09T16:01:56.247Z</atom:updated>
            <content:encoded><![CDATA[<h4>DevOps-powered Version Control for Unity Catalog with Liquibase</h4><p><strong>Author: </strong>Manuel Beuttler, Delivery Solutions Architect @ Databricks</p><h4>Introduction: Schema Drift and Version Control Challenges</h4><p>In modern data platforms like Databricks, managing schema changes can be a significant headache, often leading to schema drift and broken downstream pipelines. This challenge is a core problem faced by many data teams that seek a robust solution for version control and Data Product lifecycle management.</p><p>Without a disciplined approach, changes made manually in the UI or via ad hoc scripts create a “wild west” environment. A simple column rename in a development environment may go undocumented, only to cause a critical pipeline failure when the code is moved to production.</p><p>The solution lies in establishing a DevOps pipeline where every schema modification, from a simple column comment update to a major table alteration, is version-controlled, automated, and subject to a formal review and approval process.</p><p>While our previous articles (<a href="https://medium.com/dbsql-sme-engineering/database-change-management-on-lakehouse-with-databricks-sql-and-liquibase-c3c238781616">Database Change Management</a> and <a href="https://medium.com/dbsql-sme-engineering/advanced-schema-management-on-databricks-with-liquibase-1900e9f7b9c0">Advanced Schema Management</a>) focused on what Liquibase can do and how to execute commands, in this one, we will walk through an example architecture that leverages Liquibase within an Azure DevOps CI/CD pipeline to govern schema changes in a Databricks Unity Catalog (UC) environment and build production-ready data products.</p><h4>About Liquibase</h4><p><a href="https://www.liquibase.com/">Liquibase</a> is an open source database change management tool that helps development teams track, version, and deploy database schema changes in a controlled and automated way. It works by storing all database modifications (called “changesets”) in text-based files that are version-controlled alongside application code. Liquibase automatically translates these platform-independent change definitions (in SQL, XML, YAML, or JSON) into database-specific SQL. It supports various database systems as targets and offers additional features and capabilities, including integration with CI/CD pipelines for automated deployments across different stages, rollback support, drift detection to identify unauthorized database changes, and comprehensive tracking tables that ensure each change is applied only once.</p><h3>Solution Architecture: The key components</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*6xSXRSPTRf7k305TqE1hMA.png" /><figcaption>Architecture sketch</figcaption></figure><p>The goal is to build an architecture that establishes a bridge between code repositories and the data warehouse, treating database schemas with the same rigor as application code. Our solution relies on four key components:</p><ol><li><strong>Source Control Repository (e.g., Git):</strong> Stores the “source of truth” for the database, specifically the Liquibase change log files that define the schema state.</li><li><strong>CI/CD Pipeline (e.g., Azure DevOps):</strong> Our orchestration component and approval gateway. Including the binaries (Liquibase Docker Container) and execution environment that runs the schema migration logic, ensuring consistency across deployments.</li><li><strong>Databricks Unity Catalog (UC):</strong> The central governance layer that serves as the target for all schema changes, managing tables, permissions, and metadata.</li><li><strong>Databricks SQL Warehouse:</strong> Acts as the compute engine and connection endpoint, allowing Liquibase to execute JDBC commands against the Databricks environment.</li></ol><p>Next, let’s look deeper into the individual components and how to tie them together.</p><h3>Repository Structure and Organization</h3><p>Our starting point is a well-organized repository as a foundation for maintainable schema management. The example structure below separates concerns: <strong>binaries</strong> for dependencies, <strong>templates</strong> for reusability, <strong>pipeline</strong> <strong>definitions</strong> for orchestration, and <strong>data products</strong> for actual schema changes.</p><h4>Example Folder Structure</h4><pre>repository/<br>├── bin/<br>│   ├── DatabricksJDBC42.jar<br>│   └── liquibase-databricks-1.4.2.jar<br>│<br>├── templates/<br>│   └── liquibase-template.yml<br>│<br>├── pipelines/<br>│   └── azure-pipelines.yml<br>│<br>└── data_products/<br>    ├── changelogs/<br>    │   ├── fleet/<br>    │   │   ├── 20241215-01-create-fleet-table.sql<br>    │   │   ├── 20241220-01-add-mileage-column.sql<br>    │   │   └── 20241230-01-add-vehicle-type-comment.sql<br>    │   │<br>    │   ├── trips/<br>    │   │   ├── 20241215-01-create-trips-table.sql<br>    │   │   └── 20241222-01-add-driver-id-column.sql<br>    │<br>    └── root_changelog.xml</pre><h4>Folder Breakdown</h4><p><strong>bin/</strong>: Contains the necessary JDBC drivers and Liquibase extensions.</p><ul><li><strong>DatabricksJDBC42.jar</strong>: The official Databricks JDBC driver that enables Liquibase to connect to the SQL Warehouse.</li><li><strong>liquibase-databricks-1.4.2.jar</strong>: The Liquibase extension specifically designed for Databricks, which handles Unity Catalog-specific SQL syntax.</li></ul><p><strong>templates/</strong>: Contains reusable YAML templates that abstract the Liquibase Docker execution logic. This keeps the main pipeline file clean and promotes consistency across multiple pipelines if you manage multiple catalogs or schemas.</p><p><strong>pipelines/</strong>: Contains the Azure DevOps pipeline definition(s). This is where the orchestration logic lives, defining triggers, stages, variables, and approval gates.</p><p><strong>data_products/</strong>: The heart of the repository. Each subfolder represents a logical data product (typically corresponding to a table or a related group of tables). Within each product folder we have:</p><ul><li><strong>root_changelog.xml</strong>: The master changelog file that Liquibase reads. It references all individual SQL files in the <strong><em>changelogs/</em></strong> folder, maintaining the correct execution order.</li><li><strong>changelogs/</strong>: By using this extra level in the folder hierarchy, we only have to reference it once in the root_changelog.xml and every new subfolder (f.e. fleet or trip) will be automatically added. If we don’t want to have this redundant level, we will have to reference each of them individually.</li><li><strong>[fleet|trip|{table}]/</strong>: Contains individual SQL files with the actual schema definitions, so called changelogs (we will cover them in detail later). Each file is named with a timestamp and sequence number for easy ordering (e.g., <strong><em>YYYYMMDD-NN-description.sql</em></strong>).</li></ul><p>This structure easily scales: as new data products are added, simply create a new folder under <strong><em>changelogs/</em></strong> and start committing SQL files.</p><h3>CI/CD Pipeline Definition</h3><p>For better readability and maintainability, we split the pipeline definition into two files, encapsulating the logic for running the Liquibase Docker container and other reusable parts into a <a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/process/templates?view=azure-devops&amp;pivots=templates-includes">YAML template</a>.</p><p>The template mounts the necessary volumes (change logs and drivers), uses the master changelog as the entry point and constructs the JDBC connection string dynamically:</p><pre>parameters:<br> - name: action<br>   type: string<br> - name: displayName<br>   type: string<br><br>steps:<br> - script: |<br>     # Use latest docker liquibase image<br>     docker run \<br>       -v $(Build.SourcesDirectory)/data_products:/liquibase/changelog \<br>       -v $(Build.SourcesDirectory)/bin:/liquibase/lib \<br>       liquibase \<br>       --classpath=&quot;/liquibase/changelog:/liquibase/lib/DatabricksJDBC42.jar:/liquibase/lib/liquibase-databricks-1.4.2.jar&quot; \<br>       --url=&quot;$(DATABRICKS_SQL_ENDPOINT)ConnCatalog=$(CATALOG);ConnSchema=$(SCHEMA);UID=token;PWD=$(PAT);&quot; \<br>       --changeLogFile=root_changelog.xml \<br>       ${{ parameters.action }}<br>   displayName: ${{ parameters.displayName }}</pre><p>After defining the template, we can reference and use it in our main pipeline definition: the <strong><em>azure-pipelines.yml</em></strong> file, which orchestrates the entire workflow. It is triggered on commits to the main branch and executes a two-stage process: validation followed by deployment with an approval gate.</p><pre>trigger:<br>  - main<br><br>stages:<br>  - stage: validation<br>    displayName: &#39;Validate Schema Changes&#39;<br>    dependsOn: []<br>    jobs:<br>      - job: reviewChanges<br>        displayName: &#39;Review Changes&#39;<br>        pool:<br>          name: ${AGENT_POOL}<br>        <br>        variables:<br>          - name: CATALOG<br>            value: ${TARGET_CATALOG}<br>          - name: SCHEMA<br>            value: ${TARGET_SCHEMA}<br>          - name: SQL_ENDPOINT<br>            value: ${DATABRICKS_SQL_ENDPOINT_DEV}<br>          - name: DATABRICKS_CLIENT_ID<br>            value: $(DATABRICKS_CLIENT_ID_DEV)<br>          - name: DATABRICKS_CLIENT_SECRET<br>            value: $(DATABRICKS_CLIENT_SECRET_DEV)<br><br>        steps:<br>          - checkout: self<br><br>          - template: templates/template_liquibase.yml<br>            parameters:<br>              action: validate<br>              displayName: &#39;Validating changelog files&#39;<br>          <br>          - template: templates/template_liquibase.yml<br>            parameters:<br>              action: status<br>              displayName: &#39;Show status on current and pending changelogs&#39;<br><br>          - template: templates/template_liquibase.yml<br>            parameters:<br>              action: updateSQL<br>              displayName: &#39;Show SQL before it will be executed&#39;<br>    <br>  - stage: deployment<br>    displayName: &#39;Deploy Schema Changes&#39;<br>    dependsOn: [validation]<br>    jobs:<br>      - deployment: deployProd<br>        displayName: &#39;Deploy to Production&#39;<br>        pool:<br>          name: ${AGENT_POOL}<br>        <br>        variables:<br>          - name: CATALOG<br>            value: ${TARGET_CATALOG}<br>          - name: SCHEMA<br>            value: ${TARGET_SCHEMA}<br>          - name: SQL_ENDPOINT<br>            value: ${DATABRICKS_SQL_ENDPOINT_DEV}<br>          - name: DATABRICKS_CLIENT_ID<br>            value: $(DATABRICKS_CLIENT_ID_DEV)<br>          - name: DATABRICKS_CLIENT_SECRET<br>            value: $(DATABRICKS_CLIENT_SECRET_DEV)<br><br>        environment: demo-prod-env<br><br>        strategy:<br>          runOnce:<br>            deploy:<br>              steps:<br>                - checkout: self<br>                <br>                - template: templates/template_liquibase.yml<br>                  parameters:<br>                    action: update<br>                    displayName: &#39;Executing changelog files&#39;</pre><p><strong>Trigger</strong>: For our example, the pipeline automatically runs whenever changes are committed to the main branch, ensuring that every schema modification undergoes validation before deployment. For real-world production scenarios, we will likely want to implement more advanced trigger logic, depending on the branch used for staging and releases.</p><p><strong>Variables</strong>: Each stage defines environment-specific variables for the target catalog, schema, and Databricks connection details. Using template variables (e.g., <strong><em>${TARGET_CATALOG}</em></strong>) allows the same pipeline to be reused for different environments by simply overriding values at pipeline execution time.</p><h4>Stage 1: The Validation Stage</h4><p>The first stage of the pipeline is read-only and designed purely for validation/review. It executes several Liquibase commands to ensure the proposed changes are safe and correct, without modifying the target database.</p><ol><li><strong>validate</strong>: Scans the change logs for syntax errors, missing files, or malformed XML/SQL structures.</li><li><strong>status</strong>: Reports which changesets have not yet been applied to the target database.</li><li><strong>updateSQL</strong>: This is the most critical step for reviewers. It generates and prints the exact SQL statements that Liquibase <em>intends</em> to execute.</li></ol><p>By inspecting the output of updateSQL, the data owners can verify exactly what will happen (e.g., “Ah, this script drops a column I didn’t expect”). This is vital for preventing accidental data loss.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*_rJKxTvSNTCD8BZrkljIlA.png" /><figcaption>Screenshot of job run output</figcaption></figure><p>The screenshot above shows an example of the updateSQL stage’s output. It is intended for users who are familiar with SQL. It not only shows which statements are executed by the changelog file, but also lists database locks and some additional information about the changeset.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*aLv0amneH0wUlzIqGaI_Yg.png" /><figcaption>Screenshot of deployment stage waiting for approval</figcaption></figure><p>If the validation state succeeds without errors, the deployment state will be triggered next. However, to enforce a manual check before deployment, the pipeline pauses. This is where the <strong>Approval Gate</strong> comes into play.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*x6Jhewr7e0_9jb5vKd6FTw.png" /><figcaption>Screenshot of an example environment setup</figcaption></figure><h4>Stage 2: The Deployment Stage</h4><p>In Azure DevOps, this is implemented using <a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/process/approvals?view=azure-devops&amp;tabs=check-pass">Environments</a>. A specific environment (e.g., <strong><em>demo-prod-env</em></strong>) is configured to require approval from a designated Data Steward or Data Owner. The pipeline will not proceed to the Deployment stage until this person reviews the Validation output and explicitly clicks “Approve.”</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*0vKmKIn7srTNI3JzxjH3Jw.png" /><figcaption>Screenshot of the approval dialog</figcaption></figure><p><strong>Deployment Stage</strong>:</p><ul><li><strong>dependsOn: [validation]:</strong> Only runs after Validation succeeds.</li><li><strong>environment: demo-prod-env:</strong> Configures the Approval Gate</li><li><strong>update:</strong> Executes the pending changesets, applying the schema changes to the target catalog and schema.</li></ul><p>Once approved, the <strong>Deployment Stage</strong> triggers. It runs the update action, which:</p><ol><li>Connects to the Databricks SQL Warehouse.</li><li>Acquires a lock on the database to prevent concurrent updates.</li><li>Executes the pending changesets.</li><li>Updates the tracking table to indicate that these changes are complete.</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Ch_mW2wZ7tMdNvStfTHb9Q.png" /><figcaption>Screenshot of the job output after deployment</figcaption></figure><p>With this separation of concerns — validation without side effects, followed by human review and deployment — we ensure that only reviewed and approved changes reach production.</p><h3>Change Logs: The core of our schema definitions</h3><p>In this example, we define schema changes as a series of “changesets.” While Liquibase supports XML, JSON, and YAML, using <strong>Formatted SQL</strong> is the suggested best practice for data teams. We assume that data engineers are familiar with standard SQL, which lowers the entry barrier while still benefiting from Liquibase’s tracking capabilities. Liquibase, in general, supports various systems (not only Databricks), which allows us to use the same changesets for deployments on PostgreSQL (the core of <a href="https://www.databricks.com/product/lakebase">Databricks Lakebase</a>) or other databases.</p><p>Our recommended best practice is to use <strong>one changeset file for each version or logical change</strong>. This modular approach makes code reviews easier and simplifies the tracking of history.</p><p>The examples below illustrate this approach, showing two types of changelog files:</p><ol><li><strong>Master Changelog (XML):</strong> This file references all individual changeset files located under a specified path. We defined a filter to only consider for SQL files.</li><li><strong>Individual SQL File:</strong> This is the actual SQL code being tracked and executed by the Liquibase command.</li></ol><pre>&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;<br>&lt;databaseChangeLog<br>   xmlns=&quot;http://www.liquibase.org/xml/ns/dbchangelog&quot;<br>   xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;<br>   xsi:schemaLocation=&quot;http://www.liquibase.org/xml/ns/dbchangelog<br>       http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd&quot;&gt;<br><br><br>   &lt;includeAll path=&quot;changelogs&quot; relativeToChangelogFile=&quot;true&quot; endsWithFilter=&quot;.sql&quot;/&gt;<br>&lt;/databaseChangeLog&gt;</pre><pre>- liquibase formatted sql<br> - changeset Max.Mustermann:20251230–001<br> - comment: Updating comments for vehicle_type to include examples.<br>ALTER TABLE fleet ALTER COLUMN vehicle_type COMMENT &#39;The type of the vehicle for example TRUCK or CAR&#39;;</pre><p>Treating schema changes as modular, version-controlled artifacts brings the same discipline to data infrastructure that software engineering has long relied on. With formatted SQL and Liquibase, we can confidently evolve databases while preserving consistency and accountability across deployments.</p><h3>Advanced Usage: Ideas for using each component to its full potential</h3><p>Implementing this architecture requires us to consider certain nuances regarding how Liquibase interacts with Databricks Unity Catalog.</p><h4>Tracking State with Governance</h4><p>Liquibase maintains its own internal state using a <strong><em>DATABASECHANGELOG</em></strong> table created within the target schema. This table acts as the ledger of history, tracking:</p><ul><li><strong>ID</strong>: The unique identifier of the changeset.</li><li><strong>Author</strong>: Who created the change.</li><li><strong>Filename</strong>: Where the change was defined.</li><li><strong>Checksum</strong>: A hash of the file content to detect if a deployed script has been retroactively modified (which throws an error to prevent inconsistencies).</li></ul><p>Because this table resides within Unity Catalog, it is governed in the same manner as any other table in Unity Catalog. We can, and should, restrict access so that only the service principal running the CI/CD pipeline has write access to it, ensuring the audit trail remains tamper-proof.</p><h4>Handling Existing Tables (Brownfield Projects)</h4><p>In a real-world example, we would most likely not start from a blank slate; instead, we would have existing tables filled with valuable data. Luckily, bringing these under version control is straightforward.</p><p>When initializing the Liquibase project for an existing schema, we create a “baseline” changeset. Crucially, if the table already exists in the target environment, we can <strong>omit the <em>CREATE TABLE</em> statement</strong> or use Liquibase “preconditions” to check for existence. This allows Liquibase to “sync up” its tracking table without failing because the object already exists.</p><p>More information on <strong>diff-changelog</strong> can be found in the Liquibase <a href="https://docs.liquibase.com/reference-guide/database-inspection-change-tracking-and-utility-commands/diff-changelog">documentation</a>.</p><h4>Managing Rollbacks</h4><p>All quality gates, code reviews, or other safety measures will fail sometimes. So, what can we do if our changesets include errors and we need to revert them? Fortunately, Liquibase also offers advanced options to support you:</p><ul><li>Use the built-in rollback behavior for default operations, such as <strong><em>CREATE TABLE</em></strong>.</li><li>Define custom undo logic directly in the <strong>change-log</strong> to make rollbacks more testable and predictable</li><li><strong>Tag important versions</strong> of our deployments to later roll back to a specific point in time</li></ul><p>Before deploying such logic into production, it is essential to verify and test our rollback strategy. So we know that we are prepared when the time comes.</p><p>For more details and additional implementation examples, refer to the Liquibase <a href="https://docs.liquibase.com/secure/implementation-guide-5-0/implement-a-rollback-strategy-with-sql-changelogs-for-existing-databases">documentation</a>.</p><h4>The Risk of Automated Change Generation</h4><p>Liquibase offers a powerful feature called diff, which can compare two databases (e.g., Dev vs. Prod) and automatically generate a changelog to synchronize them. While tempting, this feature introduces significant risk, particularly regarding <strong>Column Renames</strong>.</p><p><strong>The Issue:<br></strong>If a developer renames a column from <strong><em>customer_id</em></strong> to <strong><em>client_id</em></strong> in the development environment, Liquibase’s diff tool does not recognize this as a rename operation. Instead, it interprets the change as two separate actions:</p><ol><li><strong>DROP</strong> column <strong><em>customer_id</em></strong></li><li><strong>ADD</strong> column <strong><em>client_id</em></strong></li></ol><p><strong>The Implication:<br></strong>If this auto-generated script is deployed to production, it will <strong>delete the column and all its data</strong>, then create a new, empty column.</p><p>Because of this risk, the best practice is to <strong>avoid automated changelog generation</strong> for production deployments. Instead, engineers should manually author SQL changesets. This ensures that a rename is explicitly written as <strong><em>ALTER TABLE … RENAME COLUMN</em></strong>, preserving the data.</p><h3>Tool Selection: Why Liquibase and not Databricks Asset Bundles (DABs)</h3><p>If you have already implemented Infrastructure as Code using Terraform or Databricks Asset Bundles, you might wonder why you should introduce a new technology and tool into your ecosystem, just for the sake of Schema Management.</p><p>While both tools manage Databricks resources, they serve fundamentally different purposes based on the <em>lifecycle</em> of the object and the consequences of replacing it.</p><p><strong>1. The Case for Databricks Asset Bundles (DABs): Resource Definition<br></strong>DABs follow an “Infrastructure as Code” (IaC) philosophy. They are designed for resources where the <em>definition</em> is the master, and the actual object can be updated, restarted, or replaced to match that definition.</p><p><strong>Best For:</strong></p><ul><li><strong>Spark™ Declarative Pipelines </strong>with Materialized Views.</li><li><strong>Lakeflow Jobs and Pipelines:</strong> Jobs, tasks, and pipeline schedules.</li><li><strong>Compute Infrastructure:</strong> Job Clusters, SQL Warehouses, and Instance Pools.</li></ul><p><strong>Why:</strong> These resources are <strong>declarative</strong>.</p><ul><li>If you update a <strong>Job</strong> schedule in a DAB, the platform simply overwrites the old schedule.</li><li>If you change a <strong>Compute</strong> configuration, the cluster restarts with the new settings.</li><li>If you modify logic in a <strong>Spark Declarative Pipeline</strong>, the system handles the update automatically, often by refreshing the flow or recomputing the data.<br>The state is either ephemeral (compute) or fully derived from code (pipelines).</li></ul><p><strong>2. The Case for Liquibase: Persistent State Evolution<br></strong>Liquibase follows an “Evolutionary Database” philosophy. It is designed for resources where the <em>data</em> is the primary asset, and the structure must be carefully mutated around it.</p><p><strong>Best For:</strong> Standard Managed Tables (Delta tables).</p><p><strong>Why:</strong> These tables maintain a <strong>persistent</strong> state that cannot be simply recomputed or replaced. You cannot “re-deploy” a standard table containing 5 years of historical customer data just because you want to add a <strong><em>middle_name</em></strong> column; replacing the table would delete the history. Liquibase excels here because it executes granular, imperative modifications (<strong><em>ALTER TABLE…, UPDATE…</em></strong>) that evolve the schema version by version, while strictly preserving the underlying data.</p><p><strong>The Rule of Thumb:</strong></p><ul><li>If the resource describes <strong>how code runs</strong> (Jobs, Compute) or <strong>data derived from code</strong> (Declarative Pipelines) → <strong>Use DABs.</strong></li><li>If the resource describes <strong>stored data</strong> that must be preserved and evolved over time (Standard Tables) → <strong>Use Liquibase.</strong></li></ul><h3>Conclusion</h3><p>By combining the governance of <strong>Databricks Unity Catalog</strong> with <strong>Liquibase</strong>’s version control capabilities, we showed how data teams can bring software engineering rigor to their data platforms. Our example architecture replaces anxiety-inducing manual updates with a transparent, automated process where every change is tracked, reviewed, and reversible.</p><p>While it requires an initial investment in setup, creating the repository structure, configuring the Docker container, and defining the pipelines, the result is a stable, audit-ready data environment that scales with your team.</p><p>Liquibase offers a wide range of capabilities, allowing you to quickly define your production-ready Data Products. Some automations, however, require careful evaluation, and manual definitions and steps should be preferred.</p><h3>Final remark</h3><p>Tools like Liquibase for database change management, CI/CD platforms for automation, and unified governance layers such as Unity Catalog provide the technical foundation. However, sustainable success comes from pairing these tools with a culture of domain ownership, thorough documentation, rigorous testing, and accountability for data quality and accessibility. Without data owners and stewards living up to these expectations and managers supporting the organizational change to develop data products ready for production, it is hard to achieve, and the potential for shortening the path from idea to insight and from insight to action remains unutilized.</p><p>To successfully implement production-ready data products, you will need <strong>both</strong> <strong>people</strong> and <strong>technology</strong>.</p><h3>About the author</h3><p>Manuel Beuttler is a Delivery Solutions Architect at Databricks, helping customers across Central EMEA advance their data journey. Drawing on his experience in building data pipelines on Azure Databricks and establishing data-driven products and services in the sheet metal manufacturing industry, he brings a strong mix of hands-on engineering and practical governance expertise to every engagement.</p><h3>Additional Resources</h3><p>Documentation:</p><ul><li><a href="https://docs.liquibase.com/">Liquibase Official Documentation</a></li><li><a href="https://docs.databricks.com/data-governance/unity-catalog/best-practices.html">Databricks Unity Catalog Best Practices</a></li><li><a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/process/templates">Azure DevOps Pipeline Templates</a></li><li><a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/process/approvals?view=azure-devops&amp;tabs=check-pass">Azure DevOps Environments and Approvals</a></li></ul><p>Community Posts:</p><ul><li><a href="https://medium.com/dbsql-sme-engineering/advanced-schema-management-on-databricks-with-liquibase-1900e9f7b9c0">Advanced Schema Management on Databricks with Liquibase</a></li><li><a href="https://community.databricks.com/t5/technical-blog/databricks-schema-versioning-with-flyway-and-liquibase-a-step-by/ba-p/90504">Databricks Schema Versioning with Flyway and Liquibase: A Step-by-Step Guide</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=6d1dbf5e43a3" width="1" height="1" alt=""><hr><p><a href="https://medium.com/dbsql-sme-engineering/governed-schema-evolution-on-databricks-6d1dbf5e43a3">Governed Schema Evolution on Databricks</a> was originally published in <a href="https://medium.com/dbsql-sme-engineering">DBSQL SME Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Dynamic Catalog & Schema in Databricks Dashboards (DABs, API, SDK, Terraform)]]></title>
            <link>https://medium.com/dbsql-sme-engineering/dynamic-catalog-schema-in-databricks-dashboards-b7eea62270c6?source=rss----a346361fdb66---4</link>
            <guid isPermaLink="false">https://medium.com/p/b7eea62270c6</guid>
            <category><![CDATA[databricks-dashboard]]></category>
            <category><![CDATA[data-visualization]]></category>
            <category><![CDATA[databricks]]></category>
            <category><![CDATA[data-engineering]]></category>
            <category><![CDATA[databricks-mvp]]></category>
            <dc:creator><![CDATA[Maksim Pachkouski Databricks MVP]]></dc:creator>
            <pubDate>Mon, 05 Jan 2026 18:13:58 GMT</pubDate>
            <atom:updated>2026-08-15T13:05:19.421Z</atom:updated>
            <content:encoded><![CDATA[<p>New AI/BI Dashboards in Databricks are becoming increasingly popular because they don&#39;t require additional licenses and are very easy to deploy. If you&#39;ve worked with DABs (Databricks Asset Bundles) or used the API to publish dashboards in dev and prod, you&#39;ve likely encountered issues with catalog and schema parameterization.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*IcDuYTuZ9mraaQ1ao-AbXw.jpeg" /></figure><p>Judging by the forums, some specialists have been waiting two years for a solution using various auto-replacements. However, I have an inside scoop for you: parameterization <a href="https://github.com/databricks/cli/releases/tag/v0.281.0"><strong>is finally working in the new release v0.281.0</strong></a><strong> !</strong></p><p>The feature was released two weeks ago, so it&#39;s not yet documented. I tested it and wrote examples of how it works. Spoiler alert: it didn&#39;t quite work out that way…</p><h3>Why is this a problem at all?</h3><p>Typically, you first develop a Databricks Dashboard in the <strong>dev</strong> environment and then migrate it to <strong>prod</strong>. In the Databricks Unity Catalog world, this means your data is stored in different catalogs:</p><ul><li><strong>dev_catalog</strong>.edl_schema.table1</li><li><strong>prod_catalog</strong>.edl_schema.table1</li></ul><p>If your dashboard&#39;s code (file <strong>.lvdash.json</strong>) is hardcoded to <strong>dev_catalog</strong>, the dashboard will continue to look at dev data or crash with an access error when deployed to production. DABs can override parameters for Jobs and DLTs, but they support a few dashboard parameters. Engineers had to write Python scripts that parsed the dashboard&#39;s JSON and physically changed the text before deployment.</p><h3>What happened?</h3><p>Databricks has updated its API and expanded parameterization. The resource now dashboard accepts two new parameters: <strong>dataset_catalog</strong> and <strong>dataset_schema</strong>. This means we can finally ditch the hacks and use native variables.</p><h3>How to transfer dashboards between development environments</h3><p>In Databricks, a data engineer has five main options for migrating Lakeview dashboards (CI/CD) from Dev to Prod, in addition to manually working through the UI:</p><ol><li><strong>DABs (Databricks Asset Bundles)</strong> are a new native way to manage projects.</li><li><strong>Terraform</strong> is a classic Infrastructure as Code (IaC).</li></ol><p>You can also create and update a dashboard using separate commands:</p><ol><li><strong>SDK</strong> - for those who need complex migration logic.</li><li><strong>REST API</strong> is the lowest-level and most powerful method.</li><li><strong>Databricks CLI</strong> — for Bash scripting enthusiasts.</li></ol><p>Let&#39;s see how this works in practice.</p><h4>Option 1: Databricks Asset Bundles (DABs) — The Right Way</h4><p>These parameters were especially welcomed by those using DABs. Now we can explicitly set <strong>dataset_catalog</strong> and <strong>dataset_schema,</strong> which are correctly passed to the dashboard context during deployment.</p><p>Don&#39;t forget to update your CLI to version <strong>0.281.0</strong>. Here is an example <strong>databricks.yml</strong> :</p><pre>bundle:<br>  name: project_2026_01_01<br><br>variables:<br>  dataset_catalog:<br>    description: Target Catalog for the environment<br>    default: dev<br>  dataset_schema:<br>    description: Target Schema for the environment<br>    default: edl_test<br>  warehouse_id:<br>    description: SQL Warehouse ID<br><br>resources:<br>  dashboards:<br>    my_dashboard:<br>      display_name: &quot;Test NEW feature 2026 (${bundle.target})&quot;<br>      warehouse_id: ${var.warehouse_id}<br>      file_path: ./src/dashboards/test1.lvdash.json<br>      embed_credentials: true<br>      dataset_catalog: ${var.dataset_catalog}   #🔥 NEW<br>      dataset_schema: ${var.dataset_schema}     #🔥 NEW<br><br>targets:<br>  dev:<br>    mode: development<br>    default: true<br>    variables:<br>      dataset_catalog: dev       #🔥 NEW<br>      dataset_schema: edl_test   #🔥 NEW<br>      warehouse_id: &quot;123456789101112&quot;<br>  <br>  prod:<br>    mode: production<br>    variables:<br>      dataset_catalog: prod   #🔥 NEW<br>      dataset_schema: edl     #🔥 NEW<br>      warehouse_id: &quot;123456789101112&quot;</pre><p>When executed:</p><pre>databricks bundle deploy -t prod</pre><p>DABs don&#39;t modify the original JSON file. Instead, when creating a dashboard object in the workspace, it sets the default context (Catalog/Schema) at the level of all datasets. When the user opens the dashboard, all queries without an explicit catalog (for example, <strong><em>SELECT * FROM dash1</em></strong><em> </em>) are executed in the <strong>prod.edl context:</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*u9iB5Db2uVSJ1lNt_Uzw9A.png" /></figure><h4>Option 2: Terraform</h4><p>Terraform is HashiCorp&#39;s Infrastructure as Code (IaC) tool that allows you to describe and manage cloud infrastructure through declarative configuration files. Instead of manually creating resources through the UI or REST API, you describe the desired infrastructure state in code, and Terraform automatically configures the actual infrastructure to that state. It also now supports <strong>dataset_catalog</strong> and <strong>dataset_schema</strong> for Databricks Dashboards starting with version <strong>1.100.0 . </strong>Here&#39;s an example<strong> main.tf</strong> file :</p><pre>terraform {<br>  required_providers {<br>    databricks = {<br>      source  = &quot;databricks/databricks&quot;<br>      version = &quot;&gt;= 1.100.0&quot;<br>    }<br>  }<br>}<br><br>provider &quot;databricks&quot; {<br>  host  = var.workspace_url<br>  token = var.databricks_token<br>}<br><br>resource &quot;databricks_dashboard&quot; &quot;sales&quot; {<br>  display_name         = &quot;NEW Terraform Dashboard - ${upper(var.environment)}&quot;<br>  parent_path          = var.parent_path<br>  warehouse_id         = var.warehouse_id<br>  serialized_dashboard = file(&quot;${path.module}/test2.lvdash.json&quot;)<br>  <br>  dataset_catalog = var.dataset_catalog    # 🔥 NEW<br>  dataset_schema  = var.dataset_schema     # 🔥 NEW<br>}<br><br>output &quot;dashboard_url&quot; {<br>  value = &quot;https://${var.workspace_url}/sql/dashboardsv3/${databricks_dashboard.sales.id}&quot;<br>}<br><br>output &quot;environment&quot; {<br>  value = var.environment<br>}</pre><h4>Option 3: Python SDK</h4><p>We can also create and update a Dashboard using the Python SDK. But first, we need to update the SDK to the latest version:</p><pre>%pip install -U databricks-sdk<br>dbutils.library.restartPython()</pre><p><strong>Creating a dashboard</strong> <strong>through SDK</strong>, but in this example, the dashboard will actually be copied, replacing the catalog and schema:</p><pre>from databricks.sdk import WorkspaceClient<br><br>wc = WorkspaceClient()<br><br>dashboard_path = &quot;/Workspace/Users/my_email@gmail.com/test1.lvdash.json&quot;<br><br>workspace_obj = wc.workspace.get_status(path=dashboard_path)<br>source_dashboard = wc.lakeview.get(dashboard_id=workspace_obj.resource_id)<br><br>new_dashboard = wc.lakeview.create(<br>    dashboard=Dashboard(<br>        display_name=&quot;NEW SDK 2026_01&quot;,<br>        serialized_dashboard=source.serialized_dashboard,<br>        warehouse_id=source_dashboard.warehouse_id  <br>    ),<br>    dataset_catalog=&quot;prod&quot;,  #🔥 NEW<br>    dataset_schema=&quot;edl&quot;     #🔥 NEW<br>)</pre><p><strong>Updating a dashboard via SDK</strong>. For example, we have a dashboard with many datasets, and we want to change the catalog and schema for all of them:</p><pre>from databricks.sdk import WorkspaceClient<br>from databricks.sdk.service.dashboards import Dashboard<br><br>wc = WorkspaceClient()<br><br>dashboard_path = &quot;/Workspace/Users/my_email@gmail.com/test1.lvdash.json&quot;<br><br>workspace_obj = wc.workspace.get_status(path=dashboard_path)<br>dashboard_id = workspace_obj.resource_id<br>current_dashboard = wc.lakeview.get(dashboard_id=dashboard_id)<br><br>updated_dashboard = wc.lakeview.update(<br>    dashboard_id=dashboard_id,<br>    dashboard=current_dashboard,<br>    dataset_catalog=&quot;prod&quot;,     #🔥 NEW<br>    dataset_schema=&quot;edl&quot;        #🔥 NEW<br>)</pre><h4>Option 4: Working via API</h4><p>For those writing their own deployment scripts in Python, there&#39;s the option to create and update dashboards. The code via API appears more cumbersome than via SDK. Below is an example of creating one by copying an existing one and replacing the directory and schema:</p><pre>fimport requests<br>import json<br><br>browser_host =  spark.conf.get(&quot;spark.databricks.workspaceUrl&quot;)<br>host = f&quot;https://{browser_host}&quot;<br><br>new_token = wc.tokens.create(comment=&quot;Python SDK Generated Token&quot;, lifetime_seconds=9600)<br>token = new_token.token_value<br><br>DASHBOARD_PATH = &quot;/Workspace/Users/my_email@gmail.com/test1.lvdash.json&quot;<br><br>headers = {<br>    &quot;Authorization&quot;: f&quot;Bearer {token}&quot;,<br>    &quot;Content-Type&quot;: &quot;application/json&quot;,<br>    &quot;Accept&quot;: &quot;application/json&quot;<br>}<br><br>workspace_response = requests.get(<br>    f&quot;{host}/api/2.0/workspace/get-status&quot;,<br>    headers=headers,<br>    json={&quot;path&quot;: DASHBOARD_PATH}<br>)<br>workspace_response.raise_for_status()<br>dashboard_id = workspace_response.json()[&quot;resource_id&quot;]<br><br>source_response = requests.get(f&quot;{host}/api/2.0/lakeview/dashboards/{dashboard_id}&quot;, headers=headers)<br>source_response.raise_for_status()<br>source_data = source_response.json()<br><br>serialized_dashboard = source_data[&quot;serialized_dashboard&quot;]<br>warehouse_id = source_data[&quot;warehouse_id&quot;]<br><br>create_payload = {<br>    &quot;display_name&quot;: &quot;NEW API 2026_01&quot;,<br>    &quot;serialized_dashboard&quot;: serialized_dashboard,<br>    &quot;warehouse_id&quot;: warehouse_id<br>}<br><br>create_response = requests.post(<br>    f&quot;{host}/api/2.0/lakeview/dashboards&quot;,<br>    headers=headers,<br>    params={<br>        &quot;dataset_catalog&quot;: &quot;prod&quot;,     #🔥 NEW<br>        &quot;dataset_schema&quot;: &quot;edl&quot;        #🔥 NEW<br>    },<br>    json=create_payload<br>)<br>create_response.raise_for_status()</pre><p>Now let&#39;s update the catalog and diagram in the existing dashboard:</p><pre>import requests<br>import json<br><br>browser_host = spark.conf.get(&quot;spark.databricks.workspaceUrl&quot;)<br>host = f&quot;https://{browser_host}&quot;<br><br>new_token = wc.tokens.create(comment=&quot;Python SDK Generated Token&quot;, lifetime_seconds=9600)<br>token = new_token.token_value<br><br>DASHBOARD_PATH = &quot;/Workspace/Users/my_email@gmail.com/test1.lvdash.json&quot;<br><br>headers = {<br>    &quot;Authorization&quot;: f&quot;Bearer {token}&quot;,<br>    &quot;Content-Type&quot;: &quot;application/json&quot;,<br>    &quot;Accept&quot;: &quot;application/json&quot;<br>}<br><br>workspace_response = requests.get(<br>    f&quot;{host}/api/2.0/workspace/get-status&quot;,<br>    headers=headers,<br>    json={&quot;path&quot;: DASHBOARD_PATH}<br>)<br>workspace_response.raise_for_status()<br>dashboard_id = workspace_response.json()[&quot;resource_id&quot;]<br><br>source_response = requests.get(<br>    f&quot;{host}/api/2.0/lakeview/dashboards/{dashboard_id}&quot;, <br>    headers=headers<br>)<br>source_response.raise_for_status()<br>source_data = source_response.json()<br><br>update_payload = {<br>    &quot;display_name&quot;: source_data[&quot;display_name&quot;],<br>    &quot;serialized_dashboard&quot;: source_data[&quot;serialized_dashboard&quot;],<br>    &quot;warehouse_id&quot;: source_data[&quot;warehouse_id&quot;]<br>}<br><br>update_response = requests.patch(<br>    f&quot;{host}/api/2.0/lakeview/dashboards/{dashboard_id}&quot;,<br>    headers=headers,<br>    params={<br>        &quot;dataset_catalog&quot;: &quot;dev&quot;,       # 🔥 NEW<br>        &quot;dataset_schema&quot;: &quot;edl_test&quot;    # 🔥 NEW<br>    },<br>    json=update_payload<br>)<br>update_response.raise_for_status()</pre><h4>Option 5: CLI</h4><p>It is also possible to create and update dashboards via the CLI, but this is rarely used in practice, so I will omit examples.</p><h4>What conclusions did I draw from testing the new parameters?</h4><ol><li><strong>New parameters are not saved:</strong> After the request is completed, the API &quot;forgets&quot; these parameters. The dashboard simply retains the hard-coded catalog and schemas.</li><li><strong>The current implementation is more like “Find &amp; Replace”:</strong> When you send a request, the Databricks server takes your JSON (serialized_dashboard), goes inside the array <strong>datasets</strong>, finds the fields there, the catalog, and the schema physically <strong>overwrites</strong> them with the values ​​you passed.</li></ol><blockquote>I think it would be great if Databricks added the ability to set a default catalog and schema for the entire Dashboard. After adding a new dataset, the default schema is &quot;workspace,&quot; and the default catalog is &quot;default&quot;:</blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*NcibjlJwoxSzsLCd5poZ7Q.png" /></figure><p><em>Author:</em> <strong>Maksim Pachkouski</strong> [Databricks MVP &amp; Databricks Solutions Architect Champion]. Follow my posts on <a href="https://medium.com/@protmaks">Medium</a> and <a href="https://www.linkedin.com/in/protmaks/">LinkedIn</a> to stay up-to-date on the latest news in Data Engineering and learn about best practices.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b7eea62270c6" width="1" height="1" alt=""><hr><p><a href="https://medium.com/dbsql-sme-engineering/dynamic-catalog-schema-in-databricks-dashboards-b7eea62270c6">Dynamic Catalog &amp; Schema in Databricks Dashboards (DABs, API, SDK, Terraform)</a> was originally published in <a href="https://medium.com/dbsql-sme-engineering">DBSQL SME Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>