fix: workflow blueprints freely editable after creation (preserve_user_graph)
canonicalize_workflow_config was silently rebuilding any workflow with ui.blueprint or ui.preset == still_graph from the canonical template on every save, preflight, and read — making blueprint-based workflows effectively read-only. Add preserve_user_graph=True flag that skips the rebuild blocks. Only create_workflow still uses preserve_user_graph=False so blueprints are correctly expanded at creation time. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -512,7 +512,7 @@ def _canonicalize_legacy_custom_config(raw: dict[str, Any]) -> dict[str, Any]:
|
||||
return canonical
|
||||
|
||||
|
||||
def canonicalize_workflow_config(raw: dict[str, Any]) -> dict[str, Any]:
|
||||
def canonicalize_workflow_config(raw: dict[str, Any], *, preserve_user_graph: bool = False) -> dict[str, Any]:
|
||||
if not isinstance(raw, dict):
|
||||
raise ValueError("Workflow config must be a JSON object")
|
||||
|
||||
@@ -524,32 +524,34 @@ def canonicalize_workflow_config(raw: dict[str, Any]) -> dict[str, Any]:
|
||||
ui = {}
|
||||
normalized["ui"] = dict(ui)
|
||||
normalized["ui"].setdefault("execution_mode", "legacy")
|
||||
preset = normalized["ui"].get("preset")
|
||||
blueprint = normalized["ui"].get("blueprint")
|
||||
|
||||
if preset == "still_graph":
|
||||
merged_ui = dict(normalized["ui"])
|
||||
canonical = build_preset_workflow_config(
|
||||
"still_graph",
|
||||
_extract_render_params_from_nodes(normalized.get("nodes") or [], StepName.BLENDER_STILL),
|
||||
)
|
||||
merged_ui.setdefault("execution_mode", canonical["ui"]["execution_mode"])
|
||||
canonical["ui"].update(merged_ui)
|
||||
return canonical
|
||||
if not preserve_user_graph:
|
||||
preset = normalized["ui"].get("preset")
|
||||
blueprint = normalized["ui"].get("blueprint")
|
||||
|
||||
if blueprint in _WORKFLOW_BLUEPRINTS:
|
||||
merged_ui = dict(normalized["ui"])
|
||||
canonical = build_workflow_blueprint_config(blueprint)
|
||||
merged_ui.setdefault("execution_mode", canonical["ui"]["execution_mode"])
|
||||
canonical["ui"].update(merged_ui)
|
||||
return canonical
|
||||
if preset == "still_graph":
|
||||
merged_ui = dict(normalized["ui"])
|
||||
canonical = build_preset_workflow_config(
|
||||
"still_graph",
|
||||
_extract_render_params_from_nodes(normalized.get("nodes") or [], StepName.BLENDER_STILL),
|
||||
)
|
||||
merged_ui.setdefault("execution_mode", canonical["ui"]["execution_mode"])
|
||||
canonical["ui"].update(merged_ui)
|
||||
return canonical
|
||||
|
||||
if blueprint in _WORKFLOW_STARTER_BLUEPRINTS:
|
||||
merged_ui = dict(normalized["ui"])
|
||||
canonical = build_starter_workflow_config(_WORKFLOW_STARTER_BLUEPRINTS[blueprint])
|
||||
merged_ui.setdefault("execution_mode", canonical["ui"]["execution_mode"])
|
||||
canonical["ui"].update(merged_ui)
|
||||
return canonical
|
||||
if blueprint in _WORKFLOW_BLUEPRINTS:
|
||||
merged_ui = dict(normalized["ui"])
|
||||
canonical = build_workflow_blueprint_config(blueprint)
|
||||
merged_ui.setdefault("execution_mode", canonical["ui"]["execution_mode"])
|
||||
canonical["ui"].update(merged_ui)
|
||||
return canonical
|
||||
|
||||
if blueprint in _WORKFLOW_STARTER_BLUEPRINTS:
|
||||
merged_ui = dict(normalized["ui"])
|
||||
canonical = build_starter_workflow_config(_WORKFLOW_STARTER_BLUEPRINTS[blueprint])
|
||||
merged_ui.setdefault("execution_mode", canonical["ui"]["execution_mode"])
|
||||
canonical["ui"].update(merged_ui)
|
||||
return canonical
|
||||
|
||||
return normalized
|
||||
|
||||
|
||||
@@ -264,7 +264,7 @@ async def _build_rollout_summary(
|
||||
|
||||
|
||||
async def _workflow_to_out(db: AsyncSession, wf: WorkflowDefinition) -> WorkflowDefinitionOut:
|
||||
canonical_config = canonicalize_workflow_config(wf.config)
|
||||
canonical_config = canonicalize_workflow_config(wf.config, preserve_user_graph=True)
|
||||
workflow_family = infer_workflow_family_from_config(canonical_config)
|
||||
supported_artifact_kinds = tuple(
|
||||
derive_supported_artifact_kinds_from_workflow_config(canonical_config)
|
||||
@@ -408,7 +408,7 @@ def _build_workflow_preflight_for_config(
|
||||
submit_prepared_workflow_tasks,
|
||||
)
|
||||
|
||||
normalized_config = canonicalize_workflow_config(workflow_config)
|
||||
normalized_config = canonicalize_workflow_config(workflow_config, preserve_user_graph=True)
|
||||
try:
|
||||
workflow_context = prepare_workflow_context(
|
||||
normalized_config,
|
||||
@@ -942,7 +942,7 @@ async def update_workflow(
|
||||
wf.name = body.name
|
||||
if body.config is not None:
|
||||
try:
|
||||
normalized_config = canonicalize_workflow_config(body.config)
|
||||
normalized_config = canonicalize_workflow_config(body.config, preserve_user_graph=True)
|
||||
WorkflowConfig.model_validate(normalized_config)
|
||||
except (ValidationError, ValueError) as exc:
|
||||
detail = exc.errors() if isinstance(exc, ValidationError) else str(exc)
|
||||
@@ -1033,7 +1033,7 @@ async def _dispatch_workflow_for_config(
|
||||
)
|
||||
|
||||
try:
|
||||
normalized_config = canonicalize_workflow_config(workflow_config)
|
||||
normalized_config = canonicalize_workflow_config(workflow_config, preserve_user_graph=True)
|
||||
workflow_context = prepare_workflow_context(
|
||||
normalized_config,
|
||||
context_id=context_id,
|
||||
|
||||
Reference in New Issue
Block a user