vellum-ai 1.7.4__py3-none-any.whl → 1.7.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. vellum/__init__.py +2 -0
  2. vellum/client/core/client_wrapper.py +2 -2
  3. vellum/client/reference.md +95 -0
  4. vellum/client/resources/workflow_deployments/client.py +111 -0
  5. vellum/client/resources/workflow_deployments/raw_client.py +121 -0
  6. vellum/client/types/__init__.py +2 -0
  7. vellum/client/types/paginated_workflow_deployment_release_list.py +30 -0
  8. vellum/client/types/vellum_error_code_enum.py +2 -0
  9. vellum/client/types/vellum_sdk_error_code_enum.py +2 -0
  10. vellum/client/types/workflow_execution_event_error_code.py +2 -0
  11. vellum/types/paginated_workflow_deployment_release_list.py +3 -0
  12. vellum/workflows/edges/__init__.py +2 -0
  13. vellum/workflows/edges/trigger_edge.py +67 -0
  14. vellum/workflows/events/tests/test_event.py +40 -0
  15. vellum/workflows/events/workflow.py +15 -3
  16. vellum/workflows/graph/graph.py +93 -0
  17. vellum/workflows/graph/tests/test_graph.py +167 -0
  18. vellum/workflows/nodes/displayable/tool_calling_node/node.py +1 -1
  19. vellum/workflows/nodes/displayable/tool_calling_node/utils.py +1 -1
  20. vellum/workflows/ports/port.py +11 -0
  21. vellum/workflows/runner/runner.py +5 -3
  22. vellum/workflows/triggers/__init__.py +4 -0
  23. vellum/workflows/triggers/base.py +125 -0
  24. vellum/workflows/triggers/manual.py +37 -0
  25. vellum/workflows/workflows/base.py +9 -9
  26. {vellum_ai-1.7.4.dist-info → vellum_ai-1.7.5.dist-info}/METADATA +1 -1
  27. {vellum_ai-1.7.4.dist-info → vellum_ai-1.7.5.dist-info}/RECORD +36 -29
  28. vellum_ee/assets/node-definitions.json +1 -1
  29. vellum_ee/workflows/display/base.py +26 -1
  30. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_inline_workflow_serialization.py +1 -1
  31. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_tool_calling_node_serialization.py +1 -1
  32. vellum_ee/workflows/display/tests/workflow_serialization/test_manual_trigger_serialization.py +113 -0
  33. vellum_ee/workflows/display/workflows/base_workflow_display.py +63 -10
  34. {vellum_ai-1.7.4.dist-info → vellum_ai-1.7.5.dist-info}/LICENSE +0 -0
  35. {vellum_ai-1.7.4.dist-info → vellum_ai-1.7.5.dist-info}/WHEEL +0 -0
  36. {vellum_ai-1.7.4.dist-info → vellum_ai-1.7.5.dist-info}/entry_points.txt +0 -0
@@ -3,11 +3,13 @@ from typing import TYPE_CHECKING, Iterator, List, Optional, Set, Type, Union
3
3
  from orderly_set import OrderedSet
4
4
 
5
5
  from vellum.workflows.edges.edge import Edge
6
+ from vellum.workflows.edges.trigger_edge import TriggerEdge
6
7
  from vellum.workflows.types.generics import NodeType
7
8
 
8
9
  if TYPE_CHECKING:
9
10
  from vellum.workflows.nodes.bases.base import BaseNode
10
11
  from vellum.workflows.ports.port import Port
12
+ from vellum.workflows.triggers.base import BaseTrigger
11
13
 
12
14
 
13
15
  class NoPortsNode:
@@ -46,16 +48,19 @@ class Graph:
46
48
  _entrypoints: Set[Union["Port", "NoPortsNode"]]
47
49
  _edges: List[Edge]
48
50
  _terminals: Set[Union["Port", "NoPortsNode"]]
51
+ _trigger_edges: List[TriggerEdge]
49
52
 
50
53
  def __init__(
51
54
  self,
52
55
  entrypoints: Set[Union["Port", "NoPortsNode"]],
53
56
  edges: List[Edge],
54
57
  terminals: Set[Union["Port", "NoPortsNode"]],
58
+ trigger_edges: Optional[List[TriggerEdge]] = None,
55
59
  ):
