Skip to content

UN-151 [FIX] Block workflow deletion at backend when in use by pipelines/APIs#1969

Open
pk-zipstack wants to merge 2 commits into
mainfrom
fix/un-151-backend-workflow-deletion-guard
Open

UN-151 [FIX] Block workflow deletion at backend when in use by pipelines/APIs#1969
pk-zipstack wants to merge 2 commits into
mainfrom
fix/un-151-backend-workflow-deletion-guard

Conversation

@pk-zipstack

@pk-zipstack pk-zipstack commented May 15, 2026

Copy link
Copy Markdown
Contributor

What

  • Adds a perform_destroy override on WorkflowViewSet that calls WorkflowHelper.can_update_workflow before deletion and raises a new WorkflowDeletionError (HTTP 400) with the same detailed message the UI shows when a workflow is in use.
  • Adds WorkflowHelper.build_workflow_in_use_message so the rejection detail lists the dependent pipelines and API deployments, matching the frontend format.

Why

  • The frontend (Workflows.jsx) already gates the delete button via the can_update endpoint and shows a descriptive error when a workflow is referenced by pipelines/API deployments (PR UN-3217 [FEAT] Show specific pipeline/API names in workflow deletion error message #1784).
  • However, the backend WorkflowViewSet.destroy had no equivalent guard. Because Pipeline.workflow and APIDeployment.workflow are declared with on_delete=models.CASCADE, a direct DELETE /workflow/<id>/ API call (or a UI bypass / race) would silently cascade-delete every dependent pipeline and API deployment. That was the original concern in UN-151.

How

  • New exception WorkflowDeletionError (status 400) in workflow_manager/workflow_v2/exceptions.py.
  • New static helper build_workflow_in_use_message(workflow_name, usage) in workflow_helper.py mirrors the frontend formatting (truncates after 3 names per category, appends ...and N more summary).
  • WorkflowViewSet.perform_destroy invokes can_update_workflow; on can_update=false it raises WorkflowDeletionError(detail=...). Otherwise it falls through to the default ModelViewSet destroy.

Can this PR break any existing features. If yes, please list possible items. If no, please explain why. (PS: Admins do not merge the PR without this section filled)

  • No. The UI flow already blocks deletion via can_update before issuing DELETE, so end users won't notice a change. The new guard only fires for direct API callers and any TOCTOU race between the UI check and the actual delete — both cases where the previous behavior was unsafe (silent cascade delete). Workflows with no pipeline/API references continue to delete as before.

Database Migrations

  • None.

Env Config

  • None.

Relevant Docs

  • None required.

Related Issues or PRs

Dependencies Versions

  • None.

Notes on Testing

  • Try DELETE /workflow/<id>/ for a workflow that is not referenced by any pipeline/API → should succeed (200).
  • Try DELETE /workflow/<id>/ for a workflow referenced by 1+ Pipelines and/or APIDeployments via curl or Postman → should return 400 with a body like:
    Cannot delete `<wf>` as it is used in:
    - `<api>` (API Deployment)
    - `<pipe>` (ETL Pipeline)
    
  • Verify dependent Pipeline / APIDeployment rows are intact after the rejection.
  • Confirm UI delete flow (which calls can_update first) continues to work unchanged.

Screenshots

Checklist

I have read and understood the Contribution Guidelines.

🤖 Generated with Claude Code

…nes/APIs

Adds a perform_destroy guard on WorkflowViewSet that calls
can_update_workflow and raises WorkflowDeletionError (400) with the same
detailed message used by the frontend. Prevents direct API callers from
silently cascade-deleting Pipelines/APIDeployments that reference the
workflow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 15, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8cad2327-a3f7-4605-acd1-60fde680eb5e

📥 Commits

Reviewing files that changed from the base of the PR and between d81d4a9 and 79b4565.

📒 Files selected for processing (1)
  • backend/workflow_manager/workflow_v2/views.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • backend/workflow_manager/workflow_v2/views.py

Summary by CodeRabbit

  • Improvements
    • Workflows that are currently in active use can’t be deleted, helping prevent unintended data loss.
    • When a deletion is blocked, the error message now includes a readable summary of which pipelines and APIs are using the workflow (including a limited preview with “and N more” when applicable).

Walkthrough

This PR blocks workflow deletion when the workflow is still in use, adds a dedicated API exception, and formats the usage details included in the delete error message.

Changes

Workflow deletion guard

Layer / File(s) Summary
Exception contract
backend/workflow_manager/workflow_v2/exceptions.py
WorkflowDeletionError adds a 400 APIException for blocked workflow deletion.
Usage message formatting
backend/workflow_manager/workflow_v2/workflow_helper.py
WorkflowHelper adds a display limit and a formatter for pipeline/API usage details, including truncated “more” lines and an empty-usage fallback.
Delete guard in ViewSet
backend/workflow_manager/workflow_v2/views.py
WorkflowViewSet.perform_destroy checks workflow usage and raises WorkflowDeletionError with the helper-generated message when deletion is blocked.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is specific and matches the main change: blocking backend workflow deletion when referenced by pipelines/APIs.
Description check ✅ Passed The description follows the template and fills the required sections with clear rationale, implementation, testing, and risk notes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/un-151-backend-workflow-deletion-guard

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sonarqubecloud

Copy link
Copy Markdown

@greptile-apps

greptile-apps Bot commented May 15, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds a backend guard to WorkflowViewSet.perform_destroy that calls can_update_workflow before allowing deletion, raising a new WorkflowDeletionError (HTTP 400) if any Pipeline or APIDeployment references the workflow. This closes the gap where a direct DELETE /workflow/<id>/ API call would silently cascade-delete every dependent pipeline and API deployment.

  • exceptions.py: New WorkflowDeletionError(APIException) with status 400 and a default detail string.
  • views.py: perform_destroy override calls can_update_workflow; raises WorkflowDeletionError with a descriptive message on failure, otherwise falls through to the default ModelViewSet destroy.
  • workflow_helper.py: New static build_workflow_in_use_message that mirrors the frontend's truncation format (up to 3 names per category, with an "...and N more" line).

Confidence Score: 5/5

Safe to merge — the guard only fires for direct API callers and TOCTOU races, and the cascade-delete path it closes is clearly unsafe.

The change is narrow and additive: it inserts a pre-delete check that either raises a 400 or falls through to the existing destroy path. Normal UI-driven deletes are unaffected. The truncation math in build_workflow_in_use_message is correct (remaining = total_count − shown_count uses the real DB aggregate). No migration, no schema change, no auth bypass.

No files require special attention.

Important Files Changed

Filename Overview
backend/workflow_manager/workflow_v2/exceptions.py Adds WorkflowDeletionError (HTTP 400); straightforward, no issues.
backend/workflow_manager/workflow_v2/views.py Adds perform_destroy guard; logic is correct, but can_update_workflow re-fetches the workflow from DB even though DRF already loaded the instance (noted in a previous thread).
backend/workflow_manager/workflow_v2/workflow_helper.py Adds build_workflow_in_use_message; truncation math is correct (remaining = total_count - shown_count uses real DB count). Raw pipeline_type key and unreachable zero-count branch were flagged in prior threads.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Client
    participant WorkflowViewSet
    participant WorkflowHelper
    participant DB

    Client->>WorkflowViewSet: "DELETE /workflow/<id>/"
    WorkflowViewSet->>WorkflowViewSet: get_object() → instance
    WorkflowViewSet->>WorkflowHelper: perform_destroy(instance)
    WorkflowHelper->>DB: can_update_workflow(id)
    DB-->>WorkflowHelper: workflow
    WorkflowHelper->>DB: "Pipeline.objects.filter(workflow=wf).count()"
    WorkflowHelper->>DB: "APIDeployment.objects.filter(workflow=wf).count()"
    alt "pipeline_count + api_count > 0"
        WorkflowHelper-->>WorkflowViewSet: "{can_update: false, pipelines: [...], api_names: [...]}"
        WorkflowViewSet->>WorkflowHelper: build_workflow_in_use_message(name, usage)
        WorkflowHelper-->>WorkflowViewSet: detailed message string
        WorkflowViewSet-->>Client: HTTP 400 WorkflowDeletionError
    else no dependents
        WorkflowHelper-->>WorkflowViewSet: "{can_update: true}"
        WorkflowViewSet->>DB: super().perform_destroy(instance)
        DB-->>WorkflowViewSet: deleted
        WorkflowViewSet-->>Client: HTTP 204 No Content
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Client
    participant WorkflowViewSet
    participant WorkflowHelper
    participant DB

    Client->>WorkflowViewSet: "DELETE /workflow/<id>/"
    WorkflowViewSet->>WorkflowViewSet: get_object() → instance
    WorkflowViewSet->>WorkflowHelper: perform_destroy(instance)
    WorkflowHelper->>DB: can_update_workflow(id)
    DB-->>WorkflowHelper: workflow
    WorkflowHelper->>DB: "Pipeline.objects.filter(workflow=wf).count()"
    WorkflowHelper->>DB: "APIDeployment.objects.filter(workflow=wf).count()"
    alt "pipeline_count + api_count > 0"
        WorkflowHelper-->>WorkflowViewSet: "{can_update: false, pipelines: [...], api_names: [...]}"
        WorkflowViewSet->>WorkflowHelper: build_workflow_in_use_message(name, usage)
        WorkflowHelper-->>WorkflowViewSet: detailed message string
        WorkflowViewSet-->>Client: HTTP 400 WorkflowDeletionError
    else no dependents
        WorkflowHelper-->>WorkflowViewSet: "{can_update: true}"
        WorkflowViewSet->>DB: super().perform_destroy(instance)
        DB-->>WorkflowViewSet: deleted
        WorkflowViewSet-->>Client: HTTP 204 No Content
    end
Loading

Reviews (2): Last reviewed commit: "Merge branch 'main' into fix/un-151-back..." | Re-trigger Greptile

Comment thread backend/workflow_manager/workflow_v2/workflow_helper.py
Comment thread backend/workflow_manager/workflow_v2/views.py
Comment thread backend/workflow_manager/workflow_v2/workflow_helper.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
backend/workflow_manager/workflow_v2/workflow_helper.py (1)

996-1033: ⚡ Quick win

Add defensive handling for None or empty pipeline fields.

If pipeline_name or pipeline_type is None or an empty string, the formatted message will display None or empty backticks, which degrades the user experience.

🛡️ Proposed fix to handle None/empty values gracefully
         if pipelines:
             shown = list(pipelines)[:limit]
             for p in shown:
-                name = p.get("pipeline_name")
-                p_type = p.get("pipeline_type")
+                name = p.get("pipeline_name") or "Unknown"
+                p_type = p.get("pipeline_type") or "Unknown"
                 lines.append(f"- `{name}` ({p_type} Pipeline)")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/workflow_manager/workflow_v2/workflow_helper.py` around lines 996 -
1033, The build_workflow_in_use_message function currently inserts raw
pipeline_name/pipeline_type (and api_names entries) into the user message, which
can render "None" or empty backticks; update build_workflow_in_use_message to
defensively coerce missing or empty values to a friendly placeholder (e.g.
"<unknown>" or "unnamed") before formatting: for pipelines iterate over p in
shown and compute name = (p.get("pipeline_name") or "<unknown>").strip() and
p_type = (p.get("pipeline_type") or "Pipeline").strip() (or similar), and for
api_names map any None/empty to a placeholder before adding backticked entries;
keep usage of WorkflowHelper.USAGE_MESSAGE_DISPLAY_LIMIT and the existing usage
dict keys (pipelines, api_names, pipeline_count, api_count) unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@backend/workflow_manager/workflow_v2/workflow_helper.py`:
- Around line 996-1033: The build_workflow_in_use_message function currently
inserts raw pipeline_name/pipeline_type (and api_names entries) into the user
message, which can render "None" or empty backticks; update
build_workflow_in_use_message to defensively coerce missing or empty values to a
friendly placeholder (e.g. "<unknown>" or "unnamed") before formatting: for
pipelines iterate over p in shown and compute name = (p.get("pipeline_name") or
"<unknown>").strip() and p_type = (p.get("pipeline_type") or "Pipeline").strip()
(or similar), and for api_names map any None/empty to a placeholder before
adding backticked entries; keep usage of
WorkflowHelper.USAGE_MESSAGE_DISPLAY_LIMIT and the existing usage dict keys
(pipelines, api_names, pipeline_count, api_count) unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9778eca5-e3ea-414b-b814-ab21fe1bdaa5

📥 Commits

Reviewing files that changed from the base of the PR and between 993f676 and d81d4a9.

📒 Files selected for processing (3)
  • backend/workflow_manager/workflow_v2/exceptions.py
  • backend/workflow_manager/workflow_v2/views.py
  • backend/workflow_manager/workflow_v2/workflow_helper.py

@sonarqubecloud

sonarqubecloud Bot commented Jul 7, 2026

Copy link
Copy Markdown

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Unstract test results

Per-group results

Status Group Tier Passed Failed Errors Skipped Duration (s)
unit-connectors unit 64 12 0 3 16.6
unit-core unit 0 0 4 0 1.1
unit-platform-service unit 9 0 1 0 1.2
unit-rig unit 53 0 0 0 3.1
unit-runner unit 11 0 0 0 3.0
unit-sdk1 unit 381 0 0 0 18.7
unit-tool-registry unit 0 0 1 0 1.2
unit-workers unit 0 0 0 0 16.7
TOTAL 518 12 6 3 61.6

Critical paths

⚠️ Critical paths not yet covered

  • auth-login — User can log in and obtain a session cookie. (entry: POST /api/v1/auth/login; declared coverage: no groups declared)
  • adapter-register-llm — Register and validate an LLM adapter. (entry: POST /api/v1/adapter/; declared coverage: no groups declared)
  • workflow-create-execute — Create a workflow, configure source+destination, execute, poll, fetch result. (entry: POST /api/v1/workflow/{id}/execute/; declared coverage: e2e-workflow)
  • api-deployment-run — Deploy a workflow as an API, POST a document, receive structured JSON. (entry: POST /deployment/api/{org}/{name}/; declared coverage: e2e-api-deployment)
  • prompt-studio-fetch-response — Prompt Studio: create project, add prompt, run single-pass, get response. (entry: POST /api/v1/prompt-studio/prompt-studio-tool/{id}/fetch_response/; declared coverage: e2e-prompt-studio)
  • pipeline-etl-execute — Run an ETL pipeline from source connector to destination. (entry: POST /api/v1/pipeline/{id}/execute/; declared coverage: no groups declared)
  • usage-token-tracking — Per-execution token usage is recorded and retrievable. (entry: GET /api/v1/usage/get_token_usage/; declared coverage: no groups declared)
  • workflow-execution-fan-out — Multi-file workflow execution fans out to file-processing workers and rejoins. (entry: internal: backend → rabbitmq → workers/file_processing; declared coverage: no groups declared)
  • callback-result-delivery — Async results are posted back via the callback worker. (entry: internal: workers/callback → backend /internal endpoints; declared coverage: no groups declared)
✅ Covered critical paths
  • tool-sandbox-exec — covered by unit-runner

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants