Skip to content

feat: workflow persistence closed loop, Dify/n8n import, recursive packages - #12

Closed
2233admin wants to merge 2 commits into
mainfrom
feat/workflow-persistence-closed-loop
Closed

feat: workflow persistence closed loop, Dify/n8n import, recursive packages#12
2233admin wants to merge 2 commits into
mainfrom
feat/workflow-persistence-closed-loop

Conversation

@2233admin

Copy link
Copy Markdown
Owner

Closes progress on #11.

Summary

  • Workspace -> Project -> WorkflowDraft -> immutable WorkflowVersion persistence, with revision-conflict (409) and single-use validation-run (409) guards.
  • Dify/n8n import (connections-dict flattening for n8n, provenance tagging) gated behind a Validation Run before publish.
  • Recursive Package compiler: scoped node ids (a::b::c), 16-layer nesting limit, per-level cycle detection. Depth-1 output unchanged (regression tested).
  • Compatibility tracer + fixture/passthrough validation execution, reusing existing WorkflowRun/event infra.
  • WorkspaceSettings model + migration + CRUD API.

Backend/migration/test only — no frontend changes.

Known limitations (see #11 for full writeup)

  • Compatibility runtime is simulated/fixture-driven, not real Dify/n8n worker dispatch.
  • Importer-tagged n8n nodes classify as node_library origin, not n8n (catalogId precedence) — n8n branch is unit-tested but not exercised via the importer's current outputs.
  • Nested packages below depth 1 don't emit tracer lifecycle events.

Test plan

  • uv run pytest -m "not live" -q — 1636 passed, 5 failed (pre-existing Windows/GBK locale codec issue, unrelated to this diff), 1 skipped
  • uv run pytest tests/integration/test_workflow_authoring_api.py -v --no-cov — 6 passed
  • uv run pytest tests/integration/test_workflow_external_import_api.py -v — 3 passed
  • uv run alembic upgrade head against scratch SQLite — applies cleanly on top of m2n3o4p5q6r7

…ckages

Workspace -> Project -> WorkflowDraft -> WorkflowVersion persistence with
revision-conflict and single-use validation-run guards; Dify/n8n import
with a Validation Run publish gate; recursive Package compilation with
scoped node ids and a 16-layer nesting limit; WorkspaceSettings model.

Compatibility runtime is simulated/fixture-driven validation, not real
Dify/n8n worker dispatch.
@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown

Review Change Stack

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: 7c4d1606-f4b8-4921-8f15-be60f35e383e

📥 Commits

Reviewing files that changed from the base of the PR and between c326a2a and c69cbcb.

📒 Files selected for processing (3)
  • frontend/components/studio/workflow-lifecycle-strip.logic.ts
  • frontend/components/studio/workflow-lifecycle-strip.tsx
  • frontend/scripts/test-workflow-lifecycle-strip.mjs

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Added workspace/project workflow authoring with draft validation runs and publish to version history.
    • Added nested package compilation support with enforced depth limits.
    • Extended external workflow import to support n8n and Dify runtimes.
    • Added Workflow lifecycle strip UI to reflect workflow states and blocker messaging.
  • Bug Fixes
    • Improved handling for stale draft revisions, validation-run reuse, invalid snapshots, and publish conflict scenarios.

Walkthrough

Adds workflow authoring storage, services, APIs, validation and publishing flows; supports recursive package compilation with depth limits; extends external workflow imports for Dify and n8n; and adds a workflow lifecycle strip component with regression coverage.

Changes

Workflow authoring lifecycle

Layer / File(s) Summary
Authoring contracts and persistence
backend/schemas/workflow_authoring.py, backend/models/*, backend/migrations/versions/...
Adds workspace, project, draft, validation-run, and workflow-version schemas, models, relationships, constraints, and migration tables.
Authoring and validation services
backend/services/workflow_authoring_service.py, backend/services/validation_run_service.py, backend/workflow/conformance/contracts.py
Implements CRUD operations, optimistic draft revisions, validation execution, runtime passports, validation gating, and single-use publishing.
Authoring API integration
backend/api/v1/*, tests/integration/test_workflow_authoring_api.py
Mounts workspace routes, maps domain failures to HTTP responses, and covers lifecycle and conflict cases.

Nested package compilation

Layer / File(s) Summary
Recursive package expansion and validation
backend/workflow/compiler.py
Adds scoped recursive package expansion, contextual validation paths, cycle handling, and a maximum nesting depth.
Nested compilation integration coverage
tests/integration/test_workflow_compile_api.py
Tests nested expansion, dependency wiring, maximum-depth compilation, and depth-limit errors.

External runtime import

Layer / File(s) Summary
Runtime-aware import behavior
backend/schemas/workflow.py, backend/workflow/external_importer.py
Adds Dify and n8n runtime support, n8n connection extraction, runtime-specific IDs, and provenance metadata.
External import integration coverage
tests/integration/test_workflow_external_import_api.py
Tests Dify and n8n imports and origin classification behavior.

Workflow lifecycle strip

Layer / File(s) Summary
Lifecycle view model and rendering
frontend/components/studio/workflow-lifecycle-strip.*
Defines lifecycle stage views and renders stage statuses, notes, badges, separators, and blocker alerts.
Lifecycle regression coverage
frontend/scripts/test-workflow-lifecycle-strip.mjs
Checks lifecycle labels, stage ordering, activation notes, and conditional blocker text.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant WorkspacesAPI
  participant ValidationRunService
  participant WorkflowCompiler
  participant Database
  Client->>WorkspacesAPI: create validation run
  WorkspacesAPI->>ValidationRunService: run_validation(draft)
  ValidationRunService->>WorkflowCompiler: compile workflow snapshot
  ValidationRunService->>Database: store validation result
  Client->>WorkspacesAPI: publish draft
  WorkspacesAPI->>ValidationRunService: publish_draft(validation_run_id)
  ValidationRunService->>Database: insert workflow version
  Database-->>Client: published version response
Loading

Poem

I’m a bunny with a blueprint bright,
Drafts hop through validation light.
Packages nest, then IDs align,
n8n trails leave tags behind.
Lifecycle stages march in line—
Workflow carrots, stacked just fine!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.80% 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 matches the main changes: workflow persistence, Dify/n8n import, and recursive package support.
Description check ✅ Passed The description is clearly related to the PR and summarizes the implemented backend, migration, and test changes.
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
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch feat/workflow-persistence-closed-loop

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.

@repowise-bot

repowise-bot Bot commented Jul 12, 2026

Copy link
Copy Markdown

✅ Health: 8.7

📋 At a glance
8 new findings introduced · 5 co-change pairs left out. Scoped to backend.

🚨 Change risk: 9.7/10 (high)
This change's risk is driven by:

  • more lines added than baseline
  • more scattered than baseline
🔎 More signals (1)

🔗 Hidden coupling (2 files)

  • backend/models/__init__.py co-changes with these files (not in this PR):
    • .../api/endpoints.ts (6× — 🟢 routine)
    • .../api/types.ts (6× — 🟢 routine)
  • .../v1/__init__.py co-changes with these files (not in this PR):
    • frontend/src/App.tsx (6× — 🟢 routine)
    • .../api/endpoints.ts (6× — 🟢 routine)
    • .../api/types.ts (6× — 🟢 routine)

📊 Full report · ⭐ Star Repowise · 📥 Install bot · Last updated 2026-07-16 04:10 UTC
Silence on a single PR with [skip repowise] in the title · Per-repo toggle on repowise.dev/settings?tab=bot

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a comprehensive workflow authoring closed loop, adding models, schemas, database migrations, and API endpoints for Workspaces, Projects, WorkflowDrafts, WorkflowVersions, and ValidationRuns. It also extends the compiler to support nested package internals up to a maximum depth of 16 and adds support for importing external workflows from Dify and n8n. The review feedback suggests several robustness improvements: adding regex pattern validation to ensure slugs are URL-safe, validating timezones using zoneinfo in the workspace settings schema, and defensively checking for missing projection data during validation runs to prevent potential crashes.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.


class WorkspaceCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=255)
slug: str = Field(..., min_length=1, max_length=255)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Add a regex pattern validation to the slug field to ensure it only contains URL-safe characters (alphanumeric, hyphens, and underscores).

Suggested change
slug: str = Field(..., min_length=1, max_length=255)
slug: str = Field(..., min_length=1, max_length=255, pattern=r"^[a-z0-9-_]+$")


class ProjectCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=255)
slug: str = Field(..., min_length=1, max_length=255)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Add a regex pattern validation to the slug field to ensure it only contains URL-safe characters (alphanumeric, hyphens, and underscores).

Suggested change
slug: str = Field(..., min_length=1, max_length=255)
slug: str = Field(..., min_length=1, max_length=255, pattern=r"^[a-z0-9-_]+$")

Comment on lines +37 to +40
class WorkspaceSettingsUpdate(BaseModel):
timezone: Optional[str] = None
deterministic_simulation: Optional[bool] = None
max_items_per_run: Optional[int] = Field(None, gt=0)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Validate that the provided timezone is a valid timezone name using zoneinfo from the standard library to prevent runtime errors during scheduling or execution. Note that you will need to import field_validator from pydantic at the top of the file.

Suggested change
class WorkspaceSettingsUpdate(BaseModel):
timezone: Optional[str] = None
deterministic_simulation: Optional[bool] = None
max_items_per_run: Optional[int] = Field(None, gt=0)
class WorkspaceSettingsUpdate(BaseModel):
timezone: Optional[str] = None
deterministic_simulation: Optional[bool] = None
max_items_per_run: Optional[int] = Field(None, gt=0)
@field_validator("timezone")
@classmethod
def validate_timezone(cls, v: Optional[str]) -> Optional[str]:
if v is not None:
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
try:
ZoneInfo(v)
except ZoneInfoNotFoundError:
raise ValueError(f"Invalid timezone: {v}")
return v

Comment on lines +80 to +83
projection = await start_workflow_run(
WorkflowRunStartRequest(project=project), session=session
)
validation_run.run_id = projection.runId

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Enforce defensive programming by checking if projection is None or if projection.runId is missing before accessing its attributes. This prevents potential AttributeError crashes if the workflow run fails to start.

    projection = await start_workflow_run(
        WorkflowRunStartRequest(project=project), session=session
    )
    if not projection or not projection.runId:
        validation_run.status = "failed"
        validation_run.failure_reason = "execution_failed"
        await session.flush()
        await session.refresh(validation_run)
        return validation_run

    validation_run.run_id = projection.runId

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/api/v1/workspaces.py (1)

1-283: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Add PUT /workspaces/{workspace_id}WorkspaceUpdate and authoring_service.update_workspace() exist, but this router never exposes a workspace update endpoint. If workspace edits are supported, wire this route to the service; otherwise remove the unused schema/service to avoid a misleading API contract.

🤖 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/api/v1/workspaces.py` around lines 1 - 283, Add a PUT
/workspaces/{workspace_id} handler alongside create_workspace and get_workspace,
accepting schemas.WorkspaceUpdate, loading the workspace via
authoring_service.get_workspace, returning 404 when absent, applying
authoring_service.update_workspace, committing and refreshing it, and returning
WorkspaceRead. If workspace updates are intentionally unsupported instead,
remove the unused WorkspaceUpdate schema and authoring_service.update_workspace
implementation.
🧹 Nitpick comments (4)
backend/workflow/compiler.py (1)

513-618: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Move the nesting guard into _expand_package_internals
_validate_package_internals() enforces MAX_PACKAGE_NESTING_DEPTH before compile_workflow_project() reaches this helper, so the recursion is safe here. Add the same check locally if this helper may be reused from another entrypoint.

🤖 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/compiler.py` around lines 513 - 618, Add a local
MAX_PACKAGE_NESTING_DEPTH guard at the start of _expand_package_internals before
recursing into nested internals, so the helper remains safe when called
independently of _validate_package_internals. Preserve the existing expansion
behavior within the allowed depth and stop or reject deeper nesting using the
helper’s established contract.
backend/schemas/workflow_authoring.py (3)

15-18: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add slug format validation.

WorkspaceCreate.slug and ProjectCreate.slug accept any string up to 255 chars with no format constraint. Since slugs are typically used in URLs/lookups, unconstrained values (spaces, uppercase, special chars) can produce malformed routes or inconsistent lookups.

♻️ Suggested constraint
+from pydantic import field_validator
+import re
+
+_SLUG_RE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
+
 class WorkspaceCreate(BaseModel):
     name: str = Field(..., min_length=1, max_length=255)
-    slug: str = Field(..., min_length=1, max_length=255)
+    slug: str = Field(..., min_length=1, max_length=255, pattern=_SLUG_RE.pattern)
     description: Optional[str] = None

Apply similarly to ProjectCreate.slug.

Also applies to: 55-57

🤖 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/schemas/workflow_authoring.py` around lines 15 - 18, Update the slug
fields in WorkspaceCreate and ProjectCreate to enforce a URL-safe slug format,
allowing only lowercase letters, numbers, and single hyphen separators while
preserving the existing length limits and required validation.

34-34: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider ConfigDict instead of a plain dict for model_config.

Ruff's RUF012 flags these six model_config = {"from_attributes": True} lines as mutable class defaults. This is a well-known pydantic-v2 pattern (the value is popped by the model metaclass, not a shared instance default), but using ConfigDict(from_attributes=True) is the idiomatic pydantic v2 form and silences the lint warning with typed config.
[dependency_check]

  • Flagging: pydantic v2 model_config convention vs Ruff RUF012.
  • Action: appending since this concerns library/linter interaction.

Also applies to: 52-52, 68-68, 90-90, 114-114, 133-133

🤖 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/schemas/workflow_authoring.py` at line 34, Replace the plain
dictionary assigned to each of the six model_config class attributes with the
typed pydantic v2 ConfigDict form, preserving from_attributes=True. Update the
relevant schema definitions in workflow_authoring.py and add or reuse the
appropriate ConfigDict import.

Source: Linters/SAST tools


37-53: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Timezone validation gap and casing inconsistency with WorkflowSettings.

WorkspaceSettingsUpdate.timezone/WorkspaceSettingsRead.timezone accept arbitrary strings with no validation against real IANA timezones (e.g. via zoneinfo.available_timezones()), risking bad data reaching scheduling logic downstream.

Separately, these fields use snake_case (deterministic_simulation, max_items_per_run) while the semantically equivalent WorkflowSettings embedded in WorkflowProject.settings uses camelCase (deterministicSimulation, maxItemsPerRun) per backend/schemas/workflow.py:185-202. Since WorkflowDraftRead.snapshot (camelCase settings) and WorkspaceSettingsRead (snake_case) can appear side-by-side in the same API surface, this inconsistency is confusing for API consumers.

🤖 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/schemas/workflow_authoring.py` around lines 37 - 53, Update
WorkspaceSettingsUpdate and WorkspaceSettingsRead to validate timezone values
against real IANA timezones, reusing the project’s established
timezone-validation approach or zoneinfo.available_timezones(). Align
deterministic_simulation and max_items_per_run with WorkflowSettings by exposing
the camelCase names deterministicSimulation and maxItemsPerRun while preserving
their existing optionality, defaults, and positive-value validation.
🤖 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.

Inline comments:
In `@backend/api/v1/workspaces.py`:
- Around line 55-83: Update update_workspace_settings to catch IntegrityError
around db.commit(), roll back the transaction on failure, and raise
HTTPException with status 422 consistent with other create/update paths; keep
the successful refresh and response flow unchanged.

In `@backend/models/validation_run.py`:
- Around line 1-30: Add a TYPE_CHECKING-only import for WorkflowVersion in
validation_run.py so the forward reference on ValidationRun.version resolves for
static analysis without introducing a runtime import cycle.

In `@backend/models/workflow_authoring.py`:
- Around line 95-122: Add a TYPE_CHECKING-guarded import for ValidationRun in
the module containing WorkflowVersion, keeping it excluded at runtime while
satisfying the forward reference used by validation_run:
Mapped["ValidationRun"].

In `@backend/schemas/workflow.py`:
- Line 242: Update the runtime union used by importExternalRuntimeWorkflow in
frontend/lib/workflow/backend-import.ts to include "dify" and "n8n" alongside
the existing "langgraph" and "langchain" values, keeping it synchronized with
ExternalWorkflowRuntime.

In `@backend/services/validation_run_service.py`:
- Around line 80-86: Update the validation-run workflow in the surrounding
service method to catch failures from start_workflow_run and
list_workflow_run_events, persist the ValidationRun as failed with an
appropriate failure_reason, and return the failed result instead of allowing the
exception to become a 500. Preserve the existing successful event-processing
path and ensure the failure update is committed before returning.
- Around line 160-178: Update publish_draft’s version allocation around the
WorkflowVersion query so concurrent publishes for the same project are
serialized before calculating MAX(version_number)+1. Lock the associated project
row, or use an equivalent atomic allocation mechanism, then insert the
WorkflowVersion while preserving the existing unique constraint and return flow.

In `@backend/services/workflow_authoring_service.py`:
- Around line 122-134: The update_draft function currently performs a non-atomic
Python-side revision check, allowing concurrent updates to overwrite each other.
Make the revision guard part of the database update or configure SQLAlchemy
optimistic locking for WorkflowDraft, ensuring stale writes fail with
DraftRevisionConflictError and only the row matching data.expected_revision is
updated.
- Around line 47-75: Update update_workspace and update_workspace_settings to
exclude or reject client-supplied None values before applying fields, while
preserving valid non-null updates. Ensure Workspace.name and non-null
WorkspaceSettings fields cannot be assigned None before session.flush().

In `@backend/workflow/external_importer.py`:
- Around line 234-269: Preserve n8n branch provenance by adding each
connection’s output-slot index to the edge payload produced by
_extract_n8n_connection_edges(), then update import_external_workflow() and the
ui.externalWorkflow conversion to retain and use both sourcePort and the branch
index instead of rebuilding edges from node names alone. Ensure IF/Switch true
and false branches remain distinguishable through import and round-trip export.

---

Outside diff comments:
In `@backend/api/v1/workspaces.py`:
- Around line 1-283: Add a PUT /workspaces/{workspace_id} handler alongside
create_workspace and get_workspace, accepting schemas.WorkspaceUpdate, loading
the workspace via authoring_service.get_workspace, returning 404 when absent,
applying authoring_service.update_workspace, committing and refreshing it, and
returning WorkspaceRead. If workspace updates are intentionally unsupported
instead, remove the unused WorkspaceUpdate schema and
authoring_service.update_workspace implementation.

---

Nitpick comments:
In `@backend/schemas/workflow_authoring.py`:
- Around line 15-18: Update the slug fields in WorkspaceCreate and ProjectCreate
to enforce a URL-safe slug format, allowing only lowercase letters, numbers, and
single hyphen separators while preserving the existing length limits and
required validation.
- Line 34: Replace the plain dictionary assigned to each of the six model_config
class attributes with the typed pydantic v2 ConfigDict form, preserving
from_attributes=True. Update the relevant schema definitions in
workflow_authoring.py and add or reuse the appropriate ConfigDict import.
- Around line 37-53: Update WorkspaceSettingsUpdate and WorkspaceSettingsRead to
validate timezone values against real IANA timezones, reusing the project’s
established timezone-validation approach or zoneinfo.available_timezones().
Align deterministic_simulation and max_items_per_run with WorkflowSettings by
exposing the camelCase names deterministicSimulation and maxItemsPerRun while
preserving their existing optionality, defaults, and positive-value validation.

In `@backend/workflow/compiler.py`:
- Around line 513-618: Add a local MAX_PACKAGE_NESTING_DEPTH guard at the start
of _expand_package_internals before recursing into nested internals, so the
helper remains safe when called independently of _validate_package_internals.
Preserve the existing expansion behavior within the allowed depth and stop or
reject deeper nesting using the helper’s established contract.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6a483e32-ec88-489f-8aa9-f6b737ded2ad

📥 Commits

Reviewing files that changed from the base of the PR and between 6e09973 and c326a2a.

📒 Files selected for processing (16)
  • backend/api/v1/__init__.py
  • backend/api/v1/workspaces.py
  • backend/migrations/versions/r5s6t7u8v9w0_add_workflow_authoring_tables.py
  • backend/models/__init__.py
  • backend/models/validation_run.py
  • backend/models/workflow_authoring.py
  • backend/schemas/workflow.py
  • backend/schemas/workflow_authoring.py
  • backend/services/validation_run_service.py
  • backend/services/workflow_authoring_service.py
  • backend/workflow/compiler.py
  • backend/workflow/conformance/contracts.py
  • backend/workflow/external_importer.py
  • tests/integration/test_workflow_authoring_api.py
  • tests/integration/test_workflow_compile_api.py
  • tests/integration/test_workflow_external_import_api.py

Comment on lines +55 to +83
@router.get(
"/workspaces/{workspace_id}/settings",
response_model=ApiResponse[schemas.WorkspaceSettingsRead],
)
async def get_workspace_settings(
workspace_id: str, db: AsyncSession = Depends(get_db)
) -> ApiResponse[schemas.WorkspaceSettingsRead]:
settings = await authoring_service.get_workspace_settings(db, workspace_id)
if settings is None:
raise HTTPException(status_code=404, detail="Workspace not found")
return ApiResponse.ok(schemas.WorkspaceSettingsRead.model_validate(settings))


@router.put(
"/workspaces/{workspace_id}/settings",
response_model=ApiResponse[schemas.WorkspaceSettingsRead],
)
async def update_workspace_settings(
workspace_id: str,
body: schemas.WorkspaceSettingsUpdate,
db: AsyncSession = Depends(get_db),
) -> ApiResponse[schemas.WorkspaceSettingsRead]:
settings = await authoring_service.get_workspace_settings(db, workspace_id)
if settings is None:
raise HTTPException(status_code=404, detail="Workspace not found")
settings = await authoring_service.update_workspace_settings(db, settings, body)
await db.commit()
await db.refresh(settings)
return ApiResponse.ok(schemas.WorkspaceSettingsRead.model_validate(settings))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
cat backend/migrations/versions/r5s6t7u8v9w0_add_workflow_authoring_tables.py 2>/dev/null | rg -n "workspace_settings|insert" -A3

Repository: 2233admin/opencli-admin

Length of output: 880


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== workspaces api ==\n'
sed -n '1,180p' backend/api/v1/workspaces.py

printf '\n== workflow authoring service ==\n'
sed -n '1,260p' backend/services/workflow_authoring_service.py

printf '\n== migrations mentioning workspace_settings or backfill ==\n'
rg -n "workspace_settings|backfill|timezone|create_workspace" backend/migrations backend/services backend/api -S

Repository: 2233admin/opencli-admin

Length of output: 27936


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== schemas/workflow_authoring.py ==\n'
sed -n '1,260p' backend/schemas/workflow_authoring.py

printf '\n== models/workflow_authoring.py ==\n'
sed -n '1,260p' backend/models/workflow_authoring.py

printf '\n== initial schema around workspace tables ==\n'
sed -n '1,170p' backend/migrations/versions/5a9a94795d00_initial_schema.py

printf '\n== workflow authoring migration around workspace settings ==\n'
sed -n '1,120p' backend/migrations/versions/r5s6t7u8v9w0_add_workflow_authoring_tables.py

printf '\n== search for global IntegrityError handling ==\n'
rg -n "IntegrityError|HTTPException\\(status_code=422|status_code=409" backend -S

Repository: 2233admin/opencli-admin

Length of output: 25616


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== workspaces api around later handlers ==\n'
sed -n '180,280p' backend/api/v1/workspaces.py

printf '\n== search migrations for any earlier workspaces/workspace_settings definitions ==\n'
rg -n "op\.create_table\(\s*['\"]workspaces['\"]|op\.create_table\(\s*['\"]workspace_settings['\"]|workspace_settings" backend/migrations/versions -S

printf '\n== inspect migration order / filenames ==\n'
ls backend/migrations/versions | sort

Repository: 2233admin/opencli-admin

Length of output: 6515


Handle IntegrityError in workspace settings updates
WorkspaceSettingsUpdate.timezone allows null, but workspace_settings.timezone is non-nullable, so this endpoint can raise an unhandled 500 on bad input. Wrap the commit/rollback and return 422 like the other create/update paths.

🧰 Tools
🪛 Ruff (0.15.20)

[warning] 60-60: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


[warning] 75-75: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)

🤖 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/api/v1/workspaces.py` around lines 55 - 83, Update
update_workspace_settings to catch IntegrityError around db.commit(), roll back
the transaction on failure, and raise HTTPException with status 422 consistent
with other create/update paths; keep the successful refresh and response flow
unchanged.

Comment on lines +1 to +30
from sqlalchemy import JSON, Boolean, ForeignKey, Integer, String
from sqlalchemy.orm import Mapped, mapped_column, relationship

from backend.models.base import TimestampMixin


class ValidationRun(TimestampMixin):
"""A single compile+conformance validation attempt gating draft publish."""

__tablename__ = "validation_runs"

draft_id: Mapped[str] = mapped_column(
String(36), ForeignKey("workflow_drafts.id", ondelete="CASCADE"), nullable=False, index=True
)
draft_revision: Mapped[int] = mapped_column(Integer, nullable=False)
status: Mapped[str] = mapped_column(String(50), nullable=False, default="pending")
compile_valid: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
compile_errors: Mapped[list | None] = mapped_column(JSON, nullable=True)
conformance_mode: Mapped[str] = mapped_column(String(50), nullable=False, default="passthrough")
expected_events: Mapped[list | None] = mapped_column(JSON, nullable=True)
conformance_result: Mapped[dict | None] = mapped_column(JSON, nullable=True)
runtime_passport: Mapped[dict | None] = mapped_column(JSON, nullable=True)
run_id: Mapped[str | None] = mapped_column(
String(36), ForeignKey("workflow_runs.id", ondelete="SET NULL"), nullable=True
)
failure_reason: Mapped[str | None] = mapped_column(String(2000), nullable=True)

version: Mapped["WorkflowVersion | None"] = relationship(
"WorkflowVersion", back_populates="validation_run", uselist=False
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add TYPE_CHECKING import for WorkflowVersion forward reference.

Same issue as in workflow_authoring.py: Line 28 references Mapped["WorkflowVersion | None"] without importing WorkflowVersion, triggering Ruff F821.

🔧 Proposed fix
+from typing import TYPE_CHECKING
+
 from sqlalchemy import JSON, Boolean, ForeignKey, Integer, String
 from sqlalchemy.orm import Mapped, mapped_column, relationship

 from backend.models.base import TimestampMixin
+
+if TYPE_CHECKING:
+    from backend.models.workflow_authoring import WorkflowVersion
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
from sqlalchemy import JSON, Boolean, ForeignKey, Integer, String
from sqlalchemy.orm import Mapped, mapped_column, relationship
from backend.models.base import TimestampMixin
class ValidationRun(TimestampMixin):
"""A single compile+conformance validation attempt gating draft publish."""
__tablename__ = "validation_runs"
draft_id: Mapped[str] = mapped_column(
String(36), ForeignKey("workflow_drafts.id", ondelete="CASCADE"), nullable=False, index=True
)
draft_revision: Mapped[int] = mapped_column(Integer, nullable=False)
status: Mapped[str] = mapped_column(String(50), nullable=False, default="pending")
compile_valid: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
compile_errors: Mapped[list | None] = mapped_column(JSON, nullable=True)
conformance_mode: Mapped[str] = mapped_column(String(50), nullable=False, default="passthrough")
expected_events: Mapped[list | None] = mapped_column(JSON, nullable=True)
conformance_result: Mapped[dict | None] = mapped_column(JSON, nullable=True)
runtime_passport: Mapped[dict | None] = mapped_column(JSON, nullable=True)
run_id: Mapped[str | None] = mapped_column(
String(36), ForeignKey("workflow_runs.id", ondelete="SET NULL"), nullable=True
)
failure_reason: Mapped[str | None] = mapped_column(String(2000), nullable=True)
version: Mapped["WorkflowVersion | None"] = relationship(
"WorkflowVersion", back_populates="validation_run", uselist=False
)
from typing import TYPE_CHECKING
from sqlalchemy import JSON, Boolean, ForeignKey, Integer, String
from sqlalchemy.orm import Mapped, mapped_column, relationship
from backend.models.base import TimestampMixin
if TYPE_CHECKING:
from backend.models.workflow_authoring import WorkflowVersion
class ValidationRun(TimestampMixin):
"""A single compile+conformance validation attempt gating draft publish."""
__tablename__ = "validation_runs"
draft_id: Mapped[str] = mapped_column(
String(36), ForeignKey("workflow_drafts.id", ondelete="CASCADE"), nullable=False, index=True
)
draft_revision: Mapped[int] = mapped_column(Integer, nullable=False)
status: Mapped[str] = mapped_column(String(50), nullable=False, default="pending")
compile_valid: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
compile_errors: Mapped[list | None] = mapped_column(JSON, nullable=True)
conformance_mode: Mapped[str] = mapped_column(String(50), nullable=False, default="passthrough")
expected_events: Mapped[list | None] = mapped_column(JSON, nullable=True)
conformance_result: Mapped[dict | None] = mapped_column(JSON, nullable=True)
runtime_passport: Mapped[dict | None] = mapped_column(JSON, nullable=True)
run_id: Mapped[str | None] = mapped_column(
String(36), ForeignKey("workflow_runs.id", ondelete="SET NULL"), nullable=True
)
failure_reason: Mapped[str | None] = mapped_column(String(2000), nullable=True)
version: Mapped["WorkflowVersion | None"] = relationship(
"WorkflowVersion", back_populates="validation_run", uselist=False
)
🧰 Tools
🪛 Ruff (0.15.20)

[error] 28-28: Undefined name WorkflowVersion

(F821)

🤖 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/models/validation_run.py` around lines 1 - 30, Add a
TYPE_CHECKING-only import for WorkflowVersion in validation_run.py so the
forward reference on ValidationRun.version resolves for static analysis without
introducing a runtime import cycle.

Source: Linters/SAST tools

Comment on lines +95 to +122
class WorkflowVersion(TimestampMixin):
"""An immutable, published WorkflowProject snapshot."""

__tablename__ = "workflow_versions"
__table_args__ = (
UniqueConstraint("project_id", "version_number", name="uq_workflow_versions_project_number"),
)

project_id: Mapped[str] = mapped_column(
String(36), ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True
)
draft_id: Mapped[str | None] = mapped_column(
String(36), ForeignKey("workflow_drafts.id", ondelete="SET NULL"), nullable=True, index=True
)
version_number: Mapped[int] = mapped_column(Integer, nullable=False)
source_revision: Mapped[int] = mapped_column(Integer, nullable=False)
validation_run_id: Mapped[str] = mapped_column(
String(36),
ForeignKey("validation_runs.id", ondelete="RESTRICT"),
nullable=False,
unique=True,
index=True,
)
snapshot: Mapped[dict] = mapped_column(JSON, nullable=False, default=dict)

project: Mapped["Project"] = relationship("Project", back_populates="versions")
draft: Mapped["WorkflowDraft | None"] = relationship("WorkflowDraft", back_populates="versions")
validation_run: Mapped["ValidationRun"] = relationship("ValidationRun", back_populates="version")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add TYPE_CHECKING import for cross-module forward reference.

Line 122 uses Mapped["ValidationRun"] but ValidationRun is never imported in this module, which is exactly what Ruff's F821 hint flags. This is a documented SQLAlchemy/Ruff friction point — SQLA suggests: Mapped["Parent"] (assuming Parent model is in another module not yet imported). Ruff conflicts with rules UP037 and F821. The fix confirmed by SQLAlchemy usage guidance is our forward ref to "User" will be fine as long as you've imported User via TYPE_CHECKING.

🔧 Proposed fix
+from typing import TYPE_CHECKING
+
 from sqlalchemy import JSON, Boolean, ForeignKey, Integer, String, UniqueConstraint
 from sqlalchemy.orm import Mapped, mapped_column, relationship

 from backend.models.base import TimestampMixin
+
+if TYPE_CHECKING:
+    from backend.models.validation_run import ValidationRun
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
class WorkflowVersion(TimestampMixin):
"""An immutable, published WorkflowProject snapshot."""
__tablename__ = "workflow_versions"
__table_args__ = (
UniqueConstraint("project_id", "version_number", name="uq_workflow_versions_project_number"),
)
project_id: Mapped[str] = mapped_column(
String(36), ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True
)
draft_id: Mapped[str | None] = mapped_column(
String(36), ForeignKey("workflow_drafts.id", ondelete="SET NULL"), nullable=True, index=True
)
version_number: Mapped[int] = mapped_column(Integer, nullable=False)
source_revision: Mapped[int] = mapped_column(Integer, nullable=False)
validation_run_id: Mapped[str] = mapped_column(
String(36),
ForeignKey("validation_runs.id", ondelete="RESTRICT"),
nullable=False,
unique=True,
index=True,
)
snapshot: Mapped[dict] = mapped_column(JSON, nullable=False, default=dict)
project: Mapped["Project"] = relationship("Project", back_populates="versions")
draft: Mapped["WorkflowDraft | None"] = relationship("WorkflowDraft", back_populates="versions")
validation_run: Mapped["ValidationRun"] = relationship("ValidationRun", back_populates="version")
from typing import TYPE_CHECKING
from sqlalchemy import JSON, Boolean, ForeignKey, Integer, String, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column, relationship
from backend.models.base import TimestampMixin
if TYPE_CHECKING:
from backend.models.validation_run import ValidationRun
class WorkflowVersion(TimestampMixin):
"""An immutable, published WorkflowProject snapshot."""
__tablename__ = "workflow_versions"
__table_args__ = (
UniqueConstraint("project_id", "version_number", name="uq_workflow_versions_project_number"),
)
project_id: Mapped[str] = mapped_column(
String(36), ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True
)
draft_id: Mapped[str | None] = mapped_column(
String(36), ForeignKey("workflow_drafts.id", ondelete="SET NULL"), nullable=True, index=True
)
version_number: Mapped[int] = mapped_column(Integer, nullable=False)
source_revision: Mapped[int] = mapped_column(Integer, nullable=False)
validation_run_id: Mapped[str] = mapped_column(
String(36),
ForeignKey("validation_runs.id", ondelete="RESTRICT"),
nullable=False,
unique=True,
index=True,
)
snapshot: Mapped[dict] = mapped_column(JSON, nullable=False, default=dict)
project: Mapped["Project"] = relationship("Project", back_populates="versions")
draft: Mapped["WorkflowDraft | None"] = relationship("WorkflowDraft", back_populates="versions")
validation_run: Mapped["ValidationRun"] = relationship("ValidationRun", back_populates="version")
🧰 Tools
🪛 Ruff (0.15.20)

[error] 122-122: Undefined name ValidationRun

(F821)

🤖 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/models/workflow_authoring.py` around lines 95 - 122, Add a
TYPE_CHECKING-guarded import for ValidationRun in the module containing
WorkflowVersion, keeping it excluded at runtime while satisfying the forward
reference used by validation_run: Mapped["ValidationRun"].

Source: Linters/SAST tools



ExternalWorkflowRuntime = Literal["langgraph", "langchain"]
ExternalWorkflowRuntime = Literal["langgraph", "langchain", "dify", "n8n"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Files mentioning ExternalWorkflowRuntime ==\n'
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/build/**' 'ExternalWorkflowRuntime' .

printf '\n== backend/schemas/workflow.py around the literal ==\n'
sed -n '220,260p' backend/schemas/workflow.py

printf '\n== frontend/lib/workflow/backend-import.ts ==\n'
sed -n '1,220p' frontend/lib/workflow/backend-import.ts

Repository: 2233admin/opencli-admin

Length of output: 3767


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== backend/schemas/workflow.py ==\n'
grep -n 'ExternalWorkflowRuntime' -n backend/schemas/workflow.py || true
sed -n '236,248p' backend/schemas/workflow.py

printf '\n== frontend/lib/workflow/backend-import.ts ==\n'
grep -n 'ExternalWorkflowRuntime' -n frontend/lib/workflow/backend-import.ts || true
sed -n '1,80p' frontend/lib/workflow/backend-import.ts

printf '\n== any other frontend declarations ==\n'
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/build/**' 'langgraph"\s*\|\s*"langchain"|langgraph"|langchain"|dify"|n8n"' frontend backend

Repository: 2233admin/opencli-admin

Length of output: 8692


Expand the frontend runtime union frontend/lib/workflow/backend-import.ts still only allows "langgraph" | "langchain", so importExternalRuntimeWorkflow() can’t accept "dify" or "n8n". Keep it in sync with backend/schemas/workflow.py.

🤖 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/schemas/workflow.py` at line 242, Update the runtime union used by
importExternalRuntimeWorkflow in frontend/lib/workflow/backend-import.ts to
include "dify" and "n8n" alongside the existing "langgraph" and "langchain"
values, keeping it synchronized with ExternalWorkflowRuntime.

Comment on lines +80 to +86
projection = await start_workflow_run(
WorkflowRunStartRequest(project=project), session=session
)
validation_run.run_id = projection.runId

events = await list_workflow_run_events(projection.runId, session=session) or []
actual_events = [event.model_dump(mode="json") for event in events]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## validation_run_service.py\n'
nl -ba backend/services/validation_run_service.py | sed -n '1,180p'

printf '\n## workspaces.py\n'
nl -ba backend/api/workspaces.py | sed -n '1,240p'

printf '\n## get_db / session helper search\n'
rg -n "def get_db|async def get_db|sessionmaker|yield .*session|rollback|commit|ValidationRunRequestError" backend -S

Repository: 2233admin/opencli-admin

Length of output: 231


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

def show(path, start, end):
    print(f"\n## {path}")
    for i, line in enumerate(Path(path).read_text().splitlines(), 1):
        if start <= i <= end:
            print(f"{i:4d}: {line}")

show("backend/services/validation_run_service.py", 1, 220)
show("backend/api/workspaces.py", 1, 280)
PY

printf '\n## get_db / session helper search\n'
rg -n "def get_db|async def get_db|sessionmaker|yield .*session|rollback|commit|ValidationRunRequestError" backend -S

Repository: 2233admin/opencli-admin

Length of output: 8492


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## create_validation_run search\n'
rg -n "create_validation_run|ValidationRunRequestError|run_validation\(" backend -S

printf '\n## get_db search\n'
rg -n "def get_db|async def get_db|sessionmaker|rollback|commit|yield .*session" backend -S

printf '\n## likely workspace/api files\n'
fd -a -t f 'workspaces.py' backend . || true
fd -a -t f 'workspace*.py' backend . || true
fd -a -t f '*workspace*' backend . || true

Repository: 2233admin/opencli-admin

Length of output: 15981


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

def show(path, start, end):
    print(f"\n## {path}")
    for i, line in enumerate(Path(path).read_text().splitlines(), 1):
        if start <= i <= end:
            print(f"{i:4d}: {line}")

show("backend/api/v1/workspaces.py", 170, 220)
show("backend/database.py", 1, 120)
PY

Repository: 2233admin/opencli-admin

Length of output: 6234


Handle workflow runtime failures here. get_db() rolls back the flushed row on exceptions, so it won’t stay pending, but start_workflow_run() / list_workflow_run_events() still escape as a 500 and never persist a failed ValidationRun with failure_reason.

🤖 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/services/validation_run_service.py` around lines 80 - 86, Update the
validation-run workflow in the surrounding service method to catch failures from
start_workflow_run and list_workflow_run_events, persist the ValidationRun as
failed with an appropriate failure_reason, and return the failed result instead
of allowing the exception to become a 500. Preserve the existing successful
event-processing path and ensure the failure update is committed before
returning.

Comment on lines +160 to +178
next_version_result = await session.execute(
select(func.max(WorkflowVersion.version_number)).where(
WorkflowVersion.project_id == draft.project_id
)
)
next_version_number = (next_version_result.scalar_one_or_none() or 0) + 1

version = WorkflowVersion(
project_id=draft.project_id,
draft_id=draft.id,
version_number=next_version_number,
source_revision=draft.revision,
validation_run_id=validation_run.id,
snapshot=draft.snapshot,
)
session.add(version)
await session.flush()
await session.refresh(version)
return version

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n "version_number|UniqueConstraint|unique=True" backend/migrations/versions/r5s6t7u8v9w0_add_workflow_authoring_tables.py

Repository: 2233admin/opencli-admin

Length of output: 578


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== workflow version model / service / handler locations =="
rg -n "class WorkflowVersion|version_number|publish_draft|already published|concurrent publish|IntegrityError" backend -g '*.py'

echo
echo "== relevant model/migration snippets =="
sed -n '1,220p' backend/models/workflow_authoring.py
echo
sed -n '1,220p' backend/services/validation_run_service.py
echo
sed -n '220,290p' backend/workspaces.py

Repository: 2233admin/opencli-admin

Length of output: 14854


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '210,265p' backend/api/v1/workspaces.py

Repository: 2233admin/opencli-admin

Length of output: 2523


Serialize version allocation in publish_draft. uq_workflow_versions_project_number already prevents duplicate rows, but MAX(version_number)+1 still races under concurrent publishes and one request will bounce with a generic 409. Lock the project row or allocate the next version atomically before inserting.

🤖 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/services/validation_run_service.py` around lines 160 - 178, Update
publish_draft’s version allocation around the WorkflowVersion query so
concurrent publishes for the same project are serialized before calculating
MAX(version_number)+1. Lock the associated project row, or use an equivalent
atomic allocation mechanism, then insert the WorkflowVersion while preserving
the existing unique constraint and return flow.

Comment on lines +47 to +75
async def update_workspace(
session: AsyncSession, workspace: Workspace, data: WorkspaceUpdate
) -> Workspace:
updates = data.model_dump(exclude_unset=True)
for key, value in updates.items():
setattr(workspace, key, value)
await session.flush()
await session.refresh(workspace)
return workspace


async def get_workspace_settings(
session: AsyncSession, workspace_id: str
) -> Optional[WorkspaceSettings]:
result = await session.execute(
select(WorkspaceSettings).where(WorkspaceSettings.workspace_id == workspace_id)
)
return result.scalar_one_or_none()


async def update_workspace_settings(
session: AsyncSession, settings: WorkspaceSettings, data: WorkspaceSettingsUpdate
) -> WorkspaceSettings:
updates = data.model_dump(exclude_unset=True)
for key, value in updates.items():
setattr(settings, key, value)
await session.flush()
await session.refresh(settings)
return settings

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n "class Workspace\b" -A 20 backend/models/workflow_authoring.py
rg -n "class WorkspaceSettings" -A 20 backend/models/workflow_authoring.py

Repository: 2233admin/opencli-admin

Length of output: 1752


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the update schemas and the endpoints that call these service functions.
rg -n "class WorkspaceUpdate|class WorkspaceSettingsUpdate|update_workspace_settings|update_workspace" backend -A 20 -B 10

# Inspect any IntegrityError handling around workspace update routes.
rg -n "IntegrityError|HTTPException|except.*IntegrityError" backend -A 20 -B 10

Repository: 2233admin/opencli-admin

Length of output: 50380


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Look for global handlers or middleware that translate IntegrityError / DB constraint failures.
rg -n "IntegrityError|exception_handler|add_exception_handler|SQLAlchemyError|DBAPIError|OperationalError" backend -A 20 -B 10

# Check the workspace endpoints around the update paths for local handling.
sed -n '1,130p' backend/api/v1/workspaces.py

Repository: 2233admin/opencli-admin

Length of output: 40976


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '40,90p' backend/api/v1/workspaces.py
sed -n '1,120p' backend/schemas/workflow_authoring.py

Repository: 2233admin/opencli-admin

Length of output: 5221


Reject explicit null in workspace updates. exclude_unset=True still forwards client-sent null, so Workspace.name and the non-null WorkspaceSettings fields can be set to None and fail on flush/commit; the update route doesn’t catch IntegrityError, so this becomes a 500. Filter out None values or validate them explicitly before persisting.

🤖 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/services/workflow_authoring_service.py` around lines 47 - 75, Update
update_workspace and update_workspace_settings to exclude or reject
client-supplied None values before applying fields, while preserving valid
non-null updates. Ensure Workspace.name and non-null WorkspaceSettings fields
cannot be assigned None before session.flush().

Comment on lines +122 to +134
async def update_draft(
session: AsyncSession, draft: WorkflowDraft, data: WorkflowDraftUpdate
) -> WorkflowDraft:
if draft.revision != data.expected_revision:
raise DraftRevisionConflictError(
f"draft revision {draft.revision} does not match expected "
f"{data.expected_revision}"
)
draft.snapshot = data.snapshot.model_dump(mode="json")
draft.revision += 1
await session.flush()
await session.refresh(draft)
return draft

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n "class WorkflowDraft" -A 30 backend/models/workflow_authoring.py
rg -n "version_id_col" backend/models/workflow_authoring.py

Repository: 2233admin/opencli-admin

Length of output: 1466


🏁 Script executed:

#!/bin/bash
set -e
printf '\n## workflow_authoring_service.py outline\n'
ast-grep outline backend/services/workflow_authoring_service.py --view expanded || true
printf '\n## workflow_authoring_service.py relevant lines\n'
cat -n backend/services/workflow_authoring_service.py | sed -n '1,220p'
printf '\n## search for WorkflowDraft loading / locking / revision conflict handling\n'
rg -n "WorkflowDraft|DraftRevisionConflictError|with_for_update|version_id_col|StaleDataError|expected_revision|revision" backend -g'*.py'

Repository: 2233admin/opencli-admin

Length of output: 23089


Make the draft revision update atomic backend/services/workflow_authoring_service.py:122-134

WorkflowDraft has no version_id_col, so this Python-side revision check can still lose concurrent updates. Move the revision guard into the UPDATE itself or add SQLAlchemy optimistic locking so stale writes fail instead of overwriting each other.

🤖 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/services/workflow_authoring_service.py` around lines 122 - 134, The
update_draft function currently performs a non-atomic Python-side revision
check, allowing concurrent updates to overwrite each other. Make the revision
guard part of the database update or configure SQLAlchemy optimistic locking for
WorkflowDraft, ensuring stale writes fail with DraftRevisionConflictError and
only the row matching data.expected_revision is updated.

Comment on lines +234 to +269
def _extract_n8n_connection_edges(connections: dict[str, Any]) -> list[dict[str, Any]]:
"""Flatten n8n's ``{sourceName: {main: [[{node, type, index}, ...]]}}`` shape.

n8n's export keys ``connections`` by node *name* (not the node's ``id``
field), and each output port fans out to a list of connection chains —
unlike the list-of-{source,target} shape every other runtime exports.
"""

edges: list[dict[str, Any]] = []
for source_name, outputs in connections.items():
source = _read_string(source_name)
if not source or not isinstance(outputs, dict):
continue
for port_name, chains in outputs.items():
if not isinstance(chains, list):
continue
for chain in chains:
if not isinstance(chain, list):
continue
for connection in chain:
if not isinstance(connection, dict):
continue
target = _read_string(connection.get("node"))
if not target:
continue
edges.append(
{
"id": f"edge-{len(edges) + 1}",
"source": source,
"target": target,
"sourcePort": _read_string(port_name) or "main",
}
)
return edges


Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map relevant file structure and locate the importer and its tests
git ls-files backend/workflow | sed -n '1,200p'
echo
rg -n "def _extract_n8n_connection_edges|import_external_workflow|sourcePort|externalWorkflow|sourceBranchIndex|connect_nodes" backend/workflow -n

Repository: 2233admin/opencli-admin

Length of output: 4726


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== backend/workflow/external_importer.py (relevant sections) =="
sed -n '1,160p' backend/workflow/external_importer.py
echo
sed -n '234,290p' backend/workflow/external_importer.py
echo

echo "== backend/workflow/compiler.py (sourcePort handling) =="
sed -n '300,345p' backend/workflow/compiler.py
echo
sed -n '560,610p' backend/workflow/compiler.py
echo
sed -n '840,875p' backend/workflow/compiler.py
echo

echo "== backend/workflow/runtime_registry.py (externalWorkflow handling) =="
sed -n '420,520p' backend/workflow/runtime_registry.py
echo

echo "== n8n/external importer tests =="
rg -n "n8n|external_import|externalWorkflow|sourcePort|connect_nodes" backend -g '*test*' -g '*spec*'

Repository: 2233admin/opencli-admin

Length of output: 17228


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== backend/workflow/external_importer.py (edge helpers) =="
sed -n '160,290p' backend/workflow/external_importer.py
echo

echo "== n8n-specific references in repository =="
rg -n "n8n|branch|index|true branch|false branch|Switch|IF" backend -g '!**/__pycache__/**'

Repository: 2233admin/opencli-admin

Length of output: 50379


Preserve n8n branch provenance in imported edges. _extract_n8n_connection_edges() drops the output-slot index, and import_external_workflow() then ignores even the remaining sourcePort by rebuilding edges from the node alone. That collapses IF/Switch true/false branches into indistinguishable connections, so n8n conditional control flow can’t round-trip. Thread the branch index through the edge payload and ui.externalWorkflow if n8n imports need to retain semantics.

🤖 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/external_importer.py` around lines 234 - 269, Preserve n8n
branch provenance by adding each connection’s output-slot index to the edge
payload produced by _extract_n8n_connection_edges(), then update
import_external_workflow() and the ui.externalWorkflow conversion to retain and
use both sourcePort and the branch index instead of rebuilding edges from node
names alone. Ensure IF/Switch true and false branches remain distinguishable
through import and round-trip export.

@2233admin

Copy link
Copy Markdown
Owner Author

工作流持久化、Dify 导入/编译/运行、递归包与生命周期能力已由统一基准 codex/unified-product-3002 的当前实现取代。关闭并清理该旧分支。

@2233admin 2233admin closed this Jul 24, 2026
@2233admin
2233admin deleted the feat/workflow-persistence-closed-loop branch July 24, 2026 10:32
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.

1 participant