56
60
  self._edges = edges
57
61
  self._entrypoints = entrypoints
58
62
  self._terminals = terminals
63
+ self._trigger_edges = trigger_edges or []
59
64
 
60
65
  @staticmethod
61
66
  def from_port(port: "Port") -> "Graph":
@@ -96,12 +101,79 @@ class Graph:
96
101
  def from_edge(edge: Edge) -> "Graph":
97
102
  return Graph(entrypoints={edge.from_port}, edges=[edge], terminals={port for port in edge.to_node.Ports})
98
103
 
104
+ @staticmethod
105
+ def from_trigger_edge(edge: TriggerEdge) -> "Graph":
106
+ """
107
+ Create a graph from a single TriggerEdge (Trigger >> Node).
108
+
109
+ Args:
110
+ edge: TriggerEdge connecting a trigger to a node
111
+
112
+ Returns:
113
+ Graph with the trigger edge and the target node's ports as terminals
114
+ """
115
+ ports = {port for port in edge.to_node.Ports}
116
+ if not ports:
117
+ no_ports_node = NoPortsNode(edge.to_node)
118
+ return Graph(
119
+ entrypoints={no_ports_node},
120
+ edges=[],
121
+ terminals={no_ports_node},
122
+ trigger_edges=[edge],
123
+ )
124
+ return Graph(
125
+ entrypoints=set(ports),
126
+ edges=[],
127
+ terminals=set(ports),
128
+ trigger_edges=[edge],
129
+ )
130
+
131
+ @staticmethod
132
+ def from_trigger_edges(edges: List[TriggerEdge]) -> "Graph":
133
+ """
134
+ Create a graph from multiple TriggerEdges (e.g., Trigger >> {NodeA, NodeB}).
135
+
136
+ Args:
137
+ edges: List of TriggerEdges
138
+
139
+ Returns:
140
+ Graph with all trigger edges and target nodes' ports as entrypoints/terminals
141
+ """
142
+ entrypoints: Set[Union["Port", NoPortsNode]] = set()
143
+ terminals: Set[Union["Port", NoPortsNode]] = set()
144
+
145
+ for edge in edges:
146
+ ports = {port for port in edge.to_node.Ports}
147
+ if not ports:
148
+ no_ports_node = NoPortsNode(edge.to_node)
149
+ entrypoints.add(no_ports_node)
150
+ terminals.add(no_ports_node)
151
+ else:
152
+ entrypoints.update(ports)
153
+ terminals.update(ports)
154
+
155
+ return Graph(
156
+ entrypoints=entrypoints,
157
+ edges=[],
158
+ terminals=terminals,
159
+ trigger_edges=edges,
160
+ )
161
+
99
162
  @staticmethod
100
163
  def empty() -> "Graph":
101
164
  """Create an empty graph with no entrypoints, edges, or terminals."""
102
165
  return Graph(entrypoints=set(), edges=[], terminals=set())
103
166
 
104
167
  def __rshift__(self, other: GraphTarget) -> "Graph":
168
+ # Check for trigger target (class-level only)
169
+ from vellum.workflows.triggers.base import BaseTrigger
170
+
171
+ if isinstance(other, type) and issubclass(other, BaseTrigger):
172
+ raise TypeError(
173
+ f"Cannot create edge targeting trigger {other.__name__}. "
174
+ f"Triggers must be at the start of a graph path, not as targets."
175
+ )
176
+
105
177
  if not self._edges and not self._entrypoints:
106
178
  raise ValueError("Graph instance can only create new edges from nodes within existing edges")
107
179
 
@@ -179,9 +251,30 @@ class Graph:
179
251
  def edges(self) -> Iterator[Edge]:
180
252
  return iter(self._edges)
181
253
 
254
+ @property
255
+ def trigger_edges(self) -> Iterator[TriggerEdge]:
256
+ """Get all trigger edges in this graph."""
257
+ return iter(self._trigger_edges)
258
+
259
+ @property
260
+ def triggers(self) -> Iterator[Type["BaseTrigger"]]:
261
+ """Get all unique trigger classes in this graph."""
262
+ seen_triggers = set()
263
+ for trigger_edge in self._trigger_edges:
264
+ if trigger_edge.trigger_class not in seen_triggers:
265
+ seen_triggers.add(trigger_edge.trigger_class)
266
+ yield trigger_edge.trigger_class
267
+
182
268
  @property
183
269
  def nodes(self) -> Iterator[Type["BaseNode"]]:
184
270
  nodes = set()
271
+
272
+ # Include nodes from trigger edges
273
+ for trigger_edge in self._trigger_edges:
274
+ if trigger_edge.to_node not in nodes:
275
+ nodes.add(trigger_edge.to_node)
276
+ yield trigger_edge.to_node
277
+
185
278
  if not self._edges:
186
279
  for node in self.entrypoints:
187
280
  if node not in nodes:
@@ -1,7 +1,10 @@
1
+ import pytest
2
+
1
3
  from vellum.workflows.edges.edge import Edge
2
4
  from vellum.workflows.graph.graph import Graph
3
5
  from vellum.workflows.nodes.bases.base import BaseNode
4
6
  from vellum.workflows.ports.port import Port
7
+ from vellum.workflows.triggers import ManualTrigger
5
8
 
6
9
 
7
10
  def test_graph__empty():
@@ -617,3 +620,167 @@ def test_graph__from_node_with_empty_ports():
617
620
 
618
621
  # THEN the graph should have exactly 1 node
619
622
  assert len(list(graph.nodes)) == 1
623
+
624
+
625
+ def test_graph__manual_trigger_to_node():
626
+ # GIVEN a node
627
+ class MyNode(BaseNode):
628
+ pass
629
+
630
+ # WHEN we create graph with ManualTrigger >> Node (class-level, no instantiation)
631
+ graph = ManualTrigger >> MyNode
632
+
633
+ # THEN the graph has one trigger edge
634
+ trigger_edges = list(graph.trigger_edges)
635
+ assert len(trigger_edges) == 1
636
+ assert trigger_edges[0].trigger_class == ManualTrigger
637
+ assert trigger_edges[0].to_node == MyNode
638
+
639
+ # AND the graph has one trigger
640
+ triggers = list(graph.triggers)
641
+ assert len(triggers) == 1
642
+ assert triggers[0] == ManualTrigger
643
+
644
+ # AND the graph has one node
645
+ assert len(list(graph.nodes)) == 1
646
+ assert MyNode in list(graph.nodes)
647
+
648
+
649
+ def test_graph__manual_trigger_to_set_of_nodes():
650
+ # GIVEN two nodes
651
+ class NodeA(BaseNode):
652
+ pass
653
+
654
+ class NodeB(BaseNode):
655
+ pass
656
+
657
+ # WHEN we create graph with ManualTrigger >> {NodeA, NodeB}
658
+ graph = ManualTrigger >> {NodeA, NodeB}
659
+
660
+ # THEN the graph has two trigger edges
661
+ trigger_edges = list(graph.trigger_edges)
662
+ assert len(trigger_edges) == 2
663
+
664
+ # AND both edges connect to the same ManualTrigger class
665
+ assert all(edge.trigger_class == ManualTrigger for edge in trigger_edges)
666
+
667
+ # AND edges connect to both nodes
668
+ target_nodes = {edge.to_node for edge in trigger_edges}
669
+ assert target_nodes == {NodeA, NodeB}
670
+
671
+ # AND the graph has one unique trigger
672
+ triggers = list(graph.triggers)
673
+ assert len(triggers) == 1
674
+
675
+ # AND the graph has two nodes
676
+ assert len(list(graph.nodes)) == 2
677
+
678
+
679
+ def test_graph__manual_trigger_to_graph():
680
+ # GIVEN a graph of nodes
681
+ class NodeA(BaseNode):
682
+ pass
683
+
684
+ class NodeB(BaseNode):
685
+ pass
686
+
687
+ node_graph = NodeA >> NodeB
688
+
689
+ # WHEN we create graph with ManualTrigger >> Graph
690
+ graph = ManualTrigger >> node_graph
691
+
692
+ # THEN the graph has a trigger edge to the entrypoint
693
+ trigger_edges = list(graph.trigger_edges)
694
+ assert len(trigger_edges) == 1
695
+ assert trigger_edges[0].to_node == NodeA
696
+
697
+ # AND the graph preserves the original edges
698
+ edges = list(graph.edges)
699
+ assert len(edges) == 1
700
+ assert edges[0].to_node == NodeB
701
+
702
+ # AND the graph has both nodes
703
+ nodes = list(graph.nodes)
704
+ assert len(nodes) == 2
705
+ assert NodeA in nodes
706
+ assert NodeB in nodes
707
+
708
+
709
+ def test_graph__manual_trigger_to_set_of_graphs_preserves_edges():
710
+ # GIVEN two graphs of nodes
711
+ class NodeA(BaseNode):
712
+ pass
713
+
714
+ class NodeB(BaseNode):
715
+ pass
716
+
717
+ class NodeC(BaseNode):
718
+ pass
719
+
720
+ class NodeD(BaseNode):
721
+ pass
722
+
723
+ graph_one = NodeA >> NodeB
724
+ graph_two = NodeC >> NodeD
725
+
726
+ # WHEN we create a graph with ManualTrigger >> {Graph1, Graph2}
727
+ combined_graph = ManualTrigger >> {graph_one, graph_two}
728
+
729
+ # THEN the combined graph has trigger edges to both entrypoints
730
+ trigger_edges = list(combined_graph.trigger_edges)
731
+ assert len(trigger_edges) == 2
732
+ assert {edge.to_node for edge in trigger_edges} == {NodeA, NodeC}
733
+
734
+ # AND the combined graph preserves all downstream edges
735
+ edges = list(combined_graph.edges)
736
+ assert len(edges) == 2
737
+ assert {(edge.from_port.node_class, edge.to_node) for edge in edges} == {
738
+ (NodeA, NodeB),
739
+ (NodeC, NodeD),
740
+ }
741
+
742
+ # AND the combined graph still exposes all nodes
743
+ nodes = list(combined_graph.nodes)
744
+ assert {NodeA, NodeB, NodeC, NodeD}.issubset(nodes)
745
+
746
+
747
+ def test_graph__node_to_trigger_raises():
748
+ # GIVEN a node and trigger
749
+ class MyNode(BaseNode):
750
+ pass
751
+
752
+ # WHEN we try to create Node >> Trigger (class-level)
753
+ # THEN it raises TypeError
754
+ with pytest.raises(TypeError, match="Cannot create edge targeting trigger"):
755
+ MyNode >> ManualTrigger
756
+
757
+ # WHEN we try to create Node >> Trigger (instance-level)
758
+ # THEN it also raises TypeError
759
+ with pytest.raises(TypeError, match="Cannot create edge targeting trigger"):
760
+ MyNode >> ManualTrigger
761
+
762
+
763
+ def test_graph__trigger_then_graph_then_node():
764
+ # GIVEN a trigger, a node, and another node
765
+ class StartNode(BaseNode):
766
+ pass
767
+
768
+ class EndNode(BaseNode):
769
+ pass
770
+
771
+ # WHEN we create Trigger >> Node >> Node
772
+ graph = ManualTrigger >> StartNode >> EndNode
773
+
774
+ # THEN the graph has one trigger edge
775
+ trigger_edges = list(graph.trigger_edges)
776
+ assert len(trigger_edges) == 1
777
+ assert trigger_edges[0].to_node == StartNode
778
+
779
+ # AND the graph has one regular edge
780
+ edges = list(graph.edges)
781
+ assert len(edges) == 1
782
+ assert edges[0].to_node == EndNode
783
+
784
+ # AND the graph has both nodes
785
+ nodes = list(graph.nodes)
786
+ assert len(nodes) == 2
@@ -47,7 +47,7 @@ class ToolCallingNode(BaseNode[StateType], Generic[StateType]):
47
47
  functions: ClassVar[List[Tool]] = []
48
48
  prompt_inputs: ClassVar[Optional[EntityInputsInterface]] = None
49
49
  parameters: PromptParameters = DEFAULT_PROMPT_PARAMETERS
