Skip to content

Commit 6eae980

Browse files
[APO-2321] Make entrypoint node optional in Python SDK serialization
Co-Authored-By: [email protected] <[email protected]>
1 parent 2561167 commit 6eae980

File tree

2 files changed

+53
-34
lines changed

2 files changed

+53
-34
lines changed

ee/vellum_ee/workflows/display/base.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,9 @@ class WorkflowDisplayData(UniversalBaseModel):
4949

5050
@dataclass
5151
class WorkflowMetaDisplay:
52-
entrypoint_node_id: UUID
53-
entrypoint_node_source_handle_id: UUID
54-
entrypoint_node_display: NodeDisplayData = Field(default_factory=NodeDisplayData)
52+
entrypoint_node_id: Optional[UUID] = None
53+
entrypoint_node_source_handle_id: Optional[UUID] = None
54+
entrypoint_node_display: Optional[NodeDisplayData] = None
5555
display_data: WorkflowDisplayData = field(default_factory=WorkflowDisplayData)
5656

5757
@classmethod

ee/vellum_ee/workflows/display/workflows/base_workflow_display.py

Lines changed: 50 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -249,8 +249,33 @@ def serialize(self) -> JsonObject:
249249
manual_trigger_edges = [edge for edge in trigger_edges if issubclass(edge.trigger_class, ManualTrigger)]
250250
has_manual_trigger = len(manual_trigger_edges) > 0
251251

252+
# Determine which nodes have explicit non-trigger entrypoints in the graph
253+
# We need this early to decide whether to create an ENTRYPOINT node
254+
non_trigger_entrypoint_nodes: Set[Type[BaseNode]] = set()
255+
for subgraph in self._workflow.get_subgraphs():
256+
if any(True for _ in subgraph.trigger_edges):
257+
continue
258+
for entrypoint in subgraph.entrypoints:
259+
try:
260+
non_trigger_entrypoint_nodes.add(get_unadorned_node(entrypoint))
261+
except Exception:
262+
continue
263+
264+
# Check if workflow has only non-manual triggers (IntegrationTrigger/ScheduleTrigger)
265+
has_only_non_manual_triggers = len(trigger_edges) > 0 and not has_manual_trigger
266+
267+
# We need an ENTRYPOINT node if:
268+
# 1. There's a ManualTrigger, OR
269+
# 2. There are no triggers at all (backward compatibility), OR
270+
# 3. There are non-trigger entrypoints (nodes that need ENTRYPOINT edges alongside triggers)
271+
# We skip ENTRYPOINT node only when there are ONLY non-manual triggers with no regular entrypoints
272+
needs_entrypoint_node = (
273+
has_manual_trigger or not has_only_non_manual_triggers or len(non_trigger_entrypoint_nodes) > 0
274+
)
275+
252276
entrypoint_node_id: Optional[UUID] = None
253277
entrypoint_node_source_handle_id: Optional[UUID] = None
278+
entrypoint_node_display = self.display_context.workflow_display.entrypoint_node_display
254279

255280
if has_manual_trigger:
256281
# ManualTrigger: use trigger ID for ENTRYPOINT node (backward compatibility)
@@ -267,28 +292,30 @@ def serialize(self) -> JsonObject:
267292
"label": "Entrypoint Node",
268293
"source_handle_id": str(entrypoint_node_source_handle_id),
269294
},
270-
"display_data": self.display_context.workflow_display.entrypoint_node_display.dict(),
295+
"display_data": entrypoint_node_display.dict() if entrypoint_node_display else NodeDisplayData().dict(),
271296
"base": None,
272297
"definition": None,
273298
}
274-
else:
275-
# All other cases: use workflow_display ENTRYPOINT node
299+
elif needs_entrypoint_node:
300+
# Non-trigger entrypoints exist: use workflow_display ENTRYPOINT node
276301
entrypoint_node_id = self.display_context.workflow_display.entrypoint_node_id
277302
entrypoint_node_source_handle_id = self.display_context.workflow_display.entrypoint_node_source_handle_id
278303

279-
serialized_nodes[entrypoint_node_id] = {
280-
"id": str(entrypoint_node_id),
281-
"type": "ENTRYPOINT",
282-
"inputs": [],
283-
"data": {
284-
"label": "Entrypoint Node",
285-
"source_handle_id": str(entrypoint_node_source_handle_id),
286-
},
287-
"display_data": self.display_context.workflow_display.entrypoint_node_display.dict(),
288-
"base": None,
289-
"definition": None,
290-
}
291-
# else: has_only_integration_trigger without explicit entrypoint - no ENTRYPOINT node needed
304+
if entrypoint_node_id is not None and entrypoint_node_source_handle_id is not None:
305+
display_data = entrypoint_node_display.dict() if entrypoint_node_display else NodeDisplayData().dict()
306+
serialized_nodes[entrypoint_node_id] = {
307+
"id": str(entrypoint_node_id),
308+
"type": "ENTRYPOINT",
309+
"inputs": [],
310+
"data": {
311+
"label": "Entrypoint Node",
312+
"source_handle_id": str(entrypoint_node_source_handle_id),
313+
},
314+
"display_data": display_data,
315+
"base": None,
316+
"definition": None,
317+
}
318+
# else: only non-manual triggers with no regular entrypoints - no ENTRYPOINT node needed
292319

293320
# Add all the nodes in the workflows
294321
for node in self._workflow.get_all_nodes():
@@ -395,19 +422,8 @@ def serialize(self) -> JsonObject:
395422
except Exception:
396423
continue
397424

398-
# Determine which nodes have explicit non-trigger entrypoints in the graph
399-
non_trigger_entrypoint_nodes: Set[Type[BaseNode]] = set()
400-
for subgraph in self._workflow.get_subgraphs():
401-
# If the subgraph contains trigger edges, its entrypoints were derived from triggers
402-
if any(True for _ in subgraph.trigger_edges):
403-
continue
404-
for entrypoint in subgraph.entrypoints:
405-
try:
406-
non_trigger_entrypoint_nodes.add(get_unadorned_node(entrypoint))
407-
except Exception:
408-
continue
409-
410425
# Add edges from entrypoint first to preserve expected ordering
426+
# Note: non_trigger_entrypoint_nodes was computed earlier to determine if we need an ENTRYPOINT node
411427

412428
for target_node, entrypoint_display in self.display_context.entrypoint_displays.items():
413429
unadorned_target_node = get_unadorned_node(target_node)
@@ -1048,9 +1064,12 @@ def _generate_entrypoint_display(
10481064
target_node_display = node_displays[entrypoint_target]
10491065
target_node_id = target_node_display.node_id
10501066

1051-
edge_display = edge_display_overrides or self._generate_edge_display_from_source(
1052-
entrypoint_node_id, target_node_id
1053-
)
1067+
if edge_display_overrides:
1068+
edge_display = edge_display_overrides
1069+
elif entrypoint_node_id is not None:
1070+
edge_display = self._generate_edge_display_from_source(entrypoint_node_id, target_node_id)
1071+
else:
1072+
edge_display = EdgeDisplay(id=uuid4_from_hash(f"{self.workflow_id}|id|{target_node_id}"))
10541073

10551074
return EntrypointDisplay(id=entrypoint_id, edge_display=edge_display)
10561075

0 commit comments

Comments
 (0)