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