Principle:CrewAIInc CrewAI Conditional Routing
Overview
Conditional Routing is a control flow mechanism that enables dynamic branching, fan-in, and fan-out in event-driven workflows through routing functions and boolean combinators (or_, and_).
Description
While @start and @listen provide linear execution chains, Conditional Routing adds the control flow primitives needed for real-world workflows:
Routing via @router
A @router method executes when its trigger condition is met (just like @listen), but its return value determines which downstream methods fire next. The return value is a string that matches against @listen conditions:
- The router returns a string constant (e.g.,
"SUCCESS","FAILURE") - Downstream listeners decorated with
@listen("SUCCESS")or@listen("FAILURE")fire based on the router's return value - The router's return type annotation (e.g.,
-> Literal["SUCCESS", "FAILURE"]) is used by the framework to discover possible paths at class definition time
Fan-Out via or_()
The or_() combinator creates a condition that fires when any of the specified methods completes:
@listen(or_(method_a, method_b))
def handler(self):
# Fires when EITHER method_a OR method_b completes
pass
This enables fan-out patterns where a single producer triggers one of several possible consumers, or where multiple independent producers can each trigger the same consumer.
When used with parallel listeners, or_() implements first-wins semantics: if both method_a and method_b are running concurrently, only the first to complete triggers the handler. The framework tracks fired OR listeners to prevent duplicate execution.
Fan-In via and_()
The and_() combinator creates a condition that fires only when all specified methods have completed:
@listen(and_(method_a, method_b))
def handler(self):
# Fires only when BOTH method_a AND method_b have completed
pass
This enables fan-in (join) patterns where parallel branches must all complete before the next step proceeds. The framework tracks per-listener completion counts in _pending_and_listeners to know when all required methods have fired.
Nested Conditions
or_() and and_() can be nested arbitrarily:
@listen(or_(and_(step_1, step_2), step_3))
def handler(self):
# Fires when (step_1 AND step_2) have both completed, OR when step_3 completes
pass
Theoretical Basis
Conditional Routing draws from Petri Net control flow semantics:
| Petri Net Concept | CrewAI Flow Mapping |
|---|---|
| OR-Join (merge) | or_(method_a, method_b) -- fires when any input place has a token
|
| AND-Join (synchronization) | and_(method_a, method_b) -- fires when all input places have tokens
|
| Conditional transition | @router return value selects which output place receives a token
|
| Token | Method completion event carrying the method's return value |
| Place | Listener's pending condition state |
The key properties inherited from Petri Net semantics:
- OR-Join is non-blocking: the first token triggers the transition
- AND-Join is blocking: all tokens must arrive before the transition fires
- Conditional transitions consume exactly one token and produce exactly one output token (the router's return value)
- Composability: OR and AND joins can be nested to express complex synchronization patterns
Usage
When to Use Routing
- When execution should branch based on runtime values (data quality, classification results, user choices)
- When different processing pipelines apply to different data categories
- When a workflow has error/success/retry paths
When to Use or_()
- When a handler should respond to whichever of several sources completes first
- When implementing fallback patterns (primary source OR backup source)
- When multiple independent paths converge to a single handler
When to Use and_()
- When a handler requires results from multiple parallel branches
- When implementing barrier synchronization before a final aggregation step
- When ensuring all prerequisites complete before proceeding
Constraints
- Router return values must be strings (or string-convertible); downstream listeners match on these strings
- For visualization to work, routers should have return type annotations (
LiteralorEnum) and_()listeners reset their completion tracking after firing, supporting cyclic flowsor_()listeners fire at most once per execution cycle (tracked via_fired_or_listeners)
Related Pages
- Implementation:CrewAIInc_CrewAI_Flow_Routing_Primitives
- Principle:CrewAIInc_CrewAI_Flow_Class_Definition -- The decorator system that hosts routing declarations
- Principle:CrewAIInc_CrewAI_Flow_Execution_And_Visualization -- Runtime execution and visual rendering of routing graphs
- Principle:CrewAIInc_CrewAI_State_Model_Design -- State that routing decisions read from