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:

  1. runIf conditions — Node only runs if condition is met
  2. 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:

  1. Verify upstream node actually runs
  2. Check runIf condition is correct (exact string match)
  3. 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.