2828from backend .schemas import workflow as workflow_schemas
2929from backend .schemas .common import ApiResponse
3030from backend .workflow .compiler import compile_workflow_project
31+ from backend .workflow .trigger_scope import scoped_project , select_active_union
3132
3233router = APIRouter ()
3334
@@ -57,6 +58,60 @@ def _isolated_source_errors(
5758 ]
5859
5960
61+ def _parked_diagnostics (
62+ project : workflow_schemas .WorkflowProject ,
63+ parked_ids : list [str ],
64+ ) -> list [workflow_schemas .WorkflowCompileError ]:
65+ """Emit node-anchored diagnostics for every parked canvas node.
66+
67+ Membership comes first (one ``parked_node`` row per parked id, in authored
68+ order). Any original configuration diagnostic for the same node follows in
69+ its existing order so the UI can render each failure cause individually.
70+ """
71+
72+ parked_set = set (parked_ids )
73+ diagnostics : list [workflow_schemas .WorkflowCompileError ] = []
74+ for node_id in parked_ids :
75+ diagnostics .append (
76+ workflow_schemas .WorkflowCompileError (
77+ code = "parked_node" ,
78+ message = f'Workflow node "{ node_id } " is not connected to a supported trigger.' ,
79+ node_id = node_id ,
80+ path = ["nodes" , node_id ],
81+ )
82+ )
83+
84+ # Compile parked nodes in isolation to surface configuration diagnostics
85+ # (unknown bindings, missing params, etc.) as warnings without edges that
86+ # would produce irrelevant port-mismatch noise.
87+ if not parked_set :
88+ return diagnostics
89+ parked_nodes = [n for n in project .nodes if n .id in parked_set ]
90+ parked_project = workflow_schemas .WorkflowProject (
91+ id = project .id ,
92+ name = project .name ,
93+ profile = project .profile ,
94+ version = project .version ,
95+ nodes = parked_nodes ,
96+ edges = [],
97+ settings = project .settings ,
98+ adapters = list (project .adapters ),
99+ agentPermissions = project .agentPermissions ,
100+ )
101+ parked_result = compile_workflow_project (parked_project )
102+ for error in parked_result .errors :
103+ if error .node_id and error .node_id in parked_set :
104+ diagnostics .append (
105+ workflow_schemas .WorkflowCompileError (
106+ code = error .code ,
107+ message = error .message ,
108+ node_id = error .node_id ,
109+ path = error .path ,
110+ )
111+ )
112+ return diagnostics
113+
114+
60115def _image_generation_nodes (
61116 nodes : object ,
62117 * ,
@@ -185,6 +240,9 @@ async def validate_draft(
185240 project_id = project_id ,
186241 workflow_id = workflow_id ,
187242 )
243+ warnings : list [workflow_schemas .WorkflowCompileError ] = []
244+ valid = False
245+ stored_graph : dict [str , Any ] | None = None
188246 try :
189247 project = workflow_schemas .WorkflowProject .model_validate (resolved_graph )
190248 except ValidationError as exc :
@@ -196,25 +254,47 @@ async def validate_draft(
196254 )
197255 for error in exc .errors ()
198256 )
199- valid = False
200257 else :
201- errors .extend (_isolated_source_errors (project ))
202- if errors :
203- valid = False
258+ active_union = select_active_union (project )
259+ if not active_union .has_supported_trigger :
260+ # Legacy / media-canvas / non-trigger workflows preserve the
261+ # existing full-graph validation path unchanged.
262+ errors .extend (_isolated_source_errors (project ))
263+ if not errors :
264+ result = compile_workflow_project (project )
265+ errors = list (result .errors )
266+ if result .valid and result .plan is not None :
267+ valid = True
268+ stored_graph = resolved_graph
204269 else :
205- result = compile_workflow_project (project )
206- errors = result .errors
207- valid = result .valid
208-
270+ scoped = scoped_project (
271+ project = project ,
272+ active_ids = active_union .active_node_ids ,
273+ external_ids = {
274+ node .id
275+ for node in project .nodes
276+ if isinstance (node .params .get ("externalWorkflow" ), dict )
277+ },
278+ )
279+ errors .extend (_isolated_source_errors (scoped ))
280+ if not errors :
281+ scoped_result = compile_workflow_project (scoped )
282+ errors = list (scoped_result .errors )
283+ if scoped_result .valid and scoped_result .plan is not None :
284+ valid = True
285+ stored_graph = scoped .model_dump (mode = "json" )
286+ warnings .extend (
287+ _parked_diagnostics (project , active_union .parked_node_ids )
288+ )
209289 row = StudioWorkflowValidationRun (
210290 workflow_id = workflow_id ,
211291 draft_revision = draft .revision ,
212292 status = "completed" if valid else "failed" ,
213293 valid = valid ,
214294 errors = [error .model_dump (mode = "json" ) for error in errors ],
215- warnings = [],
295+ warnings = [warning . model_dump ( mode = "json" ) for warning in warnings ],
216296 compile_version = workflow_schemas .WORKFLOW_COMPILE_VERSION ,
217- resolved_graph = resolved_graph if valid else None ,
297+ resolved_graph = stored_graph ,
218298 )
219299 db .add (row )
220300 await db .flush ()
0 commit comments