Skip to content

Commit 6937d60

Browse files
Fix: backfill entrypoint defaults and skip ENTRYPOINT for trigger-only workflows
- Backfill entrypoint IDs in _generate_workflow_meta_display when overrides omit them - Skip ENTRYPOINT node when all graph branches are sourced from triggers - Update tests to match new spec for IntegrationTrigger-only workflows Co-Authored-By: [email protected] <[email protected]>
1 parent c78b0a7 commit 6937d60

File tree

3 files changed

+61
-39
lines changed

3 files changed

+61
-39
lines changed

ee/vellum_ee/workflows/display/tests/workflow_serialization/test_integration_trigger_serialization.py

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -187,8 +187,9 @@ class TestWorkflow(BaseWorkflow[BaseInputs, BaseState]):
187187

188188

189189
def test_integration_trigger_no_entrypoint_node():
190-
"""IntegrationTrigger workflows now create ENTRYPOINT nodes and route edges through them."""
190+
"""IntegrationTrigger-only workflows should NOT have an ENTRYPOINT node when all branches are trigger-sourced."""
191191

192+
# GIVEN an IntegrationTrigger workflow where all branches are sourced from the trigger
192193
class SlackMessageTrigger(IntegrationTrigger):
193194
message: str
194195

@@ -203,39 +204,32 @@ class ProcessNode(BaseNode):
203204
class TestWorkflow(BaseWorkflow[BaseInputs, BaseState]):
204205
graph = SlackMessageTrigger >> ProcessNode
205206

207+
# WHEN we serialize the workflow
206208
result = get_workflow_display(workflow_class=TestWorkflow).serialize()
207209

208-
# Get trigger ID
210+
# THEN the trigger should be serialized
209211
triggers = result["triggers"]
210212
assert isinstance(triggers, list)
211213
assert len(triggers) == 1
212214
trigger = triggers[0]
213215
assert isinstance(trigger, dict)
214216
trigger_id = trigger["id"]
215217

216-
# Verify ENTRYPOINT node exists
218+
# AND there should be NO ENTRYPOINT node (all branches are trigger-sourced)
217219
workflow_raw_data = result["workflow_raw_data"]
218220
assert isinstance(workflow_raw_data, dict)
219221
nodes = workflow_raw_data["nodes"]
220222
assert isinstance(nodes, list)
221223
entrypoint_nodes = [n for n in nodes if isinstance(n, dict) and n.get("type") == "ENTRYPOINT"]
222-
assert len(entrypoint_nodes) == 1, "IntegrationTrigger workflows should have an ENTRYPOINT node"
223-
224-
entrypoint_node = entrypoint_nodes[0]
225-
assert isinstance(entrypoint_node, dict)
226-
entrypoint_node_id = entrypoint_node["id"]
224+
assert len(entrypoint_nodes) == 0, "IntegrationTrigger-only workflows should NOT have an ENTRYPOINT node"
227225

226+
# AND edges should use trigger ID as source_node_id
228227
edges = workflow_raw_data["edges"]
229228
assert isinstance(edges, list)
230-
entrypoint_edges = [e for e in edges if isinstance(e, dict) and e.get("source_node_id") == entrypoint_node_id]
231-
assert len(entrypoint_edges) == 0
232-
233-
# Verify edges use trigger ID as sourceNodeId (not ENTRYPOINT)
234229
trigger_edges = [e for e in edges if isinstance(e, dict) and e.get("source_node_id") == trigger_id]
235230
assert len(trigger_edges) > 0, "Should have edges from trigger ID"
236231

237-
# Verify the edge connects trigger to first node
238-
# ProcessNode should be the only non-terminal, non-entrypoint node
232+
# AND the edge should connect trigger to the process node
239233
process_nodes = [n for n in nodes if isinstance(n, dict) and n.get("type") not in ("TERMINAL", "ENTRYPOINT")]
240234
assert len(process_nodes) > 0, "Should have at least one process node"
241235
process_node = process_nodes[0]

