Skip to content
Draft
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
21 changes: 21 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1015,6 +1015,27 @@ kbagent flow update --project NAME --flow-id ID [--name N] [--description D] [--
kbagent flow delete --project NAME --flow-id ID [--branch ID] [--yes]
kbagent flow schedule --project NAME --flow-id ID --cron "0 6 * * *" [--timezone TZ] [--disabled] [--branch ID]
kbagent flow schedule-remove --project NAME --flow-id ID [--branch ID] [--yes]
kbagent flow run --project NAME --flow-id ID [--from-phase PHASE_ID | --only-task TASK_ID ...] [--dry-run] [--wait] [--timeout N] [--branch ID] [--poll-strategy exponential|fixed] [--log-tail-lines N]
# flow run (#725): run a flow, optionally only PART of it. With neither selector it is the
# ordinary full run (same as `job run --component-id keboola.flow`). With one, kbagent sends
# `onlyFlowTaskIds` on POST /jobs -- a documented Queue API field the CLI never set, which is
# why partial runs were UI-only. --from-phase expands a phase id into its tasks plus every task
# in every phase reachable via next[].goto (disabled tasks in scope are skipped silently);
# --only-task takes explicit ids (an unknown OR disabled id is a hard error -- the caller named
# it). The two are mutually exclusive. --dry-run prints the resolved selection and creates NO
# job; it REQUIRES a selector (a full run has nothing to preview).
# CRITICAL SEMANTICS -- a selected run IGNORES the flow's phase conditions. The Queue daemon
# linearizes the phase graph into one sequential chain of synthetic unconditional `selected-run`
# transitions, phases kept only to route to a selected task run EMPTY, a failing task does NOT
# stop the chain, and the flow's final status is that of the LAST phase in the chain (so a run
# whose last phase succeeded reports `success` even though an earlier selected task failed).
# Unselected variable tasks do not run, so their flow variables are not merged. Use this to
# RE-RUN part of a flow -- never to verify that new next[].condition logic evaluates correctly;
# no API does the latter without a full run. Every payload carries `conditions_evaluated: false`.
# A rejected selector (unknown phase, unknown/disabled task, empty selection) exits 2, and an
# unreachable --from-phase is refused locally before a job is created. Result JSON gains
# `flow_task_selection` (ids + names + phases) so a caller keeping only the run payload still
# knows what ran. Over `serve`: POST /flows/{project}/{config_id}/run.
kbagent flow triggers --project NAME --flow-id ID [--branch ID]
# flow triggers (#714): the honest answer to "what starts this flow automatically". A flow has at
# least three trigger mechanisms and kbagent used to see ONE: `schedule list` / `search` only ever
Expand Down
5 changes: 3 additions & 2 deletions docs/web-server-endpoints.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ auth, and the concepts behind these routes live in
[`web-server.md`](web-server.md); a running server serves the same spec
interactively at `/docs` (Swagger) and `/openapi.json`.

**235 operations** across **205 paths** and **30 routers**.
**236 operations** across **206 paths** and **30 routers**.

Paths are shown as the server registers them. Reaching them through the
Node BFF (or single-process `--ui` mode) prefixes every path with `/api`.
Expand Down Expand Up @@ -251,7 +251,7 @@ Run components, inspect job history, terminate running jobs. Mirrors `kbagent jo
| `POST` | `/jobs/{project}/terminate` | Terminate running jobs |
| `GET` | `/jobs/{project}/{job_id}/stream` | Stream job status and logs (SSE) |

### `flows` (12 operations)
### `flows` (13 operations)

Orchestrator and Flow CRUD, scheduling, run history. Mirrors `kbagent flow *`.

Expand All @@ -267,6 +267,7 @@ Orchestrator and Flow CRUD, scheduling, run history. Mirrors `kbagent flow *`.
| `POST` | `/flows/{project}` | Create a new flow |
| `GET` | `/flows/{project}/{config_id}/schedules` | List schedules for a flow |
| `GET` | `/flows/{project}/{config_id}/triggers` | List every trigger kbagent can see |
| `POST` | `/flows/{project}/{config_id}/run` | Run a flow, optionally only part of it |
| `POST` | `/flows/{project}/{config_id}/schedule` | Set a cron schedule on a flow |
| `DELETE` | `/flows/{project}/{config_id}/schedule` | Remove a flow schedule |

Expand Down
12 changes: 12 additions & 0 deletions plugins/kbagent/agents/keboola-expert.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ been retired, so its absence is NOT a promise (see §1 Rule 6).
|---|---|---|---|
| Author / edit a conditional flow (keboola.flow) | `kbagent flow validate --file @flow.yaml --project P` (fetches the live schema; loop until clean) then `kbagent flow new` / `flow update --file` | fetch `flow detail`, merge phases/tasks locally, re-validate, push | integer ids (ids are STRINGS); `dependsOn` (use `next[].goto` + conditions); `--component-id` or `keboola.orchestrator` (both dropped 0.57.0); `flow schema --full` without `--project` |
| Schedule flow | `kbagent flow schedule --cron ... [--timezone]` -- confirm `activated: true` (older versions wrote a config whose cron NEVER fired) | -- | raw REST to `/storage/configurations/keboola.scheduler` |
| Re-run part of a flow / run a flow from a phase onward | `kbagent flow run --project P --flow-id ID --from-phase PHASE --dry-run` to preview, then without `--dry-run` (vNEXT+) | `--only-task ID` for an explicit task allowlist; a full `kbagent flow run` when the conditions themselves matter | claiming a partial run VERIFIES new `next[].condition` logic -- it does NOT, conditions are ignored; reading a partial run's `success` as "every selected task passed" (the chain does not stop on failure and the final status is the LAST phase's); `job run --component-id keboola.flow` when only one phase needs re-running |
| What starts this flow automatically / is this flow scheduled | `kbagent flow triggers --project P --flow-id ID` (0.92.0+) -- cron schedules AND table triggers in one call | `kbagent schedule list` for the cron half only | concluding "this flow has no trigger" from an empty result -- `cross_project_triggers_checked` is `false`, so empty means "no trigger that kbagent CHECKED"; reading `last_run: null` as "disabled" (it means never fired); expecting table triggers on a dev branch (the Storage route is production-only) |
| Who gets notified when a flow fails | `kbagent notification list [--component-id keboola.flow]` (0.86.0+) -- recipients live in a separate service, NOT the flow config | `flow detail` for in-flow `notification` TASKS | reading "nobody is notified" off a flow config, or off a filtered run (check `project_wide_excluded`); camelCase event names (they are kebab-case) |
| Add / remove / re-point a notification recipient | `kbagent notification create --event job-failed --channel email\|webhook --address A [--config-id K] [--branch ID]` / `notification delete --subscription-id ID` / `notification replace-recipient --subscription-id ID --address NEW` (0.91.0+) | -- | caching the old id after a replace (it is delete+recreate -- a NEW `subscription_id` is always minted; a failed delete leaves a duplicate as `old_deleted: false`); omitting `--branch` and expecting the UI's behavior (no `--branch` = NO `branch.id` filter = fires on every branch) |
Expand Down Expand Up @@ -247,6 +248,17 @@ its absence is NOT a promise the entry is version-independent (see §1 Rule 6).
as needing manual scheduling because of exactly this gap (#714). For the same
reason `job detail` on a flow job carries `trigger_hint`: a run with no
matching cron schedule is NOT proof of a manual run.
- **A partial `flow run` IGNORES the flow's conditions (vNEXT+)**:
`--from-phase` / `--only-task` send the Queue API's `onlyFlowTaskIds`, and the
daemon then linearizes the phase graph into an UNCONDITIONAL chain: no
`next[].condition` is evaluated, routing-only phases run empty, a failing
task does not stop the run, and the flow's final status is the LAST phase's
-- so a partial run can report `success` with a failed task inside it. Read
the per-task results, never the job status alone. It is the right tool to
RE-RUN part of a flow cheaply; it is the wrong tool to prove new condition
logic works (nothing short of a full run does that -- use a dev branch).
Preview any selection with `--dry-run` first: `--from-phase` expands to every
downstream phase's tasks, which is usually more than the caller pictured.
- **`flow schedule` activates on the Scheduler Service**: older
versions only wrote the `keboola.scheduler` config -- it showed `enabled` but
the cron NEVER fired. Confirm `activated: true`; `activated: false` + warning
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 @@ -225,6 +225,7 @@ When working inside a git repository or project directory, run `kbagent init` (o
| Bind a cron schedule to a flow (upsert: creates or updates) | `kbagent flow schedule --project PROJECT --flow-id FLOW-ID --cron CRON` |
| Remove all schedules bound to a flow (deletes keboola.scheduler configs) | `kbagent flow schedule-remove --project PROJECT --flow-id FLOW-ID` |
| Show every trigger kbagent can see for a flow (cron + table triggers) | `kbagent flow triggers --project PROJECT --flow-id FLOW-ID` |
| Run a flow, optionally only from a given phase onward | `kbagent flow run --project PROJECT --flow-id FLOW-ID` |
| List cron schedules (keboola.scheduler configs) across projects | `kbagent schedule list` |
| Show full detail for a single cron schedule | `kbagent schedule detail --project PROJECT --schedule-id SCHEDULE-ID` |
| Audit schedules by cron window or job-freshness | `kbagent schedule find` |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,7 @@ Requires the project to be added with its **master ('owner') Storage API token**
- `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. 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
- `flow run --project NAME --flow-id ID [--from-phase PHASE_ID | --only-task TASK_ID ...] [--dry-run] [--wait] [--timeout N] [--branch ID] [--poll-strategy exponential|fixed] [--log-tail-lines N]` -- run a flow, optionally only **part** of it (since vNEXT, #725). With neither selector this is the ordinary full run, identical to `job run --component-id keboola.flow`. With one, kbagent sends `onlyFlowTaskIds` on `POST /jobs` -- a documented Queue API field the CLI never set, which is why a partial run used to be UI-only. `--from-phase` expands a phase id into its own tasks **plus every task in every phase reachable via `next[].goto`** (disabled tasks in scope are skipped silently); `--only-task` takes explicit ids, where an unknown **or disabled** id is a hard error -- the caller named it. The two are mutually exclusive. `--dry-run` prints the resolved selection and creates **no** job; it requires a selector, since a full run has nothing to preview. **A selected run IGNORES the flow's phase conditions** -- see [gotchas.md](gotchas.md); every payload carries `conditions_evaluated: false`. A rejected selector exits 2, and an unreachable `--from-phase` is refused locally before any job is created. The run result gains `flow_task_selection` (ids + names + phases) so a caller keeping only the job payload still knows what ran
- `flow triggers --project NAME --flow-id ID [--branch ID]` -- **the honest answer to "what starts this flow automatically"** (since 0.92.0, #714). Returns `cron_schedules` (the `keboola.scheduler` configs `schedule list` already saw) **and** `table_triggers` (`GET /v2/storage/triggers` -- a separate Storage resource, NOT a component config, which is why no kbagent command saw them before). Cross-project triggers (a trigger-queue app config in ANOTHER project) are **not** covered: the result carries `cross_project_triggers_checked: false`, deliberately not an empty list -- an empty list reads as "checked, found none", the exact false negative this command exists to prevent. **Check that flag before concluding a flow has no trigger.** Table triggers are **production-only** (the Storage route is declared `isAvailableInBranch: false`), so `--branch` narrows the cron half only and `table_triggers_branch_scoped` is always `false`. A `last_run` of `null` means the trigger exists but has never fired -- not that it is disabled

## Schedule Discovery & Audit (Fleet-Wide)
Expand Down
46 changes: 46 additions & 0 deletions plugins/kbagent/skills/kbagent/references/gotchas.md
Original file line number Diff line number Diff line change
Expand Up @@ -4981,3 +4981,49 @@ single-project command:
project is not the first row of `project list`.
- The fix also covers `kbagent serve`: the `/kai/*` routes, `POST /documentation/query`,
and `GET /components?query=...` resolved the project the same wrong way.


## A partial `flow run` does not evaluate the flow's conditions

*(since vNEXT)* `kbagent flow run --from-phase` / `--only-task` sends the Queue
API's `onlyFlowTaskIds`, and the job-queue daemon treats that as a **task
selection**, not as "start the flow here". Per
`keboola/job-queue-daemon` `docs/flow-documentation.md`, such a run:

1. runs **only** the selected tasks -- every other task, notification and
variable task included, is left out entirely and does not appear in the
result;
2. **does not evaluate any condition.** The phase graph is linearized into one
sequential chain of synthetic unconditional transitions with id
`selected-run` (visible in the result's `phases[].conditions[]`);
3. traverses phases kept only to route to a selected task as **empty phases** --
they run an empty phase container and report `success` with no tasks;
4. **does not stop on a failing task.** The transitions are unconditional, so
the chain continues through a failed or warning phase;
5. reports as the flow's final status the status of the **last phase in the
chain**. Combined with (4), a selected run whose last phase succeeded
reports `success` even though an earlier selected task **failed** -- the
failure is visible in the task and phase entries, not in the job status.
**Never read a partial run's `success` as "everything in it passed"**; read
the per-task results;
6. does not merge flow variables defined by unselected variable tasks -- the
selected tasks fall back to their component variable configuration.

The consequence worth internalising: **a partial run cannot verify that new
`next[].condition` logic evaluates correctly.** That was the motivation in
issue #725, and no API satisfies it short of a full run -- the conditions are
precisely the thing a selected run throws away. Use `flow run` to *re-run* part
of a flow cheaply; to exercise a new condition, run the whole flow (a dev
branch keeps that cheap). Every payload from the command carries
`conditions_evaluated: false` so an agent cannot conclude otherwise from the
JSON alone.

kbagent pre-empts the daemon's own user-errors locally, before a job exists:
an unknown phase id (the message lists the valid ones), a `--from-phase` that
is not reachable from the flow's entry phase, an unknown or **disabled**
`--only-task` id, and a selection that resolves to nothing all exit **2**
without creating a job. A disabled task inside a `--from-phase` scope is
different: it is skipped silently and reported under
`skipped_disabled_task_ids`, because the caller selected a phase, not that
task.

13 changes: 13 additions & 0 deletions src/keboola_agent_cli/client/queue.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ def create_job(
mode: str = "run",
branch_id: int | None = None,
variable_values_id: str | None = None,
only_flow_task_ids: list[str] | None = None,
) -> dict[str, Any]:
"""Create and run a Queue API job.

Expand All @@ -142,6 +143,16 @@ def create_job(
``keboola.variables`` config. When set, the Queue API binds
the row's values to the job's `{{ variable }}` placeholders.
Omit for configurations that have no linked variables.
only_flow_task_ids: Optional ``keboola.flow`` task-id allowlist
(issue #725). Only meaningful for a flow job; the Queue API
then runs ONLY those tasks. Note the daemon **linearizes the
phase graph and ignores every configured condition** in such
a run -- a selected run is a partial re-execution, not a
rehearsal of the flow's ``next[].condition`` logic. Must be
non-empty when set (the API rejects an empty array), and the
deprecated ``onlyOrchestrationTaskIds`` alias is deliberately
not sent: the API rejects the two together and mirrors this
one back under both keys anyway.

Returns:
Job dict from the Queue API.
Expand All @@ -159,6 +170,8 @@ def create_job(
body["configRowIds"] = config_row_ids
if variable_values_id:
body["variableValuesId"] = variable_values_id
if only_flow_task_ids:
body["onlyFlowTaskIds"] = only_flow_task_ids
response = self._queue_request("POST", "/jobs", json=body)
return response.json()

Expand Down
Loading