Skip to content
Open
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.66.0",
"version": "0.66.1",
"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
3 changes: 3 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,9 @@ kbagent flow schedule-remove --project NAME --flow-id ID [--branch ID] [--yes]
# flow validate: with --project fetches the live schema (full validation; fetch failure ->
# semantic-only + note); without --project runs semantic-only + a note. flow schema --full
# requires --project (fetches live schema); plain flow schema is the offline YAML template.
# flow schedule (0.66.1+) also activates the config on the Scheduler Service so the cron fires;
# activation failure keeps the config written, sets activated=false + warning, exit stays 0.
# flow schedule-remove deregisters from the service before deleting each config.
# Execute a flow with: kbagent job run --project NAME --component-id keboola.flow --config-id ID

kbagent schedule list [--project NAME ...] [--enabled-only] [--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.66.0",
"version": "0.66.1",
"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
7 changes: 7 additions & 0 deletions plugins/kbagent/agents/keboola-expert.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,13 @@ read it when a trigger fires. Each `(X.Y.Z+)` tag is the version floor.
phases+tasks -- fetch `flow detail` first, merge locally, run
`flow validate --file @merged.yaml --project ALIAS` (full schema) until clean,
then push.
- **`flow schedule` activates on the Scheduler Service (0.66.1+)**: older
versions only wrote the `keboola.scheduler` config -- the schedule showed
`enabled` but the cron NEVER fired. On 0.66.1+ the command also registers it
with the service; check `activated: true` in the result. `activated: false`
+ warning = config written but dormant (token lacks the privilege) -- re-run
with an admin token. **VERSION GATE**: schedules created by < 0.66.1 stay
dormant until `flow schedule` is re-run on 0.66.1+.
- **Snowflake transformation scaffolding**: MCP `create_config` REFUSES
`keboola.snowflake-transformation`. Use `config new --push --no-files`
(0.33.0+) or `config new --output-dir` then `config update`, or MCP
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -225,8 +225,8 @@ Requires the project to be added with its **master ('owner') Storage API token**
- `flow new --project NAME --name NAME [--description D] [--file @path.yaml|-|JSON] [--branch ID]` -- create a conditional flow; validated against the **live** CF schema fetched from the stack before the API call (`INVALID_FLOW_DEFINITION` on failure). A schema-fetch failure does NOT block the write: structural check skipped, semantic checks still run, a `structural schema validation skipped` warning is surfaced
- `flow update --project NAME --flow-id ID [--name N] [--description D] [--file @path.yaml|-|JSON] [--branch ID]` -- update name, description, or phases/tasks; `--file` is a full-replace of phases+tasks; merge-aware validation against the live CF schema (same graceful semantic-only degradation on fetch failure); requires at least one of --name/--description/--file
- `flow delete --project NAME --flow-id ID [--branch ID] [--yes]` -- delete a flow config (confirmation guard)
- `flow schedule --project NAME --flow-id ID --cron "0 6 * * *" [--timezone TZ] [--disabled] [--branch ID]` -- attach a cron schedule (stored as keboola.scheduler config, target.componentId=keboola.flow); replaces any existing schedule
- `flow schedule-remove --project NAME --flow-id ID [--branch ID] [--yes]` -- remove all cron schedules attached to a flow; idempotent
- `flow schedule --project NAME --flow-id ID --cron "0 6 * * *" [--timezone TZ] [--disabled] [--branch ID]` -- attach a cron schedule (stored as keboola.scheduler config, target.componentId=keboola.flow); replaces any existing schedule. Since v0.66.1 the config is also activated on the Scheduler Service so the trigger fires; an activation failure keeps the config written and surfaces a warning (`activated: false`, exit 0)
- `flow schedule-remove --project NAME --flow-id ID [--branch ID] [--yes]` -- remove all cron schedules attached to a flow (since v0.66.1 also deregisters them from the Scheduler Service); idempotent

## Schedule Discovery & Audit (Fleet-Wide)
- `schedule list [--project NAME ...] [--enabled-only] [--branch ID]` -- fleet-wide list of every `keboola.scheduler` config across one, many, or all projects (parallel fan-out, no --project = all). Each row has `project_alias`, `schedule_id`, `schedule_name`, `parent_component_id`, `parent_config_id`, `parent_name`, `cron`, `timezone`, `enabled`. Answers "which configs are running on cron triggers across N projects?" without enumerating flows
Expand Down
15 changes: 15 additions & 0 deletions plugins/kbagent/skills/kbagent/references/gotchas.md
Original file line number Diff line number Diff line change
Expand Up @@ -2284,6 +2284,21 @@ The trade-off is deliberate: one big call avoids the O(unique-parents) round-tri
scheduler configs that target the flow. Pair it with `--dry-run` to see the
affected configs (cron + timezone) without calling `delete_config`.

## Flow: `schedule` activates on the Scheduler Service (since v0.66.1)

- `kbagent flow schedule` registers the `keboola.scheduler` config with the
**Scheduler Service** (`POST /schedules`) after writing it — writing the
Storage config alone leaves the cron trigger dormant. Older versions only
wrote the config: a schedule they created shows `state: enabled` but never
fires until `flow schedule` is re-run on v0.66.1+ (or the schedule is
re-saved in the UI).
- Activation failure is **non-fatal**: the config stays written, the result
carries `activated: false` + a warning, and the exit code stays 0. Typical
cause is a Storage token without the schedule-management privilege — re-run
with an admin token to activate.
- `flow schedule-remove` (v0.66.1+) deregisters each schedule from the
Scheduler Service before deleting its config, so removal stops the trigger.

## `search` is a top-level command, not `config search` (since v0.30.0)

`kbagent search QUERY` searches across **all item types** (tables, buckets, configs, flows, data apps, transformations) via the Storage API global-search endpoint. It is distinct from `kbagent config search --query Q` which scans only configuration JSON bodies.
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "keboola-cli"
version = "0.66.0"
version = "0.66.1"
description = "AI-friendly CLI for managing Keboola projects"
readme = "README.md"
requires-python = ">=3.12"
Expand Down
14 changes: 14 additions & 0 deletions src/keboola_agent_cli/changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,20 @@

# Ordered newest-first. Each value is a list of brief one-line descriptions.
CHANGELOG: dict[str, list[str]] = {
"0.66.1": [
"Fix (#479): `flow schedule` now activates the schedule on the Scheduler Service, so the cron "
"trigger actually fires. Previously the command only wrote the `keboola.scheduler` Storage "
"config; the schedule looked `enabled` but never ran until re-saved in the UI. The command "
"now calls `POST /schedules` on the Scheduler Service after the config upsert (also for "
"`--disabled`, which deregisters the trigger). An activation failure -- e.g. a token without "
"the schedule-management privilege -- keeps the config written, reports `activated: false` + "
"a warning, and exits 0. Schedules created by older kbagent versions stay dormant until "
"`flow schedule` is re-run on 0.66.1+.",
"Fix (#479): `flow schedule-remove` now deregisters each schedule from the Scheduler Service "
"(`DELETE /configurations/{id}`) before deleting its Storage config, so removed schedules "
"stop firing. Deregistration failures other than 404 are surfaced as warnings and do not "
"block the config deletion.",
],
"0.66.0": [
"New: device-enrollment primitives on the importable library -- a hosted Data App can now mint "
"per-device credentials in-process (no CLI subprocess, no master token on the device). "
Expand Down
9 changes: 6 additions & 3 deletions src/keboola_agent_cli/commands/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -625,11 +625,14 @@
kbagent flow schedule --project NAME --flow-id ID --cron "0 6 * * *" [--timezone TZ] [--enabled/--disabled] [--name NAME] [--branch ID]
Upsert a cron schedule: updates the existing keboola.scheduler config if one exists, creates one
otherwise. Calling twice with a new cron replaces the old schedule — no duplicates created.
Schedules are stored as Storage API configs, not a separate scheduler service.
The config is then activated on the Scheduler Service so the cron trigger fires; an activation
failure (e.g. token cannot manage schedules) keeps the config written, sets activated=false, and
surfaces a warning (exit stays 0). Re-run with a capable token to activate.

kbagent flow schedule-remove --project NAME --flow-id ID [--branch ID] [--yes]
Remove all schedules bound to this flow (deletes all matching keboola.scheduler configs).
Idempotent: safe to run when no schedules exist.
Remove all schedules bound to this flow: each schedule is deregistered from the Scheduler
Service, then its keboola.scheduler config is deleted. Idempotent: safe to run when no
schedules exist.

### Schedule Discovery & Audit (Fleet-Wide)

Expand Down
18 changes: 17 additions & 1 deletion src/keboola_agent_cli/commands/flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -791,6 +791,10 @@ def flow_schedule(
created. If one already exists it is updated in-place — calling this
command a second time will not create duplicates.

The schedule is also activated on the Scheduler Service so the cron
trigger actually fires. If activation fails (e.g. the token cannot
manage schedules), the config stays written and a warning is shown.

\b
Examples:
# Run daily at 6am UTC
Expand Down Expand Up @@ -824,11 +828,18 @@ def flow_schedule(
else:
state_label = "[green]enabled[/green]" if enabled else "[yellow]disabled[/yellow]"
action = result.get("status", "created")
formatter.success(f"Schedule {action}: {escape(cron)} ({escape(timezone)}) — {state_label}")
activation = ""
if result.get("activated"):
activation = " and activated" if enabled else " and deactivated"
formatter.success(
f"Schedule {action}{activation}: {escape(cron)} ({escape(timezone)}) — {state_label}"
)
formatter.console.print(
f" Scheduler config: {escape(result.get('schedule_name', ''))} "
f"[dim](ID: {escape(result.get('schedule_id', ''))})[/dim]"
)
for warning in result.get("warnings", []):
formatter.warning(warning)


# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -860,6 +871,9 @@ def flow_schedule_remove(
) -> None:
"""Remove all schedules bound to a flow (deletes keboola.scheduler configs).

Each schedule is deregistered from the Scheduler Service before its
config is deleted, so the cron trigger stops firing.

Idempotent: safe to run even if no schedules exist.
"""
formatter = get_formatter(ctx)
Expand Down Expand Up @@ -946,3 +960,5 @@ def flow_schedule_remove(
formatter.console.print("[dim]No schedules found — nothing removed.[/dim]")
else:
formatter.success(f"Removed {count} schedule(s) from flow {escape(flow_id)}")
for warning in result.get("warnings", []):
formatter.warning(warning)
86 changes: 86 additions & 0 deletions src/keboola_agent_cli/scheduler_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""Keboola Scheduler Service API client with retry, timeouts, and token masking.

This module communicates with the Keboola Scheduler Service, which registers
cron triggers for ``keboola.scheduler`` Storage configurations. Writing the
Storage config alone is not enough -- the service must be told to (re)load it
via ``POST /schedules`` before the cron trigger fires. Derives the Scheduler
Service URL from the Storage API stack URL by replacing 'connection.' with
'scheduler.' in the hostname.

Inherits shared retry/error logic from BaseHttpClient.
"""

import logging
from typing import Any
from urllib.parse import quote

from .http_base import BaseHttpClient

logger = logging.getLogger(__name__)


class SchedulerClient(BaseHttpClient):
"""HTTP client for the Keboola Scheduler Service API.

Provides schedule activation (register/refresh a ``keboola.scheduler``
config with the service) and removal of the service-side registration,
with built-in retry logic (exponential backoff for 429/5xx), timeouts,
and automatic token masking in error messages.

Inherits _do_request() and _raise_api_error() from BaseHttpClient.
"""

def __init__(self, stack_url: str, token: str) -> None:
self._stack_url = stack_url.rstrip("/")
scheduler_base_url = self._derive_service_url(self._stack_url, "scheduler")
headers = {
"X-StorageApi-Token": token,
}
super().__init__(
base_url=scheduler_base_url,
token=token,
headers=headers,
)

def __enter__(self) -> "SchedulerClient":
return self

def __exit__(self, *args: Any) -> None:
self.close()

def activate_schedule(self, configuration_id: str) -> dict[str, Any]:
"""Register (or refresh) a schedule with the Scheduler Service.

The service loads the referenced config and applies its current
state; the operation is idempotent.

Args:
configuration_id: ID of the ``keboola.scheduler`` Storage
configuration (not the target flow ID).

Returns:
Dict with the registered schedule as returned by the service.

Raises:
KeboolaApiError: On API errors (403 when the token lacks the
schedule-activation privilege, etc.).
"""
payload = {"configurationId": configuration_id}
response = self._do_request("POST", "/schedules", json=payload)
return response.json()

def remove_schedule(self, configuration_id: str) -> None:
"""Deregister a schedule from the Scheduler Service.

The Storage configuration itself is untouched.

Args:
configuration_id: ID of the ``keboola.scheduler`` Storage
configuration.

Raises:
KeboolaApiError: On API errors (404 when the service has no
registration for this configuration, etc.).
"""
encoded_id = quote(configuration_id, safe="")
self._do_request("DELETE", f"/configurations/{encoded_id}")
Loading