50
- max_prompt_iterations: ClassVar[Optional[int]] = 5
50
+ max_prompt_iterations: ClassVar[Optional[int]] = 25
51
51
  settings: ClassVar[Optional[Union[PromptSettings, Dict[str, Any]]]] = None
52
52
 
53
53
  class Outputs(BaseOutputs):
@@ -89,7 +89,7 @@ class FunctionCallNodeMixin:
89
89
 
90
90
 
91
91
  class ToolPromptNode(InlinePromptNode[ToolCallingState]):
92
- max_prompt_iterations: Optional[int] = 5
92
+ max_prompt_iterations: Optional[int] = 25
93
93
 
94
94
  class Trigger(InlinePromptNode.Trigger):
95
95
  merge_behavior = MergeBehavior.AWAIT_ATTRIBUTES
@@ -61,6 +61,17 @@ class Port:
61
61
  return iter(self._edges)
62
62
 
63
63
  def __rshift__(self, other: GraphTarget) -> Graph:
64
+ # Check for trigger target (class-level only)
65
+ from vellum.workflows.triggers.base import BaseTrigger
66
+
67
+ # Check if other is a trigger class
68
+ if isinstance(other, type) and issubclass(other, BaseTrigger):
69
+ raise TypeError(
70
+ f"Cannot create edge targeting trigger {other.__name__}. "
71
+ f"Triggers must be at the start of a graph path, not as targets. "
72
+ f"Did you mean: {other.__name__} >> {self.node_class.__name__}?"
73
+ )
74
+
64
75
  if isinstance(other, set) or isinstance(other, Graph):
65
76
  return Graph.from_port(self) >> other
66
77
 
@@ -74,6 +74,7 @@ from vellum.workflows.references import ExternalInputReference, OutputReference
74
74
  from vellum.workflows.references.state_value import StateValueReference
75
75
  from vellum.workflows.state.base import BaseState
76
76
  from vellum.workflows.state.delta import StateDelta
77
+ from vellum.workflows.types.core import CancelSignal
77
78
  from vellum.workflows.types.generics import InputsType, OutputsType, StateType
78
79
 
79
80
  if TYPE_CHECKING:
@@ -103,7 +104,7 @@ class WorkflowRunner(Generic[StateType]):
103
104
  entrypoint_nodes: Optional[RunFromNodeArg] = None,
104
105
  external_inputs: Optional[ExternalInputsArg] = None,
105
106
  previous_execution_id: Optional[Union[str, UUID]] = None,
106
- cancel_signal: Optional[ThreadingEvent] = None,
107
+ cancel_signal: Optional[CancelSignal] = None,
107
108
  node_output_mocks: Optional[MockNodeExecutionArg] = None,
108
109
  max_concurrency: Optional[int] = None,
109
110
  init_execution_context: Optional[ExecutionContext] = None,
@@ -799,13 +800,14 @@ class WorkflowRunner(Generic[StateType]):
799
800
  parent=self._execution_context.parent_context,
800
801
  )
801
802
 
802
- def _fulfill_workflow_event(self, outputs: OutputsType) -> WorkflowExecutionFulfilledEvent:
803
+ def _fulfill_workflow_event(self, outputs: OutputsType, final_state: StateType) -> WorkflowExecutionFulfilledEvent:
803
804
  return WorkflowExecutionFulfilledEvent(
804
805
  trace_id=self._execution_context.trace_id,
805
806
  span_id=self._initial_state.meta.span_id,
806
807
  body=WorkflowExecutionFulfilledBody(
807
808
  workflow_definition=self.workflow.__class__,
808
809
  outputs=outputs,
810
+ final_state=final_state,
809
811
  ),
810
812
  parent=self._execution_context.parent_context,
811
813
  )
@@ -961,7 +963,7 @@ class WorkflowRunner(Generic[StateType]):
961
963
  descriptor.instance.resolve(final_state),
962
964
  )
963
965
 
964
- self._workflow_event_outer_queue.put(self._fulfill_workflow_event(fulfilled_outputs))
966
+ self._workflow_event_outer_queue.put(self._fulfill_workflow_event(fulfilled_outputs, final_state))
965
967
 
966
968
  def _run_background_thread(self) -> None:
967
969
  state_class = self.workflow.get_state_class()
@@ -0,0 +1,4 @@
1
+ from vellum.workflows.triggers.base import BaseTrigger
2
+ from vellum.workflows.triggers.manual import ManualTrigger
3
+
4
+ __all__ = ["BaseTrigger", "ManualTrigger"]
@@ -0,0 +1,125 @@
1
+ from abc import ABC, ABCMeta
2
+ from typing import TYPE_CHECKING, Any, Type, cast
3
+
4
+ if TYPE_CHECKING:
5
+ from vellum.workflows.graph.graph import Graph, GraphTarget
6
+
7
+
8
+ class BaseTriggerMeta(ABCMeta):
9
+ """
10
+ Metaclass for BaseTrigger that enables class-level >> operator.
11
+
12
+ This allows triggers to be used at the class level, similar to nodes:
13
+ ManualTrigger >> MyNode # Class-level, no instantiation
14
+ """
15
+
16
+ def __rshift__(cls, other: "GraphTarget") -> "Graph": # type: ignore[misc]
17
+ """
18
+ Enable Trigger class >> Node syntax (class-level only).
19
+
20
+ Args:
21
+ other: The target to connect to - can be a Node, Graph, or set of Nodes
22
+
23
+ Returns:
24
+ Graph: A graph object with trigger edges
25
+
26
+ Examples:
27
+ ManualTrigger >> MyNode
28
+ ManualTrigger >> {NodeA, NodeB}
29
+ ManualTrigger >> (NodeA >> NodeB)
30
+ """
31
+ from vellum.workflows.edges.trigger_edge import TriggerEdge
32
+ from vellum.workflows.graph.graph import Graph
33
+ from vellum.workflows.nodes.bases.base import BaseNode as BaseNodeClass
34
+
35
+ # Cast cls to the proper type for TriggerEdge
36
+ trigger_cls = cast("Type[BaseTrigger]", cls)
37
+
38
+ if isinstance(other, set):
39
+ # Trigger >> {NodeA, NodeB}
40
+ trigger_edges = []
41
+ graph_items = []
42
+ for item in other:
43
+ if isinstance(item, type) and issubclass(item, BaseNodeClass):
44
+ trigger_edges.append(TriggerEdge(trigger_cls, item))
45
+ elif isinstance(item, Graph):
46
+ # Trigger >> {Graph1, Graph2}
47
+ graph_items.append(item)
48
+ for entrypoint in item.entrypoints:
49
+ trigger_edges.append(TriggerEdge(trigger_cls, entrypoint))
50
+ else:
51
+ raise TypeError(
52
+ f"Cannot connect trigger to {type(item).__name__}. " f"Expected BaseNode or Graph in set."
53
+ )
54
+
55
+ result_graph = Graph.from_trigger_edges(trigger_edges)
56
+
57
+ for graph_item in graph_items:
58
+ result_graph._extend_edges(graph_item.edges)
59
+ result_graph._terminals.update(graph_item._terminals)
60
+ for existing_trigger_edge in graph_item._trigger_edges:
61
+ if existing_trigger_edge not in result_graph._trigger_edges:
62
+ result_graph._trigger_edges.append(existing_trigger_edge)
63
+
64
+ return result_graph
65
+
66
+ elif isinstance(other, Graph):
67
+ # Trigger >> Graph
68
+ edges = [TriggerEdge(trigger_cls, entrypoint) for entrypoint in other.entrypoints]
69
+ result_graph = Graph.from_trigger_edges(edges)
70
+ # Also include the edges from the original graph
71
+ result_graph._extend_edges(other.edges)
72
+ result_graph._terminals = other._terminals
73
+ return result_graph
74
+
75
+ elif isinstance(other, type) and issubclass(other, BaseNodeClass):
76
+ # Trigger >> Node
77
+ edge = TriggerEdge(trigger_cls, other)
78
+ return Graph.from_trigger_edge(edge)
79
+
80
+ else:
81
+ raise TypeError(
82
+ f"Cannot connect trigger to {type(other).__name__}. " f"Expected BaseNode, Graph, or set of BaseNodes."
83
+ )
84
+
85
+ def __rrshift__(cls, other: Any) -> "Graph":
86
+ """
87
+ Prevent Node >> Trigger class syntax.
88
+
89
+ Raises:
90
+ TypeError: Always, as this operation is not allowed
91
+ """
92
+ raise TypeError(
93
+ f"Cannot create edge targeting trigger {cls.__name__}. "
94
+ f"Triggers must be at the start of a graph path, not as targets. "
95
+ f"Did you mean: {cls.__name__} >> {other.__name__ if hasattr(other, '__name__') else other}?"
96
+ )
97
+
98
+
99
+ class BaseTrigger(ABC, metaclass=BaseTriggerMeta):
100
+ """
101
+ Base class for workflow triggers - first-class graph elements.
102
+
103
+ Triggers define how and when a workflow execution is initiated. They are integrated
104
+ into the workflow graph using the >> operator and can connect to nodes at the class level.
105
+
106
+ Examples:
107
+ # Class-level usage (consistent with nodes)
108
+ ManualTrigger >> MyNode
109
+ ManualTrigger >> {NodeA, NodeB}
110
+ ManualTrigger >> (NodeA >> NodeB)
111
+
112
+ Subclass Hierarchy:
113
+ - ManualTrigger: Explicit workflow invocation (default)
114
+ - IntegrationTrigger: External service triggers (base for Slack, GitHub, etc.)
115
+ - ScheduledTrigger: Time-based triggers with cron/interval schedules
116
+
117
+ Important:
118
+ Triggers can only appear at the start of graph paths. Attempting to create
119
+ edges targeting triggers (Node >> Trigger) will raise a TypeError.
120
+
121
+ Note:
122
+ Like nodes, triggers work at the class level only. Do not instantiate triggers.
123
+ """
124
+
125
+ pass
@@ -0,0 +1,37 @@
1
+ from vellum.workflows.triggers.base import BaseTrigger
2
+
3
+
4
+ class ManualTrigger(BaseTrigger):
5
+ """
6
+ Default trigger representing explicit workflow invocation.
7
+
8
+ ManualTrigger is used when workflows are explicitly invoked via:
9
+ - workflow.run() method calls
10
+ - workflow.stream() method calls
11
+ - API calls to execute the workflow
12
+
13
+ This is the default trigger for all workflows. When no trigger is specified
14
+ in a workflow's graph definition, ManualTrigger is implicitly added.
15
+
16
+ Examples:
17
+ # Explicit ManualTrigger (equivalent to implicit)
18
+ class MyWorkflow(BaseWorkflow):
19
+ graph = ManualTrigger >> MyNode
20
+
21
+ # Implicit ManualTrigger (normalized to above)
22
+ class MyWorkflow(BaseWorkflow):
23
+ graph = MyNode
24
+
25
+ Characteristics:
26
+ - Provides no trigger-specific inputs
27
+ - Always ready to execute when invoked
28
+ - Simplest trigger type with no configuration
29
+ - Default behavior for backward compatibility
30
+
31
+ Comparison with other triggers:
32
+ - IntegrationTrigger: Responds to external events (webhooks, API calls)
33
+ - ScheduledTrigger: Executes based on time/schedule configuration
34
+ - ManualTrigger: Executes when explicitly called
35
+ """
36
+
37
+ pass
@@ -4,7 +4,6 @@ from functools import lru_cache
4
4
  import importlib