ee/vellum_ee/workflows/display/tests/workflow_serialization/test_integration_trigger_with_entrypoint_node_id.py

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,13 @@
1616
def test_integration_trigger_with_explicit_entrypoint_node_id():
1717
"""
1818
Tests that a workflow with an IntegrationTrigger and explicit entrypoint_node_id
19-
creates an ENTRYPOINT node using the explicit ID (not the trigger's ID).
19+
does NOT create an ENTRYPOINT node when all branches are trigger-sourced.
20+
21+
Even with explicit IDs provided, if all graph branches originate from triggers,
22+
the ENTRYPOINT node should be skipped.
2023
"""
2124

25+
# GIVEN an IntegrationTrigger workflow with explicit entrypoint_node_id
2226
class SlackMessageTrigger(IntegrationTrigger):
2327
message: str
2428

@@ -53,8 +57,10 @@ class TestWorkflowDisplay(BaseWorkflowDisplay[TestWorkflow]):
5357
)
5458
}
5559

60+
# WHEN we serialize the workflow
5661
result: dict = get_workflow_display(workflow_class=TestWorkflow).serialize()
5762

63+
# THEN the trigger should be serialized
5864
assert "workflow_raw_data" in result
5965
workflow_raw_data = result["workflow_raw_data"]
6066
assert isinstance(workflow_raw_data, dict)
@@ -67,22 +73,19 @@ class TestWorkflowDisplay(BaseWorkflowDisplay[TestWorkflow]):
6773
assert isinstance(trigger, dict)
6874
trigger_id = trigger["id"]
6975

76+
# AND there should be NO ENTRYPOINT node (all branches are trigger-sourced)
7077
nodes = workflow_raw_data["nodes"]
7178
assert isinstance(nodes, list)
7279

7380
entrypoint_nodes = [n for n in nodes if isinstance(n, dict) and n.get("type") == "ENTRYPOINT"]
7481

75-
assert (
76-
len(entrypoint_nodes) == 1
77-
), "IntegrationTrigger workflows with explicit entrypoint_node_id should create an ENTRYPOINT node"
78-
79-
entrypoint_node = entrypoint_nodes[0]
80-
assert isinstance(entrypoint_node, dict)
81-
entrypoint_node_id = entrypoint_node["id"]
82-
83-
assert entrypoint_node_id == str(explicit_entrypoint_node_id), (
84-
f"Entrypoint node ID should be {explicit_entrypoint_node_id} from workflow_display, "
85-
f"not {trigger_id} from trigger"
82+
assert len(entrypoint_nodes) == 0, (
83+
"IntegrationTrigger-only workflows should NOT create an ENTRYPOINT node "
84+
"even with explicit entrypoint_node_id, since all branches are trigger-sourced"
8685
)
8786

88-
assert entrypoint_node_id != trigger_id, "Entrypoint node should not use the trigger's ID"
87+
# AND edges should use trigger ID as source_node_id
88+
edges = workflow_raw_data["edges"]
89+
assert isinstance(edges, list)
90+
trigger_edges = [e for e in edges if isinstance(e, dict) and e.get("source_node_id") == trigger_id]
91+
assert len(trigger_edges) > 0, "Should have edges from trigger ID"

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

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ def serialize(self) -> JsonObject:
250250
has_manual_trigger = len(manual_trigger_edges) > 0
251251

252252
# Determine which nodes have explicit non-trigger entrypoints in the graph
253-
# This is used later to decide whether to skip entrypoint edges for nodes with triggers
253+
# This is used to decide whether to create an ENTRYPOINT node and skip entrypoint edges
254254
non_trigger_entrypoint_nodes: Set[Type[BaseNode]] = set()
255255
for subgraph in self._workflow.get_subgraphs():
256256
if any(True for _ in subgraph.trigger_edges):
@@ -261,6 +261,14 @@ def serialize(self) -> JsonObject:
261261
except Exception:
262262
continue
263263

264+
# Determine if we need an ENTRYPOINT node:
265+
# - ManualTrigger: always need ENTRYPOINT (backward compatibility)
266+
# - No triggers: always need ENTRYPOINT (traditional workflows)
267+
# - Non-trigger entrypoints exist: need ENTRYPOINT for those branches
268+
# - Only non-manual triggers with no regular entrypoints: skip ENTRYPOINT
269+
has_triggers = len(trigger_edges) > 0
270+
needs_entrypoint_node = has_manual_trigger or not has_triggers or len(non_trigger_entrypoint_nodes) > 0
271+
264272
entrypoint_node_id: Optional[UUID] = None
265273
entrypoint_node_source_handle_id: Optional[UUID] = None
266274
entrypoint_node_display = self.display_context.workflow_display.entrypoint_node_display
@@ -284,9 +292,8 @@ def serialize(self) -> JsonObject:
284292
"base": None,
285293
"definition": None,
286294
}
287-
else:
288-
# Non-manual trigger or no triggers: use workflow_display ENTRYPOINT node if IDs are present
289-
# This allows the ENTRYPOINT node to be optional - if IDs are None, no ENTRYPOINT node is created
295+
elif needs_entrypoint_node:
296+
# No triggers or non-trigger entrypoints exist: use workflow_display ENTRYPOINT node
290297
entrypoint_node_id = self.display_context.workflow_display.entrypoint_node_id
291298
entrypoint_node_source_handle_id = self.display_context.workflow_display.entrypoint_node_source_handle_id
292299

@@ -304,6 +311,7 @@ def serialize(self) -> JsonObject:
304311
"base": None,
305312
"definition": None,
306313
}
314+
# else: only non-manual triggers with no regular entrypoints - skip ENTRYPOINT node
307315

308316
# Add all the nodes in the workflows
309317
for node in self._workflow.get_all_nodes():
@@ -991,16 +999,33 @@ def display_context(self) -> WorkflowDisplayContext:
991999
)
9921000

9931001
def _generate_workflow_meta_display(self) -> WorkflowMetaDisplay:
1002+
defaults = WorkflowMetaDisplay.get_default(self._workflow)
9941003
overrides = self.workflow_display
995-
if overrides:
996-
return WorkflowMetaDisplay(
997-
entrypoint_node_id=overrides.entrypoint_node_id,
998-
entrypoint_node_source_handle_id=overrides.entrypoint_node_source_handle_id,
999-
entrypoint_node_display=overrides.entrypoint_node_display,
1000-
display_data=overrides.display_data,
1001-
)
10021004

1003-
return WorkflowMetaDisplay.get_default(self._workflow)
1005+
if not overrides:
1006+
return defaults
1007+
1008+
# Merge overrides with defaults - if override provides None, fall back to default
1009+
entrypoint_node_id = (
1010+
overrides.entrypoint_node_id if overrides.entrypoint_node_id is not None else defaults.entrypoint_node_id
1011+
)
1012+
entrypoint_node_source_handle_id = (
1013+
overrides.entrypoint_node_source_handle_id
1014+
if overrides.entrypoint_node_source_handle_id is not None
1015+
else defaults.entrypoint_node_source_handle_id
1016+
)
1017+
entrypoint_node_display = (
1018+
overrides.entrypoint_node_display
1019+
if overrides.entrypoint_node_display is not None
1020+
else defaults.entrypoint_node_display
1021+
)
1022+
1023+
return WorkflowMetaDisplay(
1024+
entrypoint_node_id=entrypoint_node_id,
1025+
entrypoint_node_source_handle_id=entrypoint_node_source_handle_id,
1026+
entrypoint_node_display=entrypoint_node_display,
1027+
display_data=overrides.display_data,
1028+
)
10041029

10051030
def _generate_workflow_input_display(
10061031
self, workflow_input: WorkflowInputReference, overrides: Optional[WorkflowInputsDisplay] = None

0 commit comments

Comments
 (0)