Ema Workflows: Routing and Branching
How do you make an Ema workflow take different paths based on user intent? This guide covers chat_categorizer, runIf conditions, and the patterns that keep your branching logic clean and maintainable.
The Two Routing Mechanisms
Ema workflows have two ways to control which nodes run:
- runIf conditions — Node only runs if condition is met
- Data dependencies — Node only runs when inputs are available
Understanding both is essential for building workflows that branch correctly.
Mechanism 1: runIf Conditions
Each node can have a runIf that checks a value:
portfolio_agent:
runIf:
lhs: intent_classifier.category
operator: EQUALS
rhs: "PORTFOLIO_REVIEW"
This node only runs when the intent is PORTFOLIO_REVIEW.
Supported Operators
| Operator | Meaning |
|---|---|
| EQUALS | Exact match |
| NOT_EQUALS | Not this value |
| CONTAINS | String contains |
Mechanism 2: Data Dependencies
If Node B's input comes from Node A, Node B waits for Node A:
flowchart TB
A["Node A<br/>(runs if X)"]:::primary -->|output| B["Node B<br/>(has no runIf, but waits for input)"]:::secondary
Key insight: If Node A doesn't run, Node B never gets its input and also doesn't run.
This creates implicit branching without explicit conditions.
The chat_categorizer Node
The categorizer is designed specifically for routing:
intent_classifier:
action: chat_categorizer
categories:
- PORTFOLIO_REVIEW: "Questions about portfolio, holdings, performance"
- COMPLIANCE_CHECK: "Questions about compliance, TMD, regulations"
- MARKET_UPDATE: "Questions about market, stocks, news"
- SEND_EMAIL: "Requests to send communications"
- Fallback: "Unclear or general requests"
output: category # Single enum value
Why Categorizer for Routing?
| Feature | Benefit |
|---|---|
| Single enum output | Works directly with runIf |
| Mutually exclusive | Only one category per request |
| Fallback support | Handles unclear cases |
| custom_data input | Can use entity extraction for decisions |
Basic Routing Pattern
flowchart TB
A[Trigger]:::primary --> B{"intent_classifier"}:::primary
B -->|PORTFOLIO_REVIEW| C[portfolio_agent]:::agent
B -->|COMPLIANCE_CHECK| D[compliance_agent]:::agent
B -->|MARKET_UPDATE| E[market_agent]:::agent
B -->|Fallback| F[fallback_handler]:::agent
C --> G((WORKFLOW_OUTPUT)):::accent
D --> G
E --> G
F --> G
Each agent has a runIf checking intent_classifier.category.
Combining Categorizer with Entity Extraction
The categorizer can see your extracted entities via custom_data:
# Step 1: Extract entities
entity_extraction:
output: extraction_columns
{
"needs_email": true,
"recipient_email": null,
"confirmation_status": "pending"
}
# Step 2: Categorizer evaluates entities
flow_router:
action: chat_categorizer
inputs:
conversation: trigger.chat_conversation
custom_data: entity_extraction.extraction_columns # ← KEY!
categories:
- NEEDS_EMAIL: "needs_email is true AND recipient_email is null"
- NEEDS_CONFIRMATION: "has all data but confirmation_status is pending"
- READY_TO_SEND: "has all data AND confirmation_status is confirmed"
- INFO_ONLY: "needs_email is false"
Now routing decisions can be based on extracted state!
Pattern: Intent + State Routing
Often you need two categorizers:
Categorizer 1: What does the user WANT? (Intent)
intent_classifier:
categories:
- PORTFOLIO_REVIEW
- COMPLIANCE_CHECK
- SEND_EMAIL
- Fallback
Categorizer 2: What CAN we do? (State)
flow_router:
custom_data: entity_extraction.extraction_columns
categories:
- NEEDS_INFO: "missing required fields"
- READY: "all fields present"
- BLOCKED: "do_not_contact is true"
Combined Flow
flowchart TB
A{"Intent Classifier"}:::primary -->|PORTFOLIO| B[Portfolio Agent]:::agent
A -->|SEND_EMAIL| C{"Flow Router"}:::secondary
A -->|Fallback| D[Fallback Handler]:::agent
C -->|NEEDS_INFO| E[Ask for info]:::secondary
C -->|READY| F[Send email]:::accent
C -->|BLOCKED| G["'Can't contact'"]:::secondary
Why two categorizers:
- Intent tells you what the user wants
- State tells you whether you can fulfill it
Avoiding Redundant runIf Conditions
❌ Verbose: runIf on every node
construct_subject:
runIf: category == SEND_EMAIL
construct_to:
runIf: category == SEND_EMAIL # Repeated!
construct_body:
runIf: category == SEND_EMAIL # Repeated!
send_email:
runIf: category == SEND_EMAIL # Repeated!
✅ Clean: Single entry point + dependencies
email_flow_start:
runIf: category == SEND_EMAIL # Only this one!
output: "proceed"
construct_subject:
input: email_flow_start.output # Only runs if entry ran
# No runIf needed!
construct_to:
input: email_flow_start.output # Only runs if entry ran
# No runIf needed!
send_email:
inputs:
- subject: construct_subject.response
- to: construct_to.response
# No runIf needed - waits for all inputs!
How it works: If email_flow_start doesn't run, downstream nodes never get their inputs and automatically don't run.
When You NEED Explicit runIf
If a node receives inputs from always-running nodes, it needs explicit runIf:
flowchart TB
A["trigger<br/>(always)"]:::secondary --> B["summarizer<br/>(always)"]:::secondary
B --> C["construct_email_to"]:::warning
C -.->|"NEEDS runIf!"| D((⚠)):::warning
Problem: construct_email_to receives from summarizer, which always runs. Without explicit runIf, it would run even without SEND_EMAIL intent!
Rule of Thumb
| Scenario | Need runIf? |
|---|---|
| Entry point for a branch | ✅ Yes |
| Only receives from runIf-gated nodes | ❌ No |
| Receives from always-running nodes | ✅ Yes |
| Receives from multiple sources | ✅ Usually |
WORKFLOW_OUTPUT: The Terminal Point
Every path must lead to WORKFLOW_OUTPUT—this is what the user sees/hears.
# Multiple paths, each to WORKFLOW_OUTPUT
portfolio_agent:
runIf: category == PORTFOLIO
output → WORKFLOW_OUTPUT
compliance_agent:
runIf: category == COMPLIANCE
output → WORKFLOW_OUTPUT
fallback:
runIf: category == Fallback
output → WORKFLOW_OUTPUT
Only one runs per turn, but all paths are defined.
Common Routing Patterns
Pattern 1: Simple Intent Routing
Categorizer → Multiple agents (each with runIf for their intent)
Best for: Distinct, non-overlapping intents.
Pattern 2: Intent + Entity-Based Routing
Entity Extraction → Categorizer (uses extraction in custom_data) → Branches
Best for: When routing depends on extracted data (has email, confirmed, etc.).
Pattern 3: Progressive Refinement
Intent Categorizer → General branch
│
↓
State Categorizer → Specific sub-branches
Best for: Complex flows with nested decisions.
Pattern 4: Confirmation Flow
Ready to send? → NEEDS_CONFIRMATION → Ask user
│
↓ (next turn)
User says "yes" → CONFIRMED → Execute action
Best for: Destructive or important actions that need user approval.
Troubleshooting Guide
Problem: Node Runs When It Shouldn't
Symptoms: Agent runs even when intent doesn't match.
Diagnosis: Missing runIf, or receives from always-running node.
Solution: Add explicit runIf:
my_agent:
runIf:
lhs: intent_classifier.category
operator: EQUALS
rhs: "MY_INTENT"
Problem: Node Never Runs
Symptoms: Node should run but never executes.
Diagnosis: Check upstream dependencies.
Solutions:
- Verify upstream node actually runs
- Check runIf condition is correct (exact string match)
- Check if dependent input is being produced
Problem: Categorizer Returns Wrong Category
Symptoms: "PORTFOLIO_REVIEW" request categorized as "Fallback".
Diagnosis: Category descriptions may be too narrow.
Solution: Improve category descriptions:
# Before
- PORTFOLIO_REVIEW: "Portfolio review"
# After
- PORTFOLIO_REVIEW: "Questions about portfolio, holdings,
performance, how am I doing, my investments"
Problem: Can't Route Based on Entity Values
Symptoms: Want to route based on needs_email: true but can't use in runIf.
Diagnosis: runIf can't evaluate JSON blob fields directly.
Solution: Use categorizer with custom_data:
flow_router:
custom_data: entity_extraction.extraction_columns
categories:
- NEEDS_EMAIL: "needs_email is true"
- NO_EMAIL: "needs_email is false"
Then use flow_router.category in runIf.
Problem: Multiple Agents Run When Only One Should
Symptoms: Both portfolio_agent and compliance_agent run.
Diagnosis: Categories might overlap, or multiple conditions are true.
Solution: Make categories mutually exclusive:
categories:
- PORTFOLIO_REVIEW: "Explicitly about portfolio, NOT compliance"
- COMPLIANCE_CHECK: "Explicitly about compliance, NOT portfolio"
Best Practices
| Practice | Why |
|---|---|
| Use categorizer for routing | Single value output works with runIf |
| Don't repeat runIf | Use dependency chains instead |
| Always have Fallback | Handle unclear cases |
| Combine with entity extraction | Use custom_data for state-aware routing |
| Keep categories mutually exclusive | One path per request |
| All paths to WORKFLOW_OUTPUT | User must always get a response |
Summary
| Question | Answer |
|---|---|
| How to route? | chat_categorizer + runIf |
| Entity-aware routing? | Pass extraction to categorizer's custom_data |
| Reduce redundant runIf? | Use dependency chains |
| When explicit runIf? | Entry points & nodes with always-running inputs |
| Terminal output? | All paths must reach WORKFLOW_OUTPUT |
The golden rule: Use chat_categorizer for intent, combine with entity extraction for state, and let data dependencies reduce boilerplate.
For more on entity extraction for state-aware routing, see Ema Workflows: Mastering Entity Extraction.