Branching with Ports and Expressions

This guide demonstrates the syntax for using Ports and Expressions to control the flow of execution in Vellum Workflows.

Basic Port Types

Below is a basic example:

1# nodes/my_prompt_node.py
2class MyNode(BaseNode):
3
4 # ... prompt node attributes go here ...
5
6 class Ports(BaseNode.Ports):
7 first_condition = Port.on_if(condition_expression)
8 second_condition = Port.on_elif(another_condition)
9 fallback = Port.on_else()
10
11# workflow.py
12class MyWorkflow(BaseWorkflow):
13 graph = {
14 # Can route to nodes or other Workflows
15 MyPromptNode.Ports.first_condition >> MyOtherNode,
16 MyPromptNode.Ports.second_condition >> AnotherNode1,
17 MyPromptNode.Ports.fallback >> AnotherNode2,
18 }
19
20 class Outputs(BaseWorkflow.Outputs):
21 final_output = MyOtherNode.Outputs.output

Prefer using Ports directly on Nodes rather than using legacy Conditional Nodes.

LazyReference for Self-Referencing

A common use case for Ports is to branch based on the result a node’s own outputs. For example, if a Prompt Node classifies text as “positive” or “negative”, you can use a Port to immediately branch based on the result.

In this case, the node needs to reference its own outputs in port conditions, use LazyReference to do so:

1from vellum.workflows.nodes.displayable import InlinePromptNode
2from vellum.workflows.ports import Port
3from vellum.workflows.references import LazyReference
4
5class SentimentAnalysisNode(InlinePromptNode):
6
7 # ... inline prompt node attributes go here ...
8
9 class Ports(InlinePromptNode.Ports):
10 # Self-referencing port condition
11 positive = Port.on_if(
12 LazyReference(lambda: SentimentAnalysisNode.Outputs.json["sentiment"].equals("positive"))
13 )
14 negative = Port.on_elif(
15 LazyReference(lambda: SentimentAnalysisNode.Outputs.json["sentiment"].equals("negative"))
16 )
17 else_port = Port.on_else()
18
19# workflow.py
20class MyWorkflow(BaseWorkflow):
21 graph = {
22 SentimentAnalysisNode.Ports.positive >> MyOtherNode,
23 SentimentAnalysisNode.Ports.negative >> AnotherNode1,
24 SentimentAnalysisNode.Ports.else_port >> MyOtherNode,
25 }
26
27 class Outputs(BaseWorkflow.Outputs):
28 final_output = MyOtherNode.Outputs.output

Expressions

Ports use Expressions to evaluate which Port to route to. Below is a list of all available expression operators.

Equality and Inequality

1# Basic equality
2Port.on_if(Inputs.category.equals("question"))
3Port.on_if(SomeNode.Outputs.status.does_not_equal("error"))
4
5# String comparisons
6Port.on_if(Inputs.text.contains("keyword"))
7Port.on_if(Inputs.text.does_not_contain("spam"))
8Port.on_if(Inputs.filename.begins_with("temp_"))
9Port.on_if(Inputs.filename.does_not_begin_with("system"))
10Port.on_if(Inputs.url.ends_with(".pdf"))
11Port.on_if(Inputs.url.does_not_end_with(".tmp"))

Numeric Comparisons

1# Numeric operators
2Port.on_if(Inputs.score.greater_than(0.8))
3Port.on_if(Inputs.count.less_than(100))
4Port.on_if(Inputs.rating.greater_than_or_equal_to(4.0))
5Port.on_if(Inputs.attempts.less_than_or_equal_to(3))
6
7# Range checks
8Port.on_if(Inputs.temperature.between(20, 30))
9Port.on_if(Inputs.age.not_between(13, 17))

Collection Operations

1# Membership testing
2Port.on_if(Inputs.status.in_(["active", "pending"]))
3Port.on_if(Inputs.category.not_in(["spam", "deleted"]))

Null and Undefined Checks

1# Null checks
2Port.on_if(Inputs.optional_field.is_null())
3Port.on_if(Inputs.required_field.is_not_null())
4
5# Nil checks (empty/blank values)
6Port.on_if(Inputs.description.is_nil())
7Port.on_if(Inputs.title.is_not_nil())
8
9# Undefined checks
10Port.on_if(Inputs.config_value.is_undefined())
11Port.on_if(Inputs.user_input.is_not_undefined())
12
13# Blank checks (empty strings, whitespace)
14Port.on_if(Inputs.comment.is_blank())
15Port.on_if(Inputs.name.is_not_blank())

Data Processing

1# JSON parsing
2Port.on_if(Inputs.json_string.parse_json())
3
4# Coalescing (fallback values)
5Port.on_if(Inputs.primary_value.coalesce(Inputs.fallback_value))

Working with JSON

Access JSON fields using bracket notation:

1# Access JSON object fields
2Port.on_if(PromptNode.Outputs.json["status"].equals("success"))
3Port.on_if(APINode.Outputs.response["data"]["count"].greater_than(10))
4
5# Combine with LazyReference for self-referencing
6Port.on_if(LazyReference(lambda: DataProcessorNode.Outputs.result)["confidence"].greater_than(0.8))
7
8# Check nested JSON values
9Port.on_if(Inputs.config["settings"]["enabled"].equals(True))
10Port.on_if(CodeNode.Outputs.analysis["metrics"]["accuracy"].between(0.8, 1.0))

Logical Operators

AND Operations

Use the & operator to combine conditions with AND logic:

1Port.on_if(
2 Inputs.category.equals("urgent")
3 & Inputs.priority.greater_than(5)
4)
5
6# Complex AND with parentheses
7Port.on_if(
8 Inputs.status.equals("active")
9 & (Inputs.verified.equals(True) & Inputs.premium.equals(True))
10)

OR Operations

Use the | operator to combine conditions with OR logic:

1Port.on_if(
2 Inputs.category.equals("error")
3 | Inputs.category.equals("warning")
4)
5
6# Mixed AND/OR with proper precedence
7Port.on_if(
8 Inputs.type.equals("admin")
9 & (Inputs.role.equals("owner") | Inputs.role.equals("manager"))
10)

Complex Logical Expressions

1# Parentheses control precedence
2Port.on_if(
3 (
4 Inputs.user_type.equals("premium")
5 & Inputs.subscription.equals("active")
6 )
7 | Inputs.admin_override.equals(True)
8)