fix(workflow): allow presaves of an existing empty draft

This commit is contained in:
Charles Yao committed 2026-10-12 22:26:29 -07:00
1 parent 7a76aaca36
commit cbeaad8ea7
3 files changed
+26 -8

No files matched your search

+5 -7
View File
@@ -99,6 +99,7 @@ from services.app.access import resolve_app_access_filter
from services.app_generate_service import AppGenerateService
from services.errors.app import IsDraftWorkflowError, WorkflowHashNotEqualError, WorkflowNotFoundError
from services.errors.llm import InvokeRateLimitError
from services.errors.workflow_service import EmptyWorkflowConfirmationRequiredError
from services.workflow_ref_service import WorkflowRefService
from services.workflow_service import DraftWorkflowDeletionError, WorkflowInUseError, WorkflowService
from services.workflow_variable_reference_validator import (
@@ -639,7 +640,8 @@ class DraftWorkflowApi(Resource):
@rbac_permission_required(RBACCheck(RBACPermission.APP_VIEW_LAYOUT, PlainApp()))
def post(self, current_user: Account, app_model: App):
"""
Sync draft workflow
Sync draft workflow. Empty initialization or clearing requires force=true;
an already-empty draft may be saved with its current hash without force.
"""
content_type = request.headers.get("Content-Type", "")
@@ -656,11 +658,6 @@ class DraftWorkflowApi(Resource):
else:
abort(415)
# Empty autosaves can otherwise replace a populated draft, even with a matching hash.
# Require explicit confirmation for both regular saves and text/plain unload beacons.
if args_model.graph.get("nodes") == [] and args_model.graph.get("edges") == [] and not args_model.force:
raise InvalidArgumentError(description="Saving an empty workflow requires force=true.")
workflow_service = WorkflowService()
try:
@@ -693,10 +690,11 @@ class DraftWorkflowApi(Resource):
deleted_environment_variable_ids=deleted_environment_variable_ids,
preserve_environment_variables=True,
graph_only=args_model.is_collaborative,
require_empty_graph_confirmation=not args_model.force,
)
except WorkflowHashNotEqualError:
raise DraftWorkflowNotSync()
except VariableError as e:
except (VariableError, EmptyWorkflowConfirmationRequiredError) as e:
raise InvalidArgumentError(description=str(e))
return dump_response(
+4
View File
@@ -8,3 +8,7 @@ class DraftWorkflowDeletionError(ValueError):
"""Raised when attempting to delete a draft workflow"""
pass
class EmptyWorkflowConfirmationRequiredError(ValueError):
"""Raised when an unconfirmed empty save would initialize or clear a draft."""
+17 -1
View File
@@ -152,7 +152,11 @@ from services.workflow.workflow_converter import WorkflowConverter
from services.workflow_ref_service import WorkflowRef
from services.workflow_version_number_service import allocate_version_number
from .errors.workflow_service import DraftWorkflowDeletionError, WorkflowInUseError
from .errors.workflow_service import (
DraftWorkflowDeletionError,
EmptyWorkflowConfirmationRequiredError,
WorkflowInUseError,
)
from .human_input_delivery_test_service import (
DeliveryTestContext,
DeliveryTestEmailRecipient,
@@ -424,6 +428,7 @@ class WorkflowService:
commit: bool = True,
sync_agent_bindings: bool = True,
graph_only: bool = False,
require_empty_graph_confirmation: bool = False,
) -> Workflow:
"""
Sync draft workflow.
@@ -436,7 +441,13 @@ class WorkflowService:
graph-only saves preserve independently persisted draft fields, while
an explicit per-ID environment patch is merged with the graph.
Public unconfirmed saves require an empty graph to match an already-empty
draft. The False default preserves trusted DSL import initialization and
Builder repair/rollback clears; public force=true uses the same overload.
Neither overload bypasses the existing draft hash check.
:raises WorkflowHashNotEqualError
:raises EmptyWorkflowConfirmationRequiredError
"""
if environment_variable_upserts is None and deleted_environment_variable_ids:
raise ValueError("Deleted environment variable ids require an environment variable patch.")
@@ -447,6 +458,11 @@ class WorkflowService:
if workflow and workflow.unique_hash != unique_hash:
raise WorkflowHashNotEqualError()
if require_empty_graph_confirmation and graph.get("nodes") == [] and graph.get("edges") == []:
previous_graph = workflow.graph_dict if workflow is not None else None
if previous_graph is None or previous_graph.get("nodes") != [] or previous_graph.get("edges") != []:
raise EmptyWorkflowConfirmationRequiredError("Saving an empty workflow requires force=true.")
# Collaboration persists features and variables through dedicated endpoints. A graph save
# must not overwrite those newer database values with another collaborator's stale cache.
if not graph_only or not workflow: