From 20fe304021c669acdda7f353bc12046f7677b5ad Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 14 Nov 2025 19:15:53 +0000 Subject: [PATCH] Default to entrypoint trigger when no inputs provided When a workflow has exactly one IntegrationTrigger, no ManualTrigger, and no inputs are provided, automatically instantiate the trigger with empty kwargs and use it as the entrypoint. This allows workflows with integration triggers to run without explicitly providing a trigger parameter when no trigger data is needed. Key changes: - Modified WorkflowRunner._validate_no_trigger_provided to detect single-trigger case and auto-instantiate - Added default parameter value for inputs to maintain backward compatibility - Created RoutingOnlyWorkflow test fixture for workflows that don't reference trigger attributes - Added comprehensive tests for default entrypoint behavior - Updated existing test to reflect new behavior where workflows initialize successfully but may fail during execution if they reference missing trigger attributes Workflows that reference trigger attributes will still fail at runtime with a rejected event if no trigger data is provided, maintaining safety and type correctness. Co-Authored-By: harrison@vellum.ai --- src/vellum/workflows/runner/runner.py | 19 +++- .../tests/test_default_entrypoint.py | 98 +++++++++++++++++++ .../test_integration_trigger_execution.py | 15 +-- .../workflows/routing_only_workflow.py | 25 +++++ 4 files changed, 148 insertions(+), 9 deletions(-) create mode 100644 tests/workflows/integration_trigger_execution/tests/test_default_entrypoint.py create mode 100644 tests/workflows/integration_trigger_execution/workflows/routing_only_workflow.py diff --git a/src/vellum/workflows/runner/runner.py b/src/vellum/workflows/runner/runner.py index e0f58a0fc0..4970c14d67 100644 --- a/src/vellum/workflows/runner/runner.py +++ b/src/vellum/workflows/runner/runner.py @@ -240,7 +240,7 @@ def __init__( self._entrypoints = self.workflow.get_entrypoints() # Check if workflow requires a trigger but none was provided - self._validate_no_trigger_provided() + self._validate_no_trigger_provided(inputs) # This queue is responsible for sending events from WorkflowRunner to the outside world self._workflow_event_outer_queue: Queue[WorkflowEvent] = Queue() @@ -369,13 +369,19 @@ def _filter_entrypoints_for_trigger(self, trigger: BaseTrigger) -> None: if specific_entrypoints: self._entrypoints = specific_entrypoints - def _validate_no_trigger_provided(self) -> None: + def _validate_no_trigger_provided(self, inputs: Optional[InputsType] = None) -> None: """ Validate that workflow can run without a trigger. If workflow has IntegrationTrigger(s) but no ManualTrigger, it requires a trigger instance. If workflow has both, filter entrypoints to ManualTrigger path only. + Special case: If workflow has exactly one IntegrationTrigger, no ManualTrigger, and no inputs + are provided, automatically instantiate the trigger with empty kwargs and use it as the entrypoint. + + Args: + inputs: The inputs provided to the workflow run + Raises: WorkflowInitializationException: If workflow requires trigger but none was provided """ @@ -388,6 +394,15 @@ def _validate_no_trigger_provided(self) -> None: if workflow_integration_triggers: if not self._has_manual_trigger(): + # Special case: If exactly one IntegrationTrigger and no inputs provided, + if len(workflow_integration_triggers) == 1 and inputs is None: + trigger_class = workflow_integration_triggers[0] + default_trigger = trigger_class() + self._validate_and_bind_trigger(default_trigger) + self._filter_entrypoints_for_trigger(default_trigger) + self._trigger = default_trigger + return + # Workflow has ONLY IntegrationTrigger - this is an error raise WorkflowInitializationException( message="Workflow has IntegrationTrigger which requires trigger parameter", diff --git a/tests/workflows/integration_trigger_execution/tests/test_default_entrypoint.py b/tests/workflows/integration_trigger_execution/tests/test_default_entrypoint.py new file mode 100644 index 0000000000..f2e35b8254 --- /dev/null +++ b/tests/workflows/integration_trigger_execution/tests/test_default_entrypoint.py @@ -0,0 +1,98 @@ +"""Tests for default entrypoint behavior when no trigger is provided.""" + +from tests.workflows.integration_trigger_execution.nodes.slack_message_trigger import SlackMessageTrigger +from tests.workflows.integration_trigger_execution.workflows.multi_trigger_workflow import MultiTriggerWorkflow +from tests.workflows.integration_trigger_execution.workflows.routing_only_workflow import RoutingOnlyWorkflow +from tests.workflows.integration_trigger_execution.workflows.simple_workflow import SimpleSlackWorkflow + + +def test_single_trigger_no_inputs_defaults_to_entrypoint(): + """ + Tests that workflow with single IntegrationTrigger and no inputs defaults to entrypoint. + """ + + workflow = RoutingOnlyWorkflow() + + # WHEN we run the workflow without trigger and without inputs + result = workflow.run() + + # THEN it should execute successfully + assert result.name == "workflow.execution.fulfilled" + + assert result.outputs.result == "Workflow executed successfully" + + +def test_single_trigger_with_attribute_references_still_fails(): + """ + Tests that workflow referencing trigger attributes still fails without trigger data. + """ + + # GIVEN a workflow with SlackMessageTrigger that references trigger attributes + workflow = SimpleSlackWorkflow() + + # WHEN we run the workflow without trigger and without inputs + result = workflow.run() + + assert result.name == "workflow.execution.rejected" + + assert "Missing trigger attribute" in result.body.error.message + + +def test_multiple_triggers_no_inputs_uses_manual_path(): + """ + Tests that workflow with multiple triggers and no inputs uses ManualTrigger path. + """ + + # GIVEN a workflow with both ManualTrigger and IntegrationTrigger + workflow = MultiTriggerWorkflow() + + # WHEN we run the workflow without trigger and without inputs + result = workflow.run() + + # THEN it should execute successfully via ManualTrigger path (existing behavior) + assert result.name == "workflow.execution.fulfilled" + + assert result.outputs.manual_result == "Manual execution" + + +def test_explicit_trigger_param_works_unchanged(): + """ + Tests that providing explicit trigger parameter still works as before. + """ + + workflow = SimpleSlackWorkflow() + + # AND a valid Slack trigger instance + trigger = SlackMessageTrigger( + message="Explicit trigger test", + channel="C123456", + user="U789012", + ) + + # WHEN we run the workflow with the trigger + result = workflow.run(trigger=trigger) + + # THEN it should execute successfully + assert result.name == "workflow.execution.fulfilled" + + # AND the node should have access to trigger outputs + assert result.outputs.result == "Received 'Explicit trigger test' from channel C123456" + + +def test_single_trigger_no_inputs_stream_works(): + """ + Tests that workflow.stream() with single IntegrationTrigger and no inputs works. + """ + + workflow = RoutingOnlyWorkflow() + + events = list(workflow.stream()) + + # THEN we should get workflow events + assert len(events) > 0 + + # AND the final event should be fulfilled + last_event = events[-1] + assert last_event.name == "workflow.execution.fulfilled" + + assert last_event.outputs.result == "Workflow executed successfully" diff --git a/tests/workflows/integration_trigger_execution/tests/test_integration_trigger_execution.py b/tests/workflows/integration_trigger_execution/tests/test_integration_trigger_execution.py index a649b6e10d..202f8d826c 100644 --- a/tests/workflows/integration_trigger_execution/tests/test_integration_trigger_execution.py +++ b/tests/workflows/integration_trigger_execution/tests/test_integration_trigger_execution.py @@ -58,17 +58,18 @@ def test_stream_execution_with_trigger_event(): def test_error_when_trigger_event_missing(): - """Test that workflow raises error when IntegrationTrigger present but trigger missing.""" - # GIVEN a workflow with SlackMessageTrigger + """Test that workflow with IntegrationTrigger that references attributes fails without trigger data.""" + # GIVEN a workflow with SlackMessageTrigger that references trigger attributes workflow = SimpleSlackWorkflow() # WHEN we run the workflow without trigger - # THEN it should raise WorkflowInitializationException - with pytest.raises(WorkflowInitializationException) as exc_info: - workflow.run() + result = workflow.run() + + # THEN it should be rejected due to missing trigger attributes + assert result.name == "workflow.execution.rejected" - assert "IntegrationTrigger" in str(exc_info.value) - assert "trigger" in str(exc_info.value) + # AND the error should indicate missing trigger attribute + assert "Missing trigger attribute" in result.body.error.message def test_error_when_trigger_event_provided_but_no_integration_trigger(): diff --git a/tests/workflows/integration_trigger_execution/workflows/routing_only_workflow.py b/tests/workflows/integration_trigger_execution/workflows/routing_only_workflow.py new file mode 100644 index 0000000000..d176dbfc64 --- /dev/null +++ b/tests/workflows/integration_trigger_execution/workflows/routing_only_workflow.py @@ -0,0 +1,25 @@ +"""Workflow with IntegrationTrigger that doesn't reference trigger attributes.""" + +from vellum.workflows import BaseWorkflow +from vellum.workflows.nodes.bases import BaseNode + +from tests.workflows.integration_trigger_execution.nodes.slack_message_trigger import SlackMessageTrigger + + +class ConstantOutputNode(BaseNode): + """Node that returns a constant output without referencing trigger attributes.""" + + class Outputs(BaseNode.Outputs): + result: str + + def run(self) -> Outputs: + return self.Outputs(result="Workflow executed successfully") + + +class RoutingOnlyWorkflow(BaseWorkflow): + """Workflow with IntegrationTrigger used only for routing, not for data access.""" + + graph = SlackMessageTrigger >> ConstantOutputNode + + class Outputs(BaseWorkflow.Outputs): + result = ConstantOutputNode.Outputs.result