in . The title is the item's first ,
+// tagged with a class because :first-child ignores text nodes (CLAUDE.md).
+const html = raw
+ .replace(/<\/?p>/g, '')
+ .replace(/(]*)?>)\s*/g, '$1');
+const stages = (html.match(/ 6) console.warn(`StageStrip: ${stages} stages; a strip reads best with 3 to 6`);
+---
+
+
+ {caption &&
{caption}
}
+
+
diff --git a/src/content/docs/data-apps/authentication.mdx b/src/content/docs/data-apps/authentication.mdx
index 4552da206..f490c1cf1 100644
--- a/src/content/docs/data-apps/authentication.mdx
+++ b/src/content/docs/data-apps/authentication.mdx
@@ -30,7 +30,7 @@ Once an app is deployed, its URL is publicly available. Protect it so only the r
## Authentication methods
- **None (Public Access)** — the app is public to anyone with the URL. You can still add your own authorization inside the app; for Streamlit, use the [Streamlit authenticator](https://github.com/mkhorasani/Streamlit-Authenticator) ([example](https://github.com/keboola/mkt-bi-ocr/blob/master/Select_Invoices.py)).
-- **Basic (Password)** — the **default** for new apps. Keboola generates a shared password; users enter it before the app opens. Once the app is deployed, the password is shown on the app's configuration page next to **Open App**, ready to copy — and when Kai builds an app, it shows the password as the last step.
+- **Basic (Password)** — the **default** for new apps. Keboola generates a shared password; users enter it before the app opens. Once the app is deployed, click **Open App** on its configuration page: the dialog has the app's address and the password, each with a copy button. When Kai builds an app, it shows the password as the last step.
- **OIDC (Custom)** — users sign in with your identity provider (Google, Microsoft Entra ID, Okta, Auth0, or any other OIDC provider). Recommended for anything beyond a quick share.
- **GitHub** — restrict access with GitHub OAuth by organization, team, repository, or allowed users.
- **GitLab** — restrict access with GitLab OAuth by groups, projects, or roles.
diff --git a/src/content/docs/data-apps/build-locally.mdx b/src/content/docs/data-apps/build-locally.mdx
index eb57f2059..046b58ee0 100644
--- a/src/content/docs/data-apps/build-locally.mdx
+++ b/src/content/docs/data-apps/build-locally.mdx
@@ -9,6 +9,7 @@ redirect_from:
import { Tabs, TabItem } from '@astrojs/starlight/components';
import Prereqs from '../../../components/Prereqs.astro';
+import StageStrip from '../../../components/StageStrip.astro';
{/* 2026-09-29: "Build an app with an AI agent" (PR #1143) merged into this page after review (PRDCT-693); the old URL redirects here. Agent flow verified live on 2026-09-23 in project 264 (europe-west3) with Claude Code 2.1.280, kbagent 0.94.0 and dataapp-developer 1.6.1 from marketplace keboola-claude-kit 1.14.0 (App ID 74021867, about 11 minutes, including the Storage workaround below), and on 2026-09-25 with Codex CLI 0.157.0 (App ID 74022065). Michal Ševčík's Cursor review (2026-09-25): agent mode is the default, the route question appears, a prompt that names kbagent reads better, the password is the main friction. The example prompt was then revised to name kbagent and ask for a data check and the page link, and revised again to name the project and rule out MCP, after his run built in another project through an MCP sign-in; VERIFY(Michal Ševčík) that wording in Cursor. Claude Desktop, VS Code and the ChatGPT app screens: VERIFY the dataapp-developer install and the build flow. */}
@@ -54,6 +55,13 @@ To build inside Keboola instead, [Kai](/data-apps/getting-started/) does the sam
With the `dataapp-developer` plugin from Keboola's [AI Kit](/ai/ai-kit/), the agent reads your data, writes the code, creates the app with a Git repository that Keboola manages, pushes the code there and deploys it. You end up with a running app at its own URL, and the code in a repository you can keep changing.
+
+ Set up your client Connect your project and add the app-building plugin.
+ Describe the app One prompt: what it shows, which data, where the code goes.
+ The agent builds it It reads your data, writes code and deploys; you approve its commands.
+ Check the app On its page in Keboola, Open App has the URL and password.
+
+
### Set up your client
@@ -153,9 +161,15 @@ Through a Keboola MCP server, the agent doesn't run kbagent. It calls the server
### Check the app
- The agent finishes with the app's URL and its page in Keboola.
-- The app asks for its password, which is on that page next to **Open App**.
-- To let other people open it, see [Publish and share](/data-apps/publish-and-share/).
+- The app asks for its password. On the app's page in Keboola, click **Open App**. The dialog has the app's address and its password, each with a copy button. Copy the password, then click **Open app** in the dialog.
+
+
+
- If the app opens without data, ask the agent to read the app's log. [Troubleshooting](/data-apps/troubleshooting/) lists the common causes, including `Promise.withResolvers is not a function` from a too-new `@keboola/api-client`.
+- On [the MCP route](#the-mcp-route), you preview a draft at its own URL first. The production app gets **Open App** once you approve the draft and the agent deploys it.
+- To let other people open it, see [Publish and share](/data-apps/publish-and-share/).
+
+{/* Password location checked 2026-09-29 in project 264 on App ID 74021867, started for the check and stopped after: Open App opens a dialog with App address and Password (hidden, each with a copy button); a stopped app's page shows no password (its Open App dialog was not opened), and its Authentication section names only the method, Basic (Password). VERIFY(Nikita): not checked on a sleeping app. VERIFY(Nikita): seen on europe-west3 only; the UI bundles read between 22 and 29 Sep still had an older App Credentials modal (Host, Password, an Open App button), so check the labels on us-east4, where free projects live. The MCP-route bullet restates The MCP route section above and operate.md (Open App on a running app, Deploy App on one that never ran). VERIFY(Nikita): where a draft's password shows was not checked. */}
### What you get by default
@@ -165,7 +179,7 @@ The agent usually can't show you that password, because `kbagent data-app passwo
To make the app public, say so in the prompt. Anyone with its URL can then open it and see the data it shows. Changing an existing app's sign-in, including to single sign-on, belongs in its [Authentication](/data-apps/authentication/) settings.
-{/* "Say so in the prompt": kbagent data-app create offers --auth public (0.95.0 help); no run has asked for a public app yet, VERIFY(Nikita). Defaults from the kbagent create dry run (2026-09-23; it printed the size as `tiny`, which the UI and Miro Cillik's review on PR #1146 call XSmall) and the skill's app-type choice (choosing-app-type.md: Node.js + static frontend is the dashboard default; Streamlit for Python-only teams and quick prototypes). The page URL pattern is the ui-detail link the Keboola MCP server returns for a data app. The password location follows authentication.mdx; VERIFY(Michal Ševčík): the MCP link title says to click OPEN DATA APP to see it, so confirm where it shows. Asking an agent to remove the password of an existing app (2026-09-25) set auth_required to false and redeployed, yet the proxy still asked for the password 6.5 minutes later; VERIFY(Michal Ševčík) whether that takes effect later or needs the UI. */}
+{/* "Say so in the prompt": kbagent data-app create offers --auth public (0.95.0 help); no run has asked for a public app yet, VERIFY(Nikita). Defaults from the kbagent create dry run (2026-09-23; it printed the size as `tiny`, which the UI and Miro Cillik's review on PR #1146 call XSmall) and the skill's app-type choice (choosing-app-type.md: Node.js + static frontend is the dashboard default; Streamlit for Python-only teams and quick prototypes). The page URL pattern is the ui-detail link the Keboola MCP server returns for a data app. Asking an agent to remove the password of an existing app (2026-09-25) set auth_required to false and redeployed, yet the proxy still asked for the password 6.5 minutes later; VERIFY(Michal Ševčík) whether that takes effect later or needs the UI. */}
### Other agents
@@ -201,6 +215,13 @@ To add the skill to an agent by hand, download it below. The download is a copy
Write the code yourself, then create the app from a terminal with kbagent, which gives it a Keboola-managed repository, or, in the Keboola UI, connect a repository you host.
+
+ Write the code A standard web app, plus a small configuration folder.
+ Create the app kbagent creates it with a Keboola-managed repository.
+ Push the code Commit and push it to the app's repository.
+ Deploy and open Deploy, then click Open App on its page for the password.
+
+
### App structure
A Keboola app is a standard web app. The typical scaffold is:
@@ -261,7 +282,7 @@ kbagent creates the app with a Keboola-managed repository, and you push your cod
kbagent data-app detail --project --app-id
```
-The password is on the app's page, `/admin/projects//data-apps/`, next to **Open App**. With a Manage API token, `kbagent data-app password --project --app-id ` prints it too. If the app opens without data, `kbagent data-app logs --project --app-id ` shows its log; if it doesn't start at all, `kbagent data-app runs --project --app-id ` shows why.
+The password is on the app's page, `/admin/projects//data-apps/`: click **Open App** and copy it from the dialog. With a Manage API token, `kbagent data-app password --project --app-id ` prints it too. If the app opens without data, `kbagent data-app logs --project --app-id ` shows its log; if it doesn't start at all, `kbagent data-app runs --project --app-id ` shows why.
### Sync to your project
diff --git a/src/content/docs/data-apps/getting-started.mdx b/src/content/docs/data-apps/getting-started.mdx
index 016c6c095..3258fa7b0 100644
--- a/src/content/docs/data-apps/getting-started.mdx
+++ b/src/content/docs/data-apps/getting-started.mdx
@@ -76,7 +76,11 @@ When you're happy with the draft, click **Publish to Production**. Kai merges th

-By default the app is protected with **Basic (Password)** authentication — the generated password also sits on the app's configuration page, ready to copy. To change who can access it (public, a password, SSO, GitHub, and more), see [Authentication](/data-apps/authentication/).
+By default the app is protected with **Basic (Password)** authentication. The generated password is also on the app's configuration page: click **Open App** in the page header and copy it from the dialog.
+
+{/* The Open App dialog (App address, Password, each with a copy button) was seen on 2026-09-29 on a running app built with kbagent (project 264, App ID 74021867). VERIFY(Nikita): the same on apps Kai built or created in the UI. */}
+
+To change who can access it (public, a password, SSO, GitHub, and more), see [Authentication](/data-apps/authentication/).
Open the URL, enter the password, and your app is live — running on your governed data, served from its own address:
@@ -114,7 +118,7 @@ Prefer to set the app up yourself, without the chat? Manual creation lives on th

-7. When the status turns **Active**, click **Open App** to open it at its public URL. Use **Redeploy** to apply any later config change; see [App actions](/data-apps/reference/#app-actions).
+7. When the status turns **Active**, click **Open App**. For an app behind a password, copy the password from the dialog, then click **Open app** in it to open the app at its public URL. Use **Redeploy** to apply any later config change; see [App actions](/data-apps/reference/#app-actions).
From here on, the app is something you run rather than something you build: [Operate and update an app](/data-apps/operate/) covers shipping changes, secrets, sleeping and versions.
diff --git a/src/content/docs/data-apps/operate.md b/src/content/docs/data-apps/operate.md
index d5a321c68..7aa6b6a9c 100644
--- a/src/content/docs/data-apps/operate.md
+++ b/src/content/docs/data-apps/operate.md
@@ -10,6 +10,8 @@ Deploying, starting, stopping, deleting and setting secrets also work from a ter
## Read the app's state
+
+
The header shows the app's status and the one or two actions that make sense for it: **Deploy App** for an app that has never run, **Start App** for a stopped one, **Open App** and **Redeploy App** for a running one, and **Modify with Kai** on apps Kai built. The **⋯** menu holds the rest, and its contents depend on the state too: **Copy app**, **Automate** (add it to a flow), **Debug mode** and **Delete app** are always there, with **Suspend app** on a running app.
The tabs below the header split the app's life into views: **Overview** (the app's settings and its App URL), **Advanced Settings** (environment variables and secrets, theme, data mappings), **All Runs** (every start attempt), **Terminal Logs** (stdout and stderr while it runs), **Versions** (the configuration history), and **Drafts** while Kai has a draft open. The **App Info** panel on the right shows the backend version and size, the auto-sleep timeout, the last change, the owner, and the App ID.
diff --git a/src/content/docs/data-apps/publish-and-share.md b/src/content/docs/data-apps/publish-and-share.md
index b3a855e91..642bcfd9d 100644
--- a/src/content/docs/data-apps/publish-and-share.md
+++ b/src/content/docs/data-apps/publish-and-share.md
@@ -25,6 +25,8 @@ Share the app URL — found on the app's **Overview** tab (the **App URL** block
## Manage a deployed app
+
+
From the app's header you can **Modify with Kai**, **Open App**, and **Start App** / **Redeploy App** depending on its state. The **⋯** menu holds the rest: **Copy app**, **Automate** (add it to a flow), **Debug mode** and **Delete app**, plus **Suspend app** while the app is running. [Operate and update an app](/data-apps/operate/) walks through each one.

diff --git a/src/content/docs/data-apps/reference.md b/src/content/docs/data-apps/reference.md
index 418b89195..646e603b9 100644
--- a/src/content/docs/data-apps/reference.md
+++ b/src/content/docs/data-apps/reference.md
@@ -180,7 +180,7 @@ Manage a deployed app from its actions menu.

- **Deploy App** — starts the app. Once the deployment job finishes, open the app's public URL with **Open App**.
-- **Open App** — opens a new window with your app.
+- **Open App** — opens your app in a new window. On a running app behind a password, it shows a dialog first, with the app's address and the password, each with a copy button; the dialog's **Open app** button opens the app.
- **Redeploy App** — apply changes made in the app configuration (they take effect only after a redeploy).
- **Start App** — brings a stopped app back with the same settings.
- **Modify with Kai** — opens the Builder, on apps Kai built.
@@ -190,6 +190,8 @@ Manage a deployed app from its actions menu.
- **Debug mode** — runs the app with extra diagnostics.
- **Delete app** — stops the deployment and deletes its configuration.
+
+
Which of these the header and the **⋯** menu offer depends on the app's state. [Operate and update an app](/data-apps/operate/) walks through them in the order you meet them.
## Sleep and resume
diff --git a/src/content/docs/data-apps/troubleshooting.md b/src/content/docs/data-apps/troubleshooting.md
index 696e101b5..391d39c89 100644
--- a/src/content/docs/data-apps/troubleshooting.md
+++ b/src/content/docs/data-apps/troubleshooting.md
@@ -50,7 +50,7 @@ Most app problems fall into a handful of patterns. Find where the evidence is fi
## Sign-in problems
-- **Basic (Password):** the password is on the app's configuration page next to **Open App** once the app is deployed.
+- **Basic (Password):** once the app is deployed, click **Open App** on its configuration page and copy the password from the dialog.
- **OIDC:** the Google, Entra, Okta and Auth0 tabs on [Authentication](/data-apps/authentication/) each end with an "If sign-in fails" list covering that provider's error texts, redirect-URI mismatches, and audience settings.
- **GitHub, GitLab, JumpCloud:** each has its own required fields and its own optional restrictions, which are the first thing to check when the right person is turned away. GitHub filters by organization, team, repository and allowed users; GitLab by group, project and allowed roles; JumpCloud by allowed roles. All three are in their sections of [Authentication](/data-apps/authentication/).
- **Nobody can get in after you changed the authentication settings:** the change takes effect on the next start. Click **Redeploy App** (running app) or **Start App** (stopped app).
diff --git a/src/integrations/page-markdown.mjs b/src/integrations/page-markdown.mjs
index 32044295a..9a2539ec8 100644
--- a/src/integrations/page-markdown.mjs
+++ b/src/integrations/page-markdown.mjs
@@ -105,9 +105,9 @@ function prereqsTabsToText(tag, overrides = {}) {
* otherwise turn ordinary prose into an indented code block.
*
* Components are matched on an uppercase initial, the JSX convention, so
- * lowercase HTML written inline in a page is left alone. Two of them carry
+ * lowercase HTML written inline in a page is left alone. Three of them carry
* meaning a reader needs and are rendered rather than dropped: a tab's
- * `label`, and .
+ * `label`, , and , which prints as a numbered list.
*/
function stripMdx(body) {
const withoutImports = body.replace(/^import\s[^\n]*?;\s*$/gm, '');
@@ -122,6 +122,8 @@ function stripMdx(body) {
const close = /^\s*<\/[A-Z][A-Za-z0-9]*\s*>\s*$/;
let depth = 0;
+ let inStrip = false;
+ let stripN = 0;
let inPrereqs = false;
let prereqsTag = '';
let prereqsTabs = {};
@@ -136,6 +138,7 @@ function stripMdx(body) {
}
if (close.test(line)) {
depth = Math.max(0, depth - 1);
+ if (/^\s*<\/StageStrip>/.test(line)) { inStrip = false; out.push(''); }
if (/^\s*<\/Prereqs>/.test(line)) {
out.push(...prereqsGroupToText(prereqsTag, groupItems), ...prereqsTabsToText(prereqsTag, prereqsTabs));
inPrereqs = false;
@@ -153,6 +156,7 @@ function stripMdx(body) {
// with page-specific children in its slots: the
// shared lines go in first, the children follow as the lists they are.
if (/^\s* per stage: flatten it, number it, and end the bold title with a period
+ slot.push(text);
+ if (/<\/li>/.test(text)) {
+ const item = inlineToMarkdown(slot.join(' ')).replace(/^- /, '');
+ slot.length = 0;
+ if (item) out.push(`${++stripN}. ${item.replace(/^\*\*(.+?)\*\*\s*/, '**$1.** ')}`);
+ }
+ continue;
+ }
if (inPrereqs) {
// the slots hold raw JSX, or a tab's own line as ;
// buffer until the item closes, then flatten
diff --git a/src/styles/custom.css b/src/styles/custom.css
index 4e7e0ea0d..8427fe1ac 100644
--- a/src/styles/custom.css
+++ b/src/styles/custom.css
@@ -2422,6 +2422,110 @@ nav.sidebar {
}
.prereqs__label + .prereqs__group { margin-top: 0; }
+/* ───────── Stage strip (src/components/StageStrip.astro) ───────── */
+/* Numbered dots on a line, as on the Getting Started hub poster. Horizontal while
+ the content column is wide, vertical with the text beside the dots when it is
+ not: a container query, because between 800 and 1400 px the sidebar leaves the
+ column about 450 px wide. Dots and line in --kbc-blue-600, where white numerals
+ pass 4.5:1 (on blue-500 they don't). */
+.stage-strip { container-type: inline-size; margin: 1rem 0 1.5rem; }
+/* the same small caps label as the Prereqs box */
+.stage-strip__caption {
+ margin: 0 0 0.6rem;
+ font-size: var(--sl-text-xs);
+ font-weight: 600;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+ color: var(--sl-color-gray-2);
+}
+.stage-strip__list {
+ --dot: 1.75rem;
+ --gap: 0.75rem;
+ display: flex;
+ gap: var(--gap);
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ counter-reset: stage;
+}
+.stage-strip__list > li {
+ position: relative;
+ flex: 1 1 0;
+ min-width: 0;
+ padding-top: calc(var(--dot) + 0.6rem);
+ text-align: center;
+ font-size: var(--sl-text-sm);
+ line-height: 1.45;
+ color: var(--sl-color-gray-3);
+ counter-increment: stage;
+ overflow-wrap: anywhere;
+}
+/* the numbered dot */
+.stage-strip__list > li::before {
+ content: counter(stage);
+ position: absolute;
+ top: 0;
+ left: calc(50% - var(--dot) / 2);
+ z-index: 1;
+ display: grid;
+ place-items: center;
+ width: var(--dot);
+ height: var(--dot);
+ border-radius: 50%;
+ background: var(--kbc-blue-600);
+ color: #fff;
+ font-size: 0.85rem;
+ font-weight: 700;
+ line-height: 1;
+}
+/* the segment from this dot to the next one */
+.stage-strip__list > li:not(:last-child)::after {
+ content: '';
+ position: absolute;
+ top: calc(var(--dot) / 2 - 1.5px);
+ left: 50%;
+ right: calc(-50% - var(--gap));
+ height: 3px;
+ background: var(--kbc-blue-600);
+}
+.stage-strip__title {
+ display: block;
+ margin-bottom: 0.2rem;
+ font-size: var(--sl-text-base);
+ font-weight: 700;
+ color: var(--sl-color-white);
+}
+.stage-strip a { color: var(--link); text-decoration: underline; text-underline-offset: 2px; }
+.stage-strip a:hover { color: var(--link-hover); text-decoration: none; }
+.stage-strip__title a {
+ color: inherit;
+ text-decoration: underline;
+ text-decoration-color: var(--sl-color-gray-5);
+ text-decoration-thickness: 1px;
+ text-underline-offset: 3px;
+}
+.stage-strip__title a:hover { color: var(--sl-color-text-accent); text-decoration-color: currentColor; }
+@container (max-width: 36rem) {
+ .stage-strip__list { flex-direction: column; gap: 1rem; }
+ /* flex: none, or the row layout's flex-basis 0 plus min-height squeezes each
+ stage to one dot's height and its text runs into the next stage */
+ .stage-strip__list > li {
+ flex: none;
+ padding: 0.15rem 0 0 calc(var(--dot) + 0.75rem);
+ min-height: var(--dot);
+ text-align: left;
+ }
+ .stage-strip__list > li::before { left: 0; }
+ .stage-strip__list > li:not(:last-child)::after {
+ top: var(--dot);
+ bottom: -1rem;
+ left: calc(var(--dot) / 2 - 1.5px);
+ right: auto;
+ width: 3px;
+ height: auto;
+ }
+}
+
/* ───────── Print ───────── */
/* print.css ships `.print\:hidden { display: none }` inside @media print, but
Page.css loads after it, so any rule in THIS file that sets `display` on a