Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"plugins": [
{
"name": "kbagent",
"version": "0.51.1",
"version": "0.52.0",
"source": "./plugins/kbagent",
"description": "AI-friendly interface to Keboola Connection projects — explore configs, jobs, lineage, call MCP tools, manage dev branches, and debug SQL in workspaces",
"category": "development"
Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,7 @@ kbagent storage truncate-table --project NAME --table-id ID [--table-id ...] [--
kbagent storage delete-column --project NAME --table-id ID --column COL [--column ...] [--force] [--dry-run] [--yes] [--branch ID]
kbagent storage delete-bucket --project NAME --bucket-id ID [--bucket-id ...] [--force] [--dry-run] [--yes] [--branch ID]
kbagent storage swap-tables --project NAME --table-id ID --target-table-id ID --branch ID [--dry-run] [--yes]
kbagent storage clone-table --project NAME --table-id ID --branch ID [--dry-run]
kbagent storage describe-bucket --project NAME --bucket-id ID [--text STR | --file PATH | --stdin] [--branch ID]
kbagent storage describe-table --project NAME --table-id ID [--text STR | --file PATH | --stdin] [--branch ID]
kbagent storage describe-column --project NAME --table-id ID --column NAME=DESC [--column ...] [--branch ID]
Expand Down
2 changes: 1 addition & 1 deletion plugins/kbagent/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "kbagent",
"version": "0.51.1",
"version": "0.52.0",
"description": "AI-friendly interface to Keboola Connection projects — explore configs, jobs, lineage, call MCP tools, manage dev branches, and debug SQL in workspaces",
"author": {
"name": "Keboola",
Expand Down
62 changes: 26 additions & 36 deletions plugins/kbagent/agents/keboola-expert.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,10 @@ a critical failure.
needed for the current task (e.g. `flow update` needs 0.22.0+,
`schedule find` needs 0.23.0+, `config set-default-bucket` needs
0.26.0+, `data-app create / deploy / start / stop / delete / password`
need 0.27.0+, `config update` script[] string-to-array auto-normalize
against #245 trap needs 0.28.0+, list-element re-split against
need 0.27.0+, `config update` script[] auto-normalize (#245) needs
0.28.0+, list-element re-split against
the #274 ODBC `Actual statement count N != desired 1` crash needs
0.31.0+, `storage swap-tables` needs 0.28.0+,
0.31.0+, `storage swap-tables` needs 0.28.0+, `storage clone-table` = 0.52.0+,
env-var manage-token auth for `org setup` / `project refresh` /
`data-app password` needs 0.29.0+ with `--allow-env-manage-token`,
`project invite` / `project member-*` / `project invitation-*`
Expand Down Expand Up @@ -147,9 +147,9 @@ a critical failure.
| Fetch a specific config | `kbagent config detail --project P --component-id C --config-id K --json` | `tool call get_config` | re-using an earlier JSON dump |
| Override the auto-derived output bucket on a config | `kbagent config set-default-bucket --bucket in.c-name` (0.26.0+) -- read-modify-write of `storage.output.default_bucket`, preserves siblings; `--clear` removes it | `kbagent config update --set 'storage.output.default_bucket=in.c-name'` (works pre-0.26.0 but not discoverable) | editing the raw JSON in the UI; full-config replace with `--configuration` (wipes other storage keys) |
| Cross-project migration | `kbagent sync pull` + edit files locally + `kbagent sync push --dry-run` | -- | repeated `tool call` loops, one per resource |
| Retype table columns | fetch types via `workspace query`, draft types YAML, write new transformation that produces typed output table, then `kbagent storage swap-tables` (0.28.0+) to flip the typed copy into the original name in a dev branch | `kbagent --hint client create_table_definition` if the future `storage retype` composite (§14.3) is not yet present | `POST /v2/storage/buckets/.../tables-definition` (REST) followed by manual config rewrites |
| Retype table columns | fetch types via `workspace query`, draft types YAML, write new transformation that produces typed output table, then `kbagent storage swap-tables` (0.28.0+) to flip the typed copy into the original name in any branch | -- | `POST /v2/storage/buckets/.../tables-definition` (REST) followed by manual config rewrites |
| Create typed table with native types | `kbagent storage create-table --column pk:VARCHAR(40) --column amount:NUMBER(18,2) --not-null pk --default amount=0` (0.25.0+) | `tool call create_table` (accepts the same `definition.length` shape via MCP) | re-creating via raw REST to `/v2/storage/...tables-definition` |
| Promote typed rebuild back into the original name | `kbagent storage swap-tables --project P --table-id in.c-foo.data --target-table-id in.c-foo.data_change_log --branch <ID> --yes` (0.28.0+) -- async storage job (`tableSwap`); client polls to completion before returning. Service refuses without a branch | -- | renaming or deleting + re-uploading (loses history; downstream configs need to be rewritten) |
| Promote typed rebuild back into the original name | `kbagent storage swap-tables --project P --table-id in.c-foo.data --target-table-id in.c-foo.data_change_log --branch <ID> --yes` (0.28.0+) -- async storage job (`tableSwap`); client polls to completion. Service refuses without a branch; any branch incl. prod | -- | renaming or deleting + re-uploading (loses history; downstream configs need to be rewritten) |
| Re-seed a table without losing its schema / PK / dependents | `kbagent storage truncate-table --project P --table-id in.c-foo.data [--branch ID] [--dry-run] [--yes]` (0.32.0+) -- DELETE `/tables/{id}/rows?allowTruncate=1`; endpoint is uniformly async on every branch (returns a queued `tableRowsDelete` job; client polls via `_wait_for_storage_job`). Do NOT pass `async=true` -- the API rejects it. Batch via repeated `--table-id`. Returns `{truncated[], failed[], dry_run, project_alias}` with `truncated[]` entries carrying `{table_id, rows_before, rows_after, branch_id}`. Permission class: `destructive` | `tool call delete_table_rows` if the upstream MCP exposes it | drop + recreate the table (loses descriptions, PK, sharing edges, and breaks every downstream config reference); deleting rows via raw SQL in a workspace (bypasses the Storage API audit trail) |
| Debug a failed job | `kbagent job detail --project P --job-id J --json` + `kbagent job run ... --log-tail-lines 200` | `kbagent workspace from-transformation` for SQL repro | "I think the issue is..." without reading logs |
| Ad-hoc SQL / row-count / type audit | `kbagent workspace create` + `kbagent workspace load` + `kbagent workspace query --sql "..."` | `kbagent workspace from-transformation` for existing transform debugging; `workspace list --qs-compatible` (0.42.0+, #304) for data-app reuse | querying Keboola Storage directly via Snowflake credentials outside the workspace abstraction |
Expand Down Expand Up @@ -294,12 +294,16 @@ success, not a failure.
plan -- it sets up the user for an impossible step.

- **`storage create-table` in a dev branch auto-materializes the bucket**
(0.25.0+): if the target bucket has not been written to in the branch
yet, kbagent creates it there first (mirrors the Go CLI's
`EnsureBucketExists`). The response's `auto_created_bucket: true` is
informational, not an error -- surface it to the user in a write
verification payload but do not treat it as a failure signal.
Production writes never materialize anything.
(0.25.0+): if the target bucket has no branch-local write yet, kbagent
creates it first (mirrors the Go CLI `EnsureBucketExists`).
`auto_created_bucket: true` is informational, not a failure. Production
writes never materialize anything.
- **`storage clone-table` before an in-branch `swap-tables` / column drop**
(0.52.0+): on `storage-branches` projects a dev branch reads prod tables
transparently until first write, so a swap/drop (a write) fails with a
misleading "bucket not found" until the prod table is branch-local. Run
`kbagent storage clone-table --project P --table-id T --branch <ID>`
first (one-way default->branch). See `gotchas.md`.

- **`storage truncate-table` is row-only; schema and dependents are
preserved** (0.32.0+): the underlying call is
Expand Down Expand Up @@ -483,31 +487,17 @@ success, not a failure.
`--allow-env-manage-token` to their invocation, never strip the
warning by suppressing stderr.

- **Semantic-layer gotchas (since v0.41.0)** — five behavior contracts
worth committing to memory before touching `semantic-layer add/edit/
remove`. Full prose lives in
[`gotchas.md` § Semantic-layer](../skills/kbagent/references/gotchas.md);
the short form:
- **Constraint `rule` is a STRING**, never `{bounds: {min, max}}`. The
sl-builder skill docs are wrong on this. kbagent enforces it.
- **Constraint `name` regex `^[a-z][a-z0-9_]*$`** + the 3-vs-4
severity split: API `severity` is `error | warning | info` (3-level);
the 4-band health (`_critical / _warning / _healthy / _review`)
lives in the NAME SUFFIX, not on the API.
- **`edit metric --new-name` cascades through every constraint** whose
`metrics[]` referenced the old name, and prints the old/new
CODE_METRIC value. Downstream SQL joining on CODE_METRIC will break
silently — surface the change to the operator.
- **`remove metric` orphans constraints** that reference it. The
pre-deletion scan ALWAYS prints the warning (even with `--yes`);
non-TTY without `--yes` exits 2. Recommended: drop/rewrite the
constraints first, then remove the metric.
- **`build` is a HEURISTIC fallback**, not full AI: one dataset +
one COUNT(*) metric + one glossary entry per table. Response carries
`fallback_used: "heuristic"`. Treat the output as a scaffold and
follow up with `add metric`, `add relationship`, `add constraint`.
The full AI wizard lives in the `sl-build` skill under
`04_AI_Kit/ai-kit/`.
- **Semantic-layer gotchas (since v0.41.0)** — full prose in
[`gotchas.md` § Semantic-layer](../skills/kbagent/references/gotchas.md).
Key traps: constraint `rule` is a STRING (not `{bounds: {min, max}}`);
`severity` is 3-level (`error|warning|info`) while the 4-band health
lives in the name suffix (`_critical/_warning/_healthy/_review`), not the
API; `edit metric --new-name` cascades into constraints' `metrics[]` and
changes CODE_METRIC (surface it -- downstream joins break silently);
`remove metric` orphans referencing constraints (drop/rewrite them
first; the scan warns even with `--yes`, non-TTY exits 2); `build` is a
heuristic scaffold (`fallback_used: "heuristic"`), not the full AI wizard
(that lives in the `sl-build` skill).

---

Expand Down
1 change: 1 addition & 0 deletions plugins/kbagent/skills/kbagent/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ When working inside a git repository or project directory, run `kbagent init` (o
| Truncate (delete all rows from) one or more storage tables | `kbagent storage truncate-table --project PROJECT --table-id TABLE-ID` |
| Delete one or more columns from a storage table | `kbagent storage delete-column --project PROJECT --table-id TABLE-ID --column COLUMN` |
| Swap two storage tables in a development branch | `kbagent storage swap-tables --project PROJECT --table-id TABLE-ID --target-table-id TARGET-TABLE-ID` |
| Clone (pull) a production table into a development branch | `kbagent storage clone-table --project PROJECT --table-id TABLE-ID` |
| Delete one or more storage buckets | `kbagent storage delete-bucket --project PROJECT --bucket-id BUCKET-ID` |
| Set the description on a storage bucket | `kbagent storage describe-bucket --project PROJECT --bucket-id BUCKET-ID` |
| Set the description on a storage table | `kbagent storage describe-table --project PROJECT --table-id TABLE-ID` |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ Requires a **super-admin** Manage API token (same kind as `org setup`). Same def
- `storage delete-column --project NAME --table-id ID --column COL [--column ...] [--force] [--dry-run] [--yes] [--branch ID]` -- delete columns from a table (branch-aware)
- `storage delete-bucket --project NAME --bucket-id ID [--bucket-id ...] [--force] [--dry-run] [--yes] [--branch ID]` -- delete buckets (branch-aware)
- `storage swap-tables --project NAME --table-id ID --target-table-id ID --branch ID [--dry-run] [--yes]` (since v0.28.0) -- swap two storage tables in a dev branch (POST `/tables/{id}/swap`). Both tables exchange physical positions; aliases are NOT transferred (they keep pointing at the same physical position and therefore expose the OTHER table's data after the swap). Service refuses without a branch (active branch via `branch use` works too). Use to flip a typed rebuild ("data_change_log") into the original name ("data") without touching downstream config references
- `storage clone-table --project NAME --table-id ID --branch ID [--dry-run]` (since v0.52.0) -- pull (clone) a production table into a dev branch (POST `/tables/{id}/pull`, operationName `devBranchTablePull`). On `storage-branches` projects a dev branch reads prod tables transparently until the first write, so an in-branch schema mutation (`swap-tables`, dropping a column) fails with a misleading "bucket not found" until the table is materialized branch-local; `clone-table` does that. One-way (default -> branch). Service refuses without a branch (active branch via `branch use` works too). Permission class `write`
- `storage describe-bucket --project NAME --bucket-id ID [--text STR | --file PATH | --stdin] [--branch ID]` -- set a bucket description (stored as `KBC.description` in bucket metadata, upsert). Provide exactly one of `--text`, `--file`, `--stdin`. Read back via `storage bucket-detail`
- `storage describe-table --project NAME --table-id ID [--text STR | --file PATH | --stdin] [--branch ID]` -- set a table description (stored as `KBC.description` in table metadata, upsert). Provide exactly one of `--text`, `--file`, `--stdin`. Read back via `storage table-detail`
- `storage describe-column --project NAME --table-id ID --column NAME=DESCRIPTION [--column ...] [--branch ID]` -- set one or more column descriptions. Stored as `KBC.column.{name}.description` keys in the table's metadata (Keboola has no user-writable column-metadata endpoint). Read back in `storage table-detail` under `column_details[].description`
Expand Down
64 changes: 54 additions & 10 deletions plugins/kbagent/skills/kbagent/references/gotchas.md
Original file line number Diff line number Diff line change
Expand Up @@ -766,23 +766,67 @@ events and emits a final `done` SSE frame mirroring the same record.
latest -- previously only the latest was reported, leaving the user with
no signal whether their cache was stale.

## `storage swap-tables` is dev-branch only and aliases stay put (since v0.28.0)
## `storage swap-tables` is branch-scoped and aliases stay put (since v0.28.0)

- `kbagent storage swap-tables --project P --table-id A --target-table-id B
--branch <ID>` swaps two tables' physical positions in a dev branch
--branch <ID>` swaps two tables' physical positions
(`POST /v2/storage/branch/{branch}/tables/{id}/swap`).
- The Storage API rejects this on production. The service refuses with
exit 5 / `ConfigError` *before* any HTTP call when neither `--branch`
nor an active branch (via `branch use`) is set.
- **branch_id is mandatory, but any branch works -- including the
default/production branch.** The service refuses with exit 5 /
`ConfigError` *before* any HTTP call only when neither `--branch` nor an
active branch (via `branch use`) is set. (The earlier "rejected on
production" claim was wrong -- verified live 2026-06-01: a default-branch
swap succeeds and is the supported way to retype a prod table.)
- **Aliases are NOT transferred.** They keep pointing at the same
physical position, so after the swap they expose the OTHER table's
data. Plan downstream config rewrites if any aliased consumer relies
on schema, not data.
- Typical use: AI agent profiles a typeless table, builds a typed
rebuild called `<name>_change_log` via CTAS in a dev branch, then
swaps it back into the original name. After merging the branch the
original table now carries the typed schema with no downstream config
rewrite required.
- **Dev-branch merge does NOT carry storage schema** (only configs), so a
swap done inside a dev branch never reaches production via merge. The
dev branch is a *rehearsal* -- profile the typeless table, build a typed
rebuild (`<name>_change_log`) via CTAS, swap, and run downstream configs
against it to prove the typed schema is consumer-safe. Then discard the
branch and run the real build + swap in the production (default) branch.
Full procedure: `typify-table-workflow.md`.

## `storage clone-table` materializes a prod table into a dev branch (since v0.52.0)

- `kbagent storage clone-table --project P --table-id T --branch <ID>`
pulls a production table into a dev branch
(`POST /v2/storage/branch/{branch}/tables/{id}/pull`, operationName
`devBranchTablePull` -- the same call the platform issues on a branch's
first write to a prod table).
- **Why it matters on `storage-branches` projects:** a dev branch reads
production tables transparently (copy-on-write) until the first write.
A schema mutation in the branch -- `swap-tables`, dropping a column --
targets a table that is not yet branch-local, so the Storage API fails
with a misleading `"bucket ... was not found in the project"`. Run
`clone-table` first to materialize the table branch-local; the swap /
drop then succeeds. (Verified live 2026-06-01 on project 10539 with
storage-branches ON: clone -> in-branch swap succeeds; production left
untouched.)
- **One-way (default -> branch).** There is no "push branch -> default":
branch storage is never merged back to production (only configurations
are). The pull is the only API path between the two table stores.
- Branch is mandatory: the service refuses with exit 5 (`ConfigError`)
before any HTTP call when neither `--branch` nor an active branch (via
`branch use`) is set.
- Permission class: `write` (creates a branch-local copy; never deletes).

## Dev-branch merge carries only configurations, NOT storage schema (since v0.52.0, verified 2026-06-01)

- When a dev branch is merged to production, Keboola propagates
**configuration** changes only. Physical storage tables -- their
schema, column types, and rows -- are **not** merged back. (Confirmed
by the storage-branches designer and Keboola's public docs:
help.keboola.com/tutorial/branches/merge-to-production.)
- Consequence for retyping: a `swap-tables` (or `clone-table`) done inside
a dev branch stays in the branch. To retype a **production** table you
run the build + `swap-tables` in the production (default) branch itself.
The dev branch is only a rehearsal that validates the typed schema
against downstream configs. Full procedure: `typify-table-workflow.md`.
- The only API path between the two table stores is `clone-table` (pull,
default -> branch). There is no "push branch -> default".

## `storage truncate-table` preserves schema; endpoint is uniformly async-via-job (since v0.32.0)

Expand Down
Loading
Loading