Skip to content

Routing & Guards

Routing and guard nodes control the flow of execution through your graphs. Routers direct requests to appropriate handlers based on conditions. Guards validate data and enforce constraints before processing continues.

RuleRouter evaluates conditions against envelope data and routes to the first matching target.

router := petalflow.NewRuleRouter("triage", petalflow.RuleRouterConfig{
Routes: []petalflow.RouteRule{
{
When: petalflow.RouteCondition{
Var: "priority",
Op: petalflow.OpEquals,
Value: "high",
},
To: "urgent_handler",
},
{
When: petalflow.RouteCondition{
Var: "priority",
Op: petalflow.OpEquals,
Value: "medium",
},
To: "standard_handler",
},
},
Default: "low_priority_handler",
})
OperatorDescriptionExample
OpEqualsExact matchpriority == "high"
OpNotEqualsNot equalstatus != "closed"
OpGtGreater thanscore > 80
OpGteGreater than or equalscore >= 80
OpLtLess thanattempts < 3
OpLteLess than or equalattempts <= 3
OpContainsString contains substringmessage contains "urgent"
OpStartsWithString starts withemail startsWith "admin"
OpEndsWithString ends withemail endsWith "@company.com"
OpInValue in listcategory in ["billing", "sales"]
OpNotInValue not in liststatus notIn ["spam", "deleted"]
OpEmptyValue is empty/nilnotes is empty
OpNotEmptyValue exists and not emptycustomer_id is not empty
OpMatchesRegex matchphone matches "^\+1"
OpBetweenValue in rangeamount between [100, 1000]

Multi-condition routing with numeric comparisons:

router := petalflow.NewRuleRouter("order_router", petalflow.RuleRouterConfig{
Routes: []petalflow.RouteRule{
// Large orders from VIP customers
{
When: petalflow.RouteCondition{
And: []petalflow.RouteCondition{
{Var: "order_total", Op: petalflow.OpGte, Value: 10000},
{Var: "customer_tier", Op: petalflow.OpEquals, Value: "vip"},
},
},
To: "vip_large_order",
},
// Any large order
{
When: petalflow.RouteCondition{
Var: "order_total",
Op: petalflow.OpGte,
Value: 5000,
},
To: "large_order_review",
},
// International shipping
{
When: petalflow.RouteCondition{
Var: "shipping_country",
Op: petalflow.OpNotEquals,
Value: "US",
},
To: "international_shipping",
},
},
Default: "standard_fulfillment",
})

String pattern matching:

router := petalflow.NewRuleRouter("email_router", petalflow.RuleRouterConfig{
Routes: []petalflow.RouteRule{
// Internal emails
{
When: petalflow.RouteCondition{
Var: "sender_email",
Op: petalflow.OpEndsWith,
Value: "@company.com",
},
To: "internal_handler",
},
// Known partners
{
When: petalflow.RouteCondition{
Var: "sender_domain",
Op: petalflow.OpIn,
Value: []string{"partner1.com", "partner2.com", "partner3.com"},
},
To: "partner_handler",
},
// Suspicious patterns
{
When: petalflow.RouteCondition{
Var: "subject",
Op: petalflow.OpMatches,
Value: `(?i)(urgent|act now|limited time)`,
},
To: "spam_review",
},
},
Default: "general_inbox",
})

Combine conditions with And and Or:

router := petalflow.NewRuleRouter("access_control", petalflow.RuleRouterConfig{
Routes: []petalflow.RouteRule{
// Admin access
{
When: petalflow.RouteCondition{
Or: []petalflow.RouteCondition{
{Var: "role", Op: petalflow.OpEquals, Value: "admin"},
{Var: "role", Op: petalflow.OpEquals, Value: "superuser"},
},
},
To: "admin_panel",
},
// Verified users with premium subscription
{
When: petalflow.RouteCondition{
And: []petalflow.RouteCondition{
{Var: "email_verified", Op: petalflow.OpEquals, Value: true},
{Var: "subscription", Op: petalflow.OpIn, Value: []string{"premium", "enterprise"}},
},
},
To: "premium_features",
},
},
Default: "basic_access",
})

LLMRouter uses a language model to classify inputs and route based on semantic understanding.

router := petalflow.NewLLMRouter("intent_classifier", client, petalflow.LLMRouterConfig{
Model: "gpt-4o-mini",
InputKey: "user_message",
Categories: []petalflow.Category{
{
Name: "billing",
Description: "Questions about invoices, payments, charges, refunds, or pricing",
Examples: []string{"Why was I charged twice?", "How do I update my payment method?"},
},
{
Name: "technical",
Description: "Technical issues, bugs, errors, integration help, or how-to questions",
Examples: []string{"The API returns a 500 error", "How do I authenticate?"},
},
{
Name: "sales",
Description: "Product inquiries, feature comparisons, pricing plans, or upgrades",
Examples: []string{"What's included in the Pro plan?", "Can I get a demo?"},
},
{
Name: "general",
Description: "General questions, feedback, or topics not matching other categories",
Examples: []string{"What are your business hours?", "I have a suggestion"},
},
},
})

Override the default classification prompt:

router := petalflow.NewLLMRouter("sentiment_router", client, petalflow.LLMRouterConfig{
Model: "gpt-4o-mini",
InputKey: "feedback",
Categories: []petalflow.Category{
{Name: "positive", Description: "Satisfied, happy, or complimentary feedback"},
{Name: "negative", Description: "Dissatisfied, frustrated, or complaint feedback"},
{Name: "neutral", Description: "Factual, informational, or mixed feedback"},
},
PromptTemplate: `Analyze the sentiment of this customer feedback and classify it.
Feedback: {{.Vars.feedback}}
Categories:
{{range .Categories}}- {{.Name}}: {{.Description}}
{{end}}
Consider the overall tone, specific language used, and implied emotions.
Respond with exactly one category name.`,
})

Route uncertain classifications to human review:

router := petalflow.NewLLMRouter("confident_classifier", client, petalflow.LLMRouterConfig{
Model: "gpt-4o-mini",
InputKey: "query",
Categories: categories,
ConfidenceThreshold: 0.8,
LowConfidenceTarget: "human_review",
})

When inputs may match multiple categories:

router := petalflow.NewLLMRouter("tag_classifier", client, petalflow.LLMRouterConfig{
Model: "gpt-4o-mini",
InputKey: "document",
MultiLabel: true,
Categories: []petalflow.Category{
{Name: "urgent", Description: "Time-sensitive issues requiring immediate attention"},
{Name: "security", Description: "Security-related concerns or vulnerabilities"},
{Name: "compliance", Description: "Regulatory or compliance matters"},
{Name: "customer_facing", Description: "Impacts external customers"},
},
OutputKey: "tags", // Array of matched categories
})

GuardianNode validates envelope data against constraints. Use guards to enforce data quality, prevent invalid states, and fail fast on bad inputs.

guard := petalflow.NewGuardianNode("input_validator", petalflow.GuardianNodeConfig{
Checks: []petalflow.GuardCheck{
{
Var: "customer_id",
Op: petalflow.OpNotEmpty,
Message: "Customer ID is required",
},
{
Var: "email",
Op: petalflow.OpNotEmpty,
Message: "Email address is required",
},
{
Var: "order_items",
Op: petalflow.OpNotEmpty,
Message: "Order must contain at least one item",
},
},
OnFail: petalflow.GuardActionReject,
})
guard := petalflow.NewGuardianNode("format_validator", petalflow.GuardianNodeConfig{
Checks: []petalflow.GuardCheck{
// Email format
{
Var: "email",
Op: petalflow.OpMatches,
Value: `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`,
Message: "Invalid email format",
},
// Phone format (US)
{
Var: "phone",
Op: petalflow.OpMatches,
Value: `^\+?1?[-.\s]?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}$`,
Message: "Invalid phone number format",
},
// UUID format
{
Var: "request_id",
Op: petalflow.OpMatches,
Value: `^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`,
Message: "Request ID must be a valid UUID",
},
},
OnFail: petalflow.GuardActionReject,
})
guard := petalflow.NewGuardianNode("bounds_validator", petalflow.GuardianNodeConfig{
Checks: []petalflow.GuardCheck{
// String length
{
Var: "username",
Op: petalflow.OpMinLength,
Value: 3,
Message: "Username must be at least 3 characters",
},
{
Var: "username",
Op: petalflow.OpMaxLength,
Value: 50,
Message: "Username cannot exceed 50 characters",
},
// Numeric range
{
Var: "quantity",
Op: petalflow.OpBetween,
Value: []int{1, 1000},
Message: "Quantity must be between 1 and 1000",
},
// Minimum value
{
Var: "price",
Op: petalflow.OpGt,
Value: 0,
Message: "Price must be greater than 0",
},
},
OnFail: petalflow.GuardActionReject,
})
ActionBehavior
GuardActionRejectStop execution and return error
GuardActionWarnLog warning and continue
GuardActionRouteRoute to specified error handler
GuardActionSkipSkip remaining nodes in branch
GuardActionTransformApply transformation and continue
guard := petalflow.NewGuardianNode("input_guard", petalflow.GuardianNodeConfig{
Checks: []petalflow.GuardCheck{
{Var: "query", Op: petalflow.OpNotEmpty, Message: "Query cannot be empty"},
{Var: "query", Op: petalflow.OpMaxLength, Value: 10000, Message: "Query too long"},
},
OnFail: petalflow.GuardActionRoute,
FailRoute: "validation_error_handler",
ErrorKey: "validation_errors",
})
// Error handler node
errorHandler := petalflow.NewTransformNode("validation_error_handler", petalflow.TransformNodeConfig{
InputKeys: []string{"validation_errors"},
OutputKey: "response",
Transform: func(inputs map[string]any) (any, error) {
errors := inputs["validation_errors"].([]string)
return map[string]any{
"success": false,
"errors": errors,
"message": "Please fix the following issues and try again",
}, nil
},
})

piiGuard := petalflow.NewGuardianNode("pii_guard", petalflow.GuardianNodeConfig{
Checks: []petalflow.GuardCheck{
// SSN pattern
{
Var: "user_input",
Op: petalflow.OpNotMatches,
Value: `\b\d{3}-\d{2}-\d{4}\b`,
Message: "Input contains SSN pattern",
},
// Credit card pattern
{
Var: "user_input",
Op: petalflow.OpNotMatches,
Value: `\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b`,
Message: "Input contains credit card pattern",
},
},
OnFail: petalflow.GuardActionRoute,
FailRoute: "pii_redaction",
})

Use an LLM to detect policy violations:

moderationNode := petalflow.NewLLMNode("content_moderator", client, petalflow.LLMNodeConfig{
Model: "gpt-4o-mini",
SystemPrompt: `You are a content moderator. Analyze the input for policy violations.
Respond with JSON: {"safe": true/false, "violations": ["list", "of", "violations"]}`,
PromptTemplate: "Content to review: {{.Vars.user_content}}",
OutputKey: "moderation_result",
ResponseFormat: petalflow.ResponseFormatJSON,
})
moderationGuard := petalflow.NewGuardianNode("moderation_guard", petalflow.GuardianNodeConfig{
Checks: []petalflow.GuardCheck{
{
Var: "moderation_result.safe",
Op: petalflow.OpEquals,
Value: true,
Message: "Content violates usage policy",
},
},
OnFail: petalflow.GuardActionRoute,
FailRoute: "content_blocked",
})

Build robust workflows by combining routing and validation:

g := petalflow.NewGraph("support-workflow")
// Stage 1: Input validation
g.AddNode(petalflow.NewGuardianNode("input_guard", petalflow.GuardianNodeConfig{
Checks: []petalflow.GuardCheck{
{Var: "ticket_id", Op: petalflow.OpNotEmpty, Message: "Ticket ID required"},
{Var: "message", Op: petalflow.OpNotEmpty, Message: "Message required"},
{Var: "message", Op: petalflow.OpMaxLength, Value: 50000, Message: "Message too long"},
},
OnFail: petalflow.GuardActionRoute,
FailRoute: "invalid_input",
}))
// Stage 2: Content moderation
g.AddNode(petalflow.NewGuardianNode("content_guard", petalflow.GuardianNodeConfig{
Checks: []petalflow.GuardCheck{
{Var: "message", Op: petalflow.OpNotMatches, Value: blockedPatterns, Message: "Blocked content"},
},
OnFail: petalflow.GuardActionRoute,
FailRoute: "content_blocked",
}))
// Stage 3: Intent classification
g.AddNode(petalflow.NewLLMRouter("classify_intent", client, petalflow.LLMRouterConfig{
Model: "gpt-4o-mini",
InputKey: "message",
Categories: []petalflow.Category{
{Name: "billing", Description: "Billing and payment issues"},
{Name: "technical", Description: "Technical support"},
{Name: "general", Description: "General inquiries"},
},
}))
// Stage 4: Priority routing within each category
g.AddNode(petalflow.NewRuleRouter("priority_router", petalflow.RuleRouterConfig{
Routes: []petalflow.RouteRule{
{When: petalflow.RouteCondition{Var: "customer_tier", Op: petalflow.OpEquals, Value: "enterprise"}, To: "priority_queue"},
{When: petalflow.RouteCondition{Var: "sentiment", Op: petalflow.OpEquals, Value: "angry"}, To: "escalation_queue"},
},
Default: "standard_queue",
}))
// Define edges
g.AddEdge("input_guard", "content_guard")
g.AddEdge("content_guard", "classify_intent")
g.AddEdge("classify_intent", "priority_router")
g.SetEntry("input_guard")

func TestPriorityRouter(t *testing.T) {
router := petalflow.NewRuleRouter("test_router", petalflow.RuleRouterConfig{
Routes: []petalflow.RouteRule{
{When: petalflow.RouteCondition{Var: "priority", Op: petalflow.OpEquals, Value: "high"}, To: "urgent"},
{When: petalflow.RouteCondition{Var: "priority", Op: petalflow.OpEquals, Value: "low"}, To: "normal"},
},
Default: "normal",
})
tests := []struct {
name string
priority string
expected string
}{
{"high priority routes to urgent", "high", "urgent"},
{"low priority routes to normal", "low", "normal"},
{"unknown priority uses default", "medium", "normal"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
env := petalflow.NewEnvelope()
env.SetVar("priority", tt.priority)
target := router.Evaluate(env)
assert.Equal(t, tt.expected, target)
})
}
}
func TestInputGuard(t *testing.T) {
guard := petalflow.NewGuardianNode("test_guard", petalflow.GuardianNodeConfig{
Checks: []petalflow.GuardCheck{
{Var: "email", Op: petalflow.OpNotEmpty, Message: "Email required"},
{Var: "email", Op: petalflow.OpMatches, Value: `^.+@.+\..+$`, Message: "Invalid email"},
},
OnFail: petalflow.GuardActionReject,
})
tests := []struct {
name string
email string
shouldErr bool
}{
{"valid email passes", "user@example.com", false},
{"empty email fails", "", true},
{"invalid format fails", "not-an-email", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
env := petalflow.NewEnvelope()
env.SetVar("email", tt.email)
err := guard.Execute(context.Background(), env)
if tt.shouldErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
func TestRoutingPath(t *testing.T) {
graph := buildSupportGraph()
runtime := petalflow.NewRuntime()
var visitedNodes []string
handler := func(event petalflow.Event) {
if event.Kind == petalflow.EventNodeStart {
visitedNodes = append(visitedNodes, event.NodeID)
}
}
env := petalflow.NewEnvelope()
env.SetVar("priority", "high")
env.SetVar("message", "I need help urgently!")
_, err := runtime.Run(context.Background(), graph, env, petalflow.RunOptions{
EventHandler: handler,
})
require.NoError(t, err)
// Verify the expected path was taken
assert.Contains(t, visitedNodes, "input_guard")
assert.Contains(t, visitedNodes, "classify_intent")
assert.Contains(t, visitedNodes, "urgent_handler")
}