5
5
  import inspect
6
6
  import logging
7
- from threading import Event as ThreadingEvent
8
7
  from uuid import UUID, uuid4
9
8
  from typing import (
10
9
  Any,
@@ -76,6 +75,7 @@ from vellum.workflows.runner.runner import ExternalInputsArg, RunFromNodeArg
76
75
  from vellum.workflows.state.base import BaseState, StateMeta
77
76
  from vellum.workflows.state.context import WorkflowContext
78
77
  from vellum.workflows.state.store import Store
78
+ from vellum.workflows.types import CancelSignal
79
79
  from vellum.workflows.types.generics import InputsType, StateType
80
80
  from vellum.workflows.types.utils import get_original_base
81
81
  from vellum.workflows.utils.uuids import uuid4_from_hash
@@ -227,12 +227,12 @@ class BaseWorkflow(Generic[InputsType, StateType], BaseExecutable, metaclass=_Ba
227
227
  WorkflowEvent = Union[ # type: ignore
228
228
  GenericWorkflowEvent,
229
229
  WorkflowExecutionInitiatedEvent[InputsType, StateType], # type: ignore[valid-type]
230
- WorkflowExecutionFulfilledEvent[Outputs],
230
+ WorkflowExecutionFulfilledEvent[Outputs, StateType], # type: ignore[valid-type]
231
231
  WorkflowExecutionSnapshottedEvent[StateType], # type: ignore[valid-type]
232
232
  ]
233
233
 
234
234
  TerminalWorkflowEvent = Union[
235
- WorkflowExecutionFulfilledEvent[Outputs],
235
+ WorkflowExecutionFulfilledEvent[Outputs, StateType], # type: ignore[valid-type]
236
236
  WorkflowExecutionRejectedEvent,
237
237
  WorkflowExecutionPausedEvent,
238
238
  ]
@@ -374,7 +374,7 @@ class BaseWorkflow(Generic[InputsType, StateType], BaseExecutable, metaclass=_Ba
374
374
  entrypoint_nodes: Optional[RunFromNodeArg] = None,
375
375
  external_inputs: Optional[ExternalInputsArg] = None,
376
376
  previous_execution_id: Optional[Union[str, UUID]] = None,
377
- cancel_signal: Optional[ThreadingEvent] = None,
377
+ cancel_signal: Optional[CancelSignal] = None,
378
378
  node_output_mocks: Optional[MockNodeExecutionArg] = None,
379
379
  max_concurrency: Optional[int] = None,
380
380
  ) -> TerminalWorkflowEvent:
@@ -402,8 +402,8 @@ class BaseWorkflow(Generic[InputsType, StateType], BaseExecutable, metaclass=_Ba
402
402
  previous_execution_id: Optional[Union[str, UUID]] = None
403
403
  The execution ID of the previous execution to resume from.
404
404
 
405
- cancel_signal: Optional[ThreadingEvent] = None
406
- A threading event that can be used to cancel the Workflow Execution.
405
+ cancel_signal: Optional[CancelSignal] = None
406
+ A cancel signal that can be used to cancel the Workflow Execution.
407
407
 
408
408
  node_output_mocks: Optional[MockNodeExecutionArg] = None
409
409
  A list of Outputs to mock for Nodes during Workflow Execution. Each mock can include a `when_condition`
@@ -493,7 +493,7 @@ class BaseWorkflow(Generic[InputsType, StateType], BaseExecutable, metaclass=_Ba
493
493
  entrypoint_nodes: Optional[RunFromNodeArg] = None,
494
494
  external_inputs: Optional[ExternalInputsArg] = None,
495
495
  previous_execution_id: Optional[Union[str, UUID]] = None,
496
- cancel_signal: Optional[ThreadingEvent] = None,
496
+ cancel_signal: Optional[CancelSignal] = None,
497
497
  node_output_mocks: Optional[MockNodeExecutionArg] = None,
498
498
  max_concurrency: Optional[int] = None,
499
499
  ) -> WorkflowEventStream:
@@ -522,8 +522,8 @@ class BaseWorkflow(Generic[InputsType, StateType], BaseExecutable, metaclass=_Ba
522
522
  previous_execution_id: Optional[Union[str, UUID]] = None
523
523
  The execution ID of the previous execution to resume from.
524
524
 
525
- cancel_signal: Optional[ThreadingEvent] = None
526
- A threading event that can be used to cancel the Workflow Execution.
525
+ cancel_signal: Optional[CancelSignal] = None
526
+ A cancel signal that can be used to cancel the Workflow Execution.
527
527
 
528
528
  node_output_mocks: Optional[MockNodeExecutionArg] = None
529
529
  A list of Outputs to mock for Nodes during Workflow Execution. Each mock can include a `when_condition`
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: vellum-ai
3
- Version: 1.7.4
3
+ Version: 1.7.5
4
4
  Summary:
5
5
  License: MIT
6
6
  Requires-Python: >=3.9,<4.0