Skip to content

kbagent notification: fleet-wide audit of Flow Notification subscriptions (Notification Service API) #600

Description

@MichalProchazkaP3

Problem

Auditing a Keboola org's Flows for "who gets notified when something breaks" is only half-possible today.

kbagent flow list / config detail --component-id keboola.flow fully expose:

  • flow descriptions
  • in-flow Notification tasks (a task node of type: "notification" with recipients: [{channel, address}], added inside a phase)

They do not expose the Flow Builder's own Notifications tab — the per-flow Success / Error / Processing-delay / Warning cards with their own recipient lists, reachable via the bell icon in the UI. That's a genuinely different mechanism: it's backed by a separate Notification service, not stored inside the flow's configuration JSON.

I ran a fleet-wide audit for a customer (20 projects, 276 keboola.flow + keboola.orchestrator configs) checking every email address referenced anywhere in flow descriptions, in-flow notification tasks, and email-sending component configs (kds-team.app-email-smtp-sender, kds-team.app-mailgun-v2) — and found real issues (placeholder addresses, typo'd recipients, missing owners). But the Notifications-tab recipients — arguably the most important ones, since they're what actually pages someone when a production flow fails — were completely unreachable from kbagent, the MCP server, or any CLI. The only fallback was "open each flow in the UI by hand."

What exists today (confirmed via public discovery, no auth needed)

  • GET https://connection.<region>.keboola.com/v2/storage lists platform services, including:
    {"id": "notification", "url": "https://notification.eu-central-1.keboola.com"}
  • Its swagger is public: https://notification.eu-central-1.keboola.com/docs/swagger.yaml. Relevant paths:
    • GET /project-subscriptions — list all subscriptions for the project resolved from the auth token. Optional ?event= filter.
    • POST /project-subscriptions / DELETE /project-subscriptions/{id} — create/remove.
    • Auth: security: [StorageApiTokenAuth, StorageApiBearerToken, ...]a plain Storage API token, the same kind every KeboolaClient in kbagent already holds per project. No elevated scope (e.g. canManageTokens) needed for the read path — I originally assumed it would and burned a while confirming otherwise.
    • Schema: event (jobFailed, jobSucceeded, jobSucceededWithWarning, jobProcessingLong, plus phaseJob* variants), filters (field/value/operator, e.g. matching configurationId/component), recipient (channel: email|webhook + address), optional expiresAt.

So the missing piece is purely a client + CLI surface in kbagent — the API itself is a normal sibling service exactly like Queue/Encryption/Sync-Actions, which kbagent already talks to.

Proposed solution

Follow the existing queue / encryption / sync-actions pattern in client/_core.py + the existing schedule command as the structural template (it already solves the identical shape of problem — "audit a per-project sibling-service concept across every registered project, joined against keboola.flow/keboola.orchestrator config names").

  1. client/_core.py: add

    @property
    def _notification_base_url(self) -> str:
        return self._derive_service_url(self._stack_url, "notification")
    
    def _notification_request(self, method: str, path: str, **kwargs: Any) -> httpx.Response:
        client = self._get_or_create_sub_client("_notification_client", self._notification_base_url)
        return self._do_request(method, path, client=client, base_url=self._notification_base_url, **kwargs)

    plus the matching self._notification_client: httpx.Client | None = None init and close() wiring — copy-paste of the _queue_* / _encrypt_* trio.

  2. client/notifications.py (new mixin, modeled on client/queue.py):

    class _NotificationMixin(_CoreClient):
        def list_project_subscriptions(self, event: str | None = None) -> list[dict[str, Any]]:
            params = {"event": event} if event else {}
            return self._notification_request("GET", "/project-subscriptions", params=params).json()

    (Write methods — create_project_subscription / delete_project_subscription — are trivial to add too, but out of scope for this ask; read-only is enough to close the audit gap.)

  3. services/notification_service.py (new, modeled on services/schedule_service.py::ScheduleService):

    • list_subscriptions(aliases: list[str] | None, event: str | None, component_id: str | None, config_id: str | None) -> dict — fans out self._run_parallel over self.resolve_projects(aliases), calls list_project_subscriptions(event) per project.
    • Joins each subscription's filters (configurationId / component) against list_components_with_configs() from the same project — exactly the _partition_components trick ScheduleService uses to resolve parent_name — so the output row can show which flow a subscription belongs to, not just a bare configurationId.
    • Same error-accumulation contract as ScheduleService: per-project failures land in errors, never abort the whole fan-out.
  4. commands/notification.py (new, modeled on commands/schedule.py):

    kbagent notification list [--project ALIAS...] [--event jobFailed|jobSucceeded|jobSucceededWithWarning|jobProcessingLong] [--component-id keboola.flow] [--config-id ID]
    

    Output columns: project_alias, subscription_id, event, flow_name (resolved), component_id/config_id, channel, address, expires_at. Register the command group next to schedule_app in the root Typer app and in check_cli_permission the same way (read-only, safe under --deny-writes).

  5. Docs: mention notification list from flow list's docstring as the companion command for "what does the Notifications tab say", the way flow list already flags the legacy_orchestrator_count caveat for orchestrator flows. Add a changelog entry.

Open questions / things to verify while implementing

  • Whether subscriptions are branch-aware (dev branch flows). The swagger doesn't show a branch filter — worth checking against a real dev-branch flow before assuming production-only scope, similar to the existing branch caveat on schedule find --not-run-since (Queue API isn't branch-aware either).
  • Whether filters always contains a configurationId for flow-level subscriptions, or whether some events are project-wide with no filters at all (e.g. a catch-all "notify me on any job failure" subscription) — the join logic needs to degrade gracefully to "project-wide, no specific flow" rather than erroring.

Non-goals for this issue

  • Write path (create/delete subscriptions) — natural follow-up, not needed to close the audit gap.
  • Anything about the in-flow type: "notification" task (already fully supported today via flow detail/config detail) — this issue is only about the Notifications-tab / Notification-service side.

Why it matters

This was the one blind spot left after an otherwise complete fleet-wide "are our flow notification emails valid" audit across 20 projects — every other notification surface (owner emails in descriptions, in-flow notification tasks, email-sending component recipients) was fully auditable via existing kbagent commands in a few minutes; this one required opening every flow by hand in the UI. Given kbagent's stated goal of wrapping "everything ... into workflow-oriented commands where dev branches propagate automatically, multi-project operations run in parallel" — this is one of the last multi-project operations that still can't be done from the CLI.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions