Conditional Logic
Conditional Logic
Conditional logic allows flows to branch based on runtime data. Use predicates to evaluate FlowState and execute different paths.
Basic Pattern
Use When() to start a conditional block:
flow := core.NewFlow("conditional-flow").
TriggeredBy(core.Manual("api")).
Then(fetchOrderNode).
When(func(s *core.FlowState) bool {
order := core.Get[Order](s, "fetch-order")
return order.Total > 1000
}).
Then(requireApprovalNode).
Otherwise(autoApproveNode).
Then(fulfillOrderNode).
Build()Execution:
fetchOrderNoderuns- Predicate evaluates order total
- If
> 1000:requireApprovalNoderuns - Otherwise:
autoApproveNoderuns fulfillOrderNoderuns (always)
Predicate Functions
A predicate is a function that takes *FlowState and returns bool:
type Predicate func(*FlowState) boolSimple Predicates
// Check a boolean field
When(func(s *core.FlowState) bool {
result := core.Get[CheckOutput](s, "check")
return result.IsValid
})
// Compare values
When(func(s *core.FlowState) bool {
order := core.Get[Order](s, "order")
return order.Total > 1000
})
// Check string equality
When(func(s *core.FlowState) bool {
user := core.Get[User](s, "user")
return user.Role == "admin"
})Complex Predicates
// Multiple conditions
When(func(s *core.FlowState) bool {
order := core.Get[Order](s, "order")
user := core.Get[User](s, "user")
return order.Total > 1000 && user.VIPStatus
})
// Check array length
When(func(s *core.FlowState) bool {
result := core.Get[FetchOutput](s, "fetch")
return len(result.Items) > 0
})
// Check for errors in previous step
When(func(s *core.FlowState) bool {
validation := core.Get[ValidationOutput](s, "validate")
return len(validation.Errors) == 0
})Conditional API
Then / Otherwise (Simple Branch)
For simple if/else logic:
flow := core.NewFlow("simple-branch").
TriggeredBy(core.Manual("api")).
Then(checkNode).
When(predicate).
Then(doIfTrue).
Otherwise(doIfFalse).
Then(continueNode).
Build()Then()adds to the “if true” branchOtherwise()adds to the “if false” branch and returns to main flow
Then / Else / EndWhen (Complex Branches)
For multi-step branches:
flow := core.NewFlow("complex-branch").
TriggeredBy(core.Manual("api")).
Then(checkNode).
When(predicate).
Then(step1IfTrue).
Then(step2IfTrue).
ThenParallel("parallel-if-true", nodeA, nodeB).
Else().
Then(step1IfFalse).
Then(step2IfFalse).
EndWhen().
Then(continueNode).
Build()Else()switches to building the “if false” branchEndWhen()ends the conditional block without an else action- After
EndWhen()orOtherwise(), you’re back on the main flow
No Else Branch
Use EndWhen() when no else branch is needed:
flow := core.NewFlow("optional-step").
TriggeredBy(core.Manual("api")).
Then(fetchNode).
When(func(s *core.FlowState) bool {
result := core.Get[FetchOutput](s, "fetch")
return result.NeedsEnrichment
}).
Then(enrichNode).
EndWhen().
Then(storeNode).
Build()If predicate is false, flow skips directly to storeNode.
Parallel in Conditionals
Both branches support parallel execution:
flow := core.NewFlow("parallel-branches").
TriggeredBy(core.Manual("api")).
Then(checkPriorityNode).
When(func(s *core.FlowState) bool {
return core.Get[PriorityCheck](s, "check").IsHighPriority
}).
ThenParallel("high-priority-actions",
notifyManagerNode,
escalateNode,
logAuditNode,
).
Otherwise(standardProcessingNode).
Then(completeNode).
Build()Using OtherwiseParallel():
flow := core.NewFlow("parallel-else").
TriggeredBy(core.Manual("api")).
When(predicate).
Then(singleActionNode).
OtherwiseParallel("fallback-actions",
fallbackA,
fallbackB,
fallbackC,
).
Build()Nested Conditionals
Conditionals can be nested within branches:
flow := core.NewFlow("nested-conditions").
TriggeredBy(core.Manual("api")).
Then(fetchOrderNode).
When(func(s *core.FlowState) bool {
order := core.Get[Order](s, "order")
return order.Total > 100
}).
// First level: order > $100
Then(checkInventoryNode).
When(func(s *core.FlowState) bool {
inv := core.Get[InventoryCheck](s, "inventory")
return inv.InStock
}).
// Nested: in stock
Then(reserveStockNode).
Otherwise(backorderNode).
// Back to first level
Else().
// First level else: order <= $100
Then(expressCheckoutNode).
EndWhen().
Then(confirmOrderNode).
Build()Complete Example
Order processing with conditional approval flow:
package main
import (
"context"
"time"
"github.com/resolute/resolute/core"
)
type Order struct {
ID string
Total float64
Customer Customer
Items []Item
}
type Customer struct {
ID string
VIP bool
CreditScore int
}
type ValidationResult struct {
Valid bool
Errors []string
}
type ApprovalResult struct {
Approved bool
ApproverID string
Notes string
}
func main() {
// Fetch and validate order
fetchOrder := core.NewNode("fetch-order", fetchOrderFn, FetchInput{}).
As("order")
validateOrder := core.NewNode("validate-order", validateOrderFn, ValidateInput{}).
WithInputFunc(func(s *core.FlowState) ValidateInput {
order := core.Get[Order](s, "order")
return ValidateInput{OrderID: order.ID}
}).
As("validation")
// Approval nodes
autoApprove := core.NewNode("auto-approve", autoApproveFn, ApproveInput{}).
WithInputFunc(func(s *core.FlowState) ApproveInput {
order := core.Get[Order](s, "order")
return ApproveInput{OrderID: order.ID, Auto: true}
})
managerApproval := core.NewNode("manager-approval", requestApprovalFn, ApprovalRequest{}).
WithInputFunc(func(s *core.FlowState) ApprovalRequest {
order := core.Get[Order](s, "order")
return ApprovalRequest{
OrderID: order.ID,
Amount: order.Total,
Level: "manager",
}
}).
WithTimeout(24 * time.Hour)
vpApproval := core.NewNode("vp-approval", requestApprovalFn, ApprovalRequest{}).
WithInputFunc(func(s *core.FlowState) ApprovalRequest {
order := core.Get[Order](s, "order")
return ApprovalRequest{
OrderID: order.ID,
Amount: order.Total,
Level: "vp",
}
}).
WithTimeout(48 * time.Hour)
// Processing nodes
processPayment := core.NewNode("process-payment", processPaymentFn, PaymentInput{}).
WithInputFunc(func(s *core.FlowState) PaymentInput {
order := core.Get[Order](s, "order")
return PaymentInput{OrderID: order.ID, Amount: order.Total}
})
rejectOrder := core.NewNode("reject-order", rejectOrderFn, RejectInput{}).
WithInputFunc(func(s *core.FlowState) RejectInput {
order := core.Get[Order](s, "order")
validation := core.Get[ValidationResult](s, "validation")
return RejectInput{
OrderID: order.ID,
Reasons: validation.Errors,
}
})
fulfillOrder := core.NewNode("fulfill-order", fulfillOrderFn, FulfillInput{}).
WithInputFunc(func(s *core.FlowState) FulfillInput {
order := core.Get[Order](s, "order")
return FulfillInput{OrderID: order.ID}
})
// Build flow with conditionals
flow := core.NewFlow("order-processing").
TriggeredBy(core.Manual("order-api")).
Then(fetchOrder).
Then(validateOrder).
// First check: Is order valid?
When(func(s *core.FlowState) bool {
validation := core.Get[ValidationResult](s, "validation")
return validation.Valid
}).
// Valid order path
When(func(s *core.FlowState) bool {
order := core.Get[Order](s, "order")
// VIP customers or small orders auto-approve
return order.Customer.VIP || order.Total < 500
}).
Then(autoApprove).
Else().
// Large orders need approval
When(func(s *core.FlowState) bool {
order := core.Get[Order](s, "order")
return order.Total >= 10000
}).
Then(managerApproval).
Then(vpApproval). // VP also reviews very large orders
Otherwise(managerApproval). // Just manager for medium orders
EndWhen().
// After approval, process payment
Then(processPayment).
Then(fulfillOrder).
Otherwise(rejectOrder). // Invalid order path
Build()
core.NewWorker().
WithConfig(core.WorkerConfig{
TaskQueue: "orders-queue",
}).
WithFlow(flow).
Run()
}Conditional Decision Diagram
┌──────────────┐
│ fetchOrder │
└──────┬───────┘
│
┌──────▼───────┐
│validateOrder │
└──────┬───────┘
│
┌────────────▼────────────┐
│ validation.Valid? │
└────────────┬────────────┘
yes │ no
┌────────────┴────────────┐
│ │
┌────────▼────────┐ ┌────────▼────────┐
│VIP || total<500?│ │ rejectOrder │
└────────┬────────┘ └─────────────────┘
yes │ no
┌────────┴────────┐
│ │
┌────▼────┐ ┌───────▼───────┐
│autoApprove│ │ total>=10000? │
└────┬────┘ └───────┬───────┘
│ yes │ no
│ ┌────────────┴────────────┐
│ │ │
│ ┌─▼──────────┐ ┌─────────▼─┐
│ │mgr+vp │ │mgr only │
│ │approval │ │approval │
│ └────────────┘ └───────────┘
│ │ │
│ └────────┬───────┘
│ │
└────────────────────┤
│
┌──────▼───────┐
│processPayment│
└──────┬───────┘
│
┌──────▼───────┐
│ fulfillOrder │
└──────────────┘Best Practices
1. Keep Predicates Simple
Extract complex logic into helper functions:
// Good: Clear, named predicate
func needsApproval(s *core.FlowState) bool {
order := core.Get[Order](s, "order")
return order.Total > 1000 && !order.Customer.VIP
}
flow := core.NewFlow("orders").
When(needsApproval).
...
// Avoid: Complex inline logic
When(func(s *core.FlowState) bool {
o := core.Get[Order](s, "order")
c := core.Get[Customer](s, "customer")
h := core.Get[History](s, "history")
return o.Total > 1000 && !c.VIP &&
h.Disputes < 3 &&
time.Since(c.CreatedAt) > 30*24*time.Hour
})2. Handle All Cases
Ensure all paths are handled:
// Good: Explicit handling
When(predicate).
Then(handleTrue).
Otherwise(handleFalse)
// Or explicit no-action else
When(predicate).
Then(handleTrue).
EndWhen() // Explicit: do nothing if false
// Risky: Forgetting else
When(predicate).
Then(handleTrue)
// What happens if predicate is false?3. Use Descriptive Node Names
Make conditional paths clear in logs:
// Good: Clear what each path does
When(isHighValue).
Then(core.NewNode("approve-high-value", ...)).
Otherwise(core.NewNode("auto-approve-standard", ...))
// Avoid: Generic names
When(isHighValue).
Then(core.NewNode("step1", ...)).
Otherwise(core.NewNode("step2", ...))4. Avoid Deep Nesting
Flatten complex conditionals when possible:
// Consider: Multiple simple conditionals
flow := core.NewFlow("flat").
Then(checkA).
When(conditionA).Then(handleA).EndWhen().
Then(checkB).
When(conditionB).Then(handleB).EndWhen().
Then(checkC).
When(conditionC).Then(handleC).EndWhen().
Build()
// Instead of: Deeply nested
flow := core.NewFlow("nested").
When(conditionA).
When(conditionB).
When(conditionC).
Then(handleABC).
...5. Test All Branches
Ensure test coverage for each conditional path:
func TestOrderFlow_VIPAutoApproves(t *testing.T) {
tester := core.NewFlowTester(flow)
tester.SetResult("order", Order{Total: 5000, Customer: Customer{VIP: true}})
tester.SetResult("validation", ValidationResult{Valid: true})
err := tester.Run()
require.NoError(t, err)
// Verify auto-approve was called, not manual approval
assert.True(t, tester.WasExecuted("auto-approve"))
assert.False(t, tester.WasExecuted("manager-approval"))
}
func TestOrderFlow_LargeOrderNeedsVPApproval(t *testing.T) {
tester := core.NewFlowTester(flow)
tester.SetResult("order", Order{Total: 15000, Customer: Customer{VIP: false}})
tester.SetResult("validation", ValidationResult{Valid: true})
err := tester.Run()
require.NoError(t, err)
// Both manager and VP should approve
assert.True(t, tester.WasExecuted("manager-approval"))
assert.True(t, tester.WasExecuted("vp-approval"))
}See Also
- Sequential Steps - Basic sequential execution
- Parallel Execution - Concurrent execution
- Error Handling - Handling failures
- FlowState - Accessing state in predicates
- Testing - Testing conditional flows