Two ways to use this template
- 1. Click "Copy prompt" below
- 2. Paste into Cursor, Claude Code, Codex, or any coding agent
- 3. Your agent builds the app — it asks questions along the way so the result is exactly what you want
Follow the steps below to set things up manually, at your own pace.
Genie Multi-Space Selector
Add a space selector so users can switch between multiple AI/BI Genie spaces from a single page. Covers multi-alias server config, per-space bundle resources, and automatic conversation cleanup on space switch and redeployment.
Prerequisites
This template upgrades an existing single-space Genie app to switch between multiple spaces. Verify these Databricks workspace features are enabled before starting. If any check fails, ask your workspace admin to enable the feature.
- Databricks CLI authenticated. Run
databricks auth profilesand confirm at least one profile showsValid: YES. If none do, authenticate withdatabricks auth login --host <workspace-url> --profile <PROFILE>. - AI/BI Genie enabled. Run
databricks genie list-spaces --profile <PROFILE>and confirm the command succeeds. - Two or more Genie spaces configured. The list above must return at least two spaces — one per entry you want in the selector. If you have fewer, create additional spaces in AI/BI Genie in the Databricks UI.
- Databricks Apps enabled. Run
databricks apps list --profile <PROFILE>and confirm the command succeeds. - A scaffolded AppKit app with the Genie feature. Complete the Genie Conversational Analytics template first. This template only covers the upgrade from one space to many — not the initial Genie wiring.
Genie Multi-Space Selector
Upgrade a single-space Genie app to let users switch between multiple AI/BI Genie spaces from a dropdown. Each space gets a named alias; switching spaces remounts <GenieChat> and clears stale conversation state automatically.
1. List all Genie spaces you want to include
List your spaces:
databricks genie list-spaces -o json --profile <PROFILE>
Tip: Avoid repeating --profile on every command
Add your profile to the bundle's databricks.yml under the target — then bundle deploy and apps commands pick it up automatically:
targets:
default:
workspace:
profile: <PROFILE>
This is more reliable than export DATABRICKS_CONFIG_PROFILE since it persists across shells and works for agents running commands in subshells.
Note the space_id and title for each space. Decide on a short lowercase alias for each (e.g. sales, support) — these become the keys in the server spaces map and in the SPACES array on the client. They must match exactly.
2. Update the server plugin
Replace the single-space genie() call in server/server.ts with a spaces map. Use one environment variable per space following the pattern DATABRICKS_GENIE_SPACE_<ALIAS>:
import { createApp, genie, server } from "@databricks/appkit";
createApp({
plugins: [
server(),
genie({
spaces: {
sales: process.env.DATABRICKS_GENIE_SPACE_SALES ?? "",
support: process.env.DATABRICKS_GENIE_SPACE_SUPPORT ?? "",
},
}),
],
}).catch(console.error);
Each key becomes the alias for all API routes (/api/genie/<alias>/messages) and the <GenieChat alias="..." /> prop. Add one entry per space.
3. Update configuration files
.env (local development)
Keep DATABRICKS_GENIE_SPACE_ID — AppKit requires it at startup even when using a custom spaces map. Point it at any of your spaces. Add one variable per UI space:
DATABRICKS_GENIE_SPACE_ID=<any-space-id>
DATABRICKS_GENIE_SPACE_SALES=<sales-space-id>
DATABRICKS_GENIE_SPACE_SUPPORT=<support-space-id>
app.yaml
Keep the DATABRICKS_GENIE_SPACE_ID → genie-space mapping — AppKit validates it on startup. Add one valueFrom per UI space:
command: ["npm", "run", "start"]
env:
- name: DATABRICKS_GENIE_SPACE_ID
valueFrom: genie-space
- name: DATABRICKS_GENIE_SPACE_SALES
valueFrom: genie-space-sales
- name: DATABRICKS_GENIE_SPACE_SUPPORT
valueFrom: genie-space-support
databricks.yml
Keep genie_space_id and genie-space — AppKit requires DATABRICKS_GENIE_SPACE_ID to be set at runtime. This resource does not appear in the UI dropdown; only aliases in the server spaces map do. Add a new variable and resource for each UI space:
variables:
genie_space_id:
description: Default Genie space ID (required by AppKit)
genie_space_sales_id:
description: Sales Genie space ID
genie_space_support_id:
description: Support Genie space ID
resources:
apps:
app:
user_api_scopes:
- dashboards.genie
resources:
- name: genie-space
genie_space:
name: genie-space
space_id: ${var.genie_space_id}
permission: CAN_RUN
- name: genie-space-sales
genie_space:
name: genie-space-sales
space_id: ${var.genie_space_sales_id}
permission: CAN_RUN
- name: genie-space-support
genie_space:
name: genie-space-support
space_id: ${var.genie_space_support_id}
permission: CAN_RUN
targets:
default:
variables:
genie_space_id: <any-space-id>
genie_space_sales_id: <sales-space-id>
genie_space_support_id: <support-space-id>
Repeat the variable and resource block for every space you want in the UI.
4. Inject a build version stamp
GenieChat stores the active conversation ID in two places that can become stale across space switches or redeployments:
- URL: stored as
?conversationId=<id>, read on every mount to replay history. When the user switches spaces, the newGenieChatinstance reads this param and tries to fetch the old conversation through the new space's alias, resulting inNOT_FOUND. - localStorage:
appkit:genie:*keys for related state. After a redeployment, stored IDs may no longer exist in the Genie backend, resulting inNOT_FOUND.
Stamping every build with a timestamp lets the page detect a new deployment and clean up before GenieChat mounts.
In client/vite.config.ts, add a define block alongside your existing config:
export default defineConfig({
// ... existing config ...
define: {
// Changes on every build so the page can detect a new deployment
// and clear stale conversation state before GenieChat mounts.
"import.meta.env.VITE_APP_VERSION": JSON.stringify(Date.now().toString()),
},
});
5. Replace the Genie page
Replace the contents of client/src/pages/genie/GeniePage.tsx with the multi-space version below.
clearConversationUrl strips ?conversationId from the URL before the alias state changes, so the newly mounted GenieChat instance always starts without a stale cross-space conversation reference.
initAlias runs once at component mount. On build-version mismatch it wipes all appkit:genie:* localStorage keys and clears the URL param, then restores the user's last-selected alias before returning it as initial state.
import { useState } from "react";
import { GenieChat } from "@databricks/appkit-ui/react";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@databricks/appkit-ui/react";
const SPACES = [
{ alias: "sales", label: "Sales Analytics" },
{ alias: "support", label: "Support Analytics" },
];
const VERSION_KEY = "appkit:genie:version";
const ALIAS_KEY = "appkit:genie:alias";
function clearConversationUrl() {
const url = new URL(window.location.href);
url.searchParams.delete("conversationId");
window.history.replaceState({}, "", url.toString());
}
function initAlias(): string {
const buildVersion = import.meta.env.VITE_APP_VERSION ?? "dev";
if (localStorage.getItem(VERSION_KEY) !== buildVersion) {
const savedAlias = localStorage.getItem(ALIAS_KEY);
Object.keys(localStorage)
.filter((k) => k.startsWith("appkit:genie:"))
.forEach((k) => localStorage.removeItem(k));
localStorage.setItem(VERSION_KEY, buildVersion);
if (savedAlias) localStorage.setItem(ALIAS_KEY, savedAlias);
clearConversationUrl();
}
return localStorage.getItem(ALIAS_KEY) ?? SPACES[0]?.alias ?? "";
}
export function GeniePage() {
const [selectedAlias, setSelectedAlias] = useState(initAlias);
return (
<div className="space-y-6 w-full max-w-4xl mx-auto">
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold text-foreground">Genie</h2>
<p className="text-sm text-muted-foreground mt-1">
Ask questions about your data using Databricks AI/BI Genie.
</p>
</div>
<Select
value={selectedAlias}
onValueChange={(alias) => {
clearConversationUrl();
setSelectedAlias(alias);
localStorage.setItem(ALIAS_KEY, alias);
}}
>
<SelectTrigger className="w-52">
<SelectValue placeholder="Select space" />
</SelectTrigger>
<SelectContent>
{SPACES.map((space) => (
<SelectItem key={space.alias} value={space.alias}>
{space.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="h-[600px] border rounded-lg overflow-hidden">
<GenieChat key={selectedAlias} alias={selectedAlias} />
</div>
</div>
);
}
No changes are needed in client/src/App.tsx — the import, nav link, and route from the single-space setup carry over unchanged.
6. Deploy and test
From the app project directory (the folder containing databricks.yml):
# Build the client
npm run build
# Deploy bundle resources and sync files to workspace
# Copy the upload path printed in the output — you'll need it below
databricks bundle deploy
# Put the app in RUNNING state and wait for compute to be ready
# The loop polls every 5 seconds — press Ctrl+C if it hangs more than 2 minutes
databricks apps start <app-name>
until databricks apps get <app-name> -o json | grep -q '"ACTIVE"'; do sleep 5; done
# First deploy requires --source-code-path: paste the path from bundle deploy output above
databricks apps deploy <app-name> \
--source-code-path <path-from-bundle-deploy-output>
bundle deploy prints the workspace upload path (Uploading bundle files to ...) — copy that value for --source-code-path. apps start puts the app into RUNNING state; the until loop waits for compute to be ACTIVE. apps deploy deploys the source and starts the app server.
For subsequent deploys, --source-code-path is not needed. Run both bundle deploy and apps deploy when changing databricks.yml; for client-only changes, apps deploy alone is sufficient after npm run build:
npm run build
databricks bundle deploy
databricks apps deploy <app-name>
Check app status and get the URL:
databricks apps get <app-name>
If compute is STOPPED, run databricks apps start <app-name> and wait for compute_status.state: ACTIVE before deploying.
Open <app-url>/genie while signed in to Databricks and verify:
- The space selector shows all configured spaces
- Asking a question routes to the correct space
- Switching spaces resets the conversation with no
NOT_FOUNDerror - Reloading the page restores the last selected space and replays the conversation
- After redeploying, stale conversation IDs are automatically cleared on the next page load