Ema Workflows: RAG Flow Control
A technical guide to controlling RAG flow in Ema workflows. Understanding trigger outputs, conversation handling, and query construction is the difference between a chatbot that retrieves relevant context and one that searches with noise. This guide demystifies the core concepts.
The Core Problem
When building conversational AI workflows, you face a fundamental challenge: what text do you send to your knowledge search?
Send the wrong thing, and your RAG retrieval returns irrelevant results. Send the right thing, and your AI Employee becomes remarkably accurate.
This guide covers the primitives you need to master:
| Concept | Purpose |
|---|---|
user_query |
Current message only |
chat_conversation |
Full conversation history |
additional_context |
External context from your app |
conversation_summarizer |
Smart query extraction |
call_llm query builder |
Custom query construction |
entity_extraction |
Structured data from conversation |
Trigger Outputs Explained
Every Ema workflow starts with a trigger node. The trigger provides several outputs that you can wire to downstream agents.
The Three Trigger Outputs
| Output | Type | What It Contains |
|---|---|---|
user_query |
TEXT_WITH_SOURCES | Current message only—just what the user said this turn |
chat_conversation |
CHAT_CONVERSATION | Full history—entire back-and-forth with all messages |
additional_context |
TEXT_WITH_SOURCES | Custom context passed from API/SDK when invoking the persona |
When to Use Each
flowchart TB
subgraph user_query["user_query"]
U1["Single-turn Q&A, simple lookups"]:::secondary
U2["When history doesn't matter"]:::secondary
U3["'What's the PTO policy?'"]:::secondary
end
subgraph chat_conversation["chat_conversation"]
C1["Multi-turn with references"]:::secondary
C2["When you need full context for agent reasoning"]:::secondary
C3["NOT directly for search queries"]:::secondary
end
subgraph additional_context["additional_context"]
A1["App-provided context"]:::accent
A2["Pre-populated from your integration"]:::accent
A3["The 'hidden gem' for contextual awareness"]:::accent
end
How Search Query Works
When you connect something to search.query, that text becomes the semantic search query against your knowledge base.
Good query:
"I need portfolio update for Michael Thompson"
This searches your knowledge base for documents related to portfolios and Michael Thompson. Clean, specific, effective.
Bad query:
User: Hi
Bot: Hello! How can I help?
User: What's Michael's portfolio?
Bot: Let me check...
User: Also add his email
This makes a terrible search query—too much noise, conversation artifacts, and irrelevant tokens diluting the semantic signal.
Key insight: What goes into
search.querydirectly determines retrieval quality. Garbage in, garbage out.
The Problem with Raw Conversation
If you wire trigger.chat_conversation directly to search.query, you're sending the entire conversation history as your search text.
Why this fails:
- Noise accumulation — Every "Hi", "Thanks", and "Let me check" pollutes the query
- Token dilution — The actual search intent gets lost in conversational filler
- Semantic drift — Embeddings get pulled toward conversation artifacts, not the user's intent
Example of what happens:
Conversation History:
├── User: "Hi there"
├── Bot: "Hello! How can I help you today?"
├── User: "I'm looking for info on Michael Thompson"
├── Bot: "Sure, let me look that up."
├── User: "Specifically his portfolio performance"
├── Bot: "I found some information..."
└── User: "What about compliance status?"
Search query (if using raw conversation):
"Hi there Hello! How can I help you today? I'm looking for info on
Michael Thompson Sure, let me look that up. Specifically his portfolio
performance I found some information... What about compliance status?"
This is not what you want to search for.
Solution: conversation_summarizer
The conversation_summarizer agent transforms messy conversation history into clean, search-optimized text.
Input:
User: Hi
Bot: Hello! How can I help?
User: What's Michael's portfolio?
Bot: Let me check...
User: Also add his email
Output:
"Portfolio information and email for client Michael Thompson"
Much better for semantic search.
How It Works
flowchart LR
A[Trigger]:::primary --> B[chat_conversation]:::secondary
B --> C[conversation_summarizer]:::accent
C --> D[summarized_conversation]:::secondary
D --> E[search.query]:::accent
Basic Configuration
name: "conversation_summarizer"
action: "actions.emainternal.conversation_summarizer"
inputs:
conversation: ""
outputs:
- summarized_conversation
additional_context: The Hidden Gem
This is context you pass from outside when calling the persona via API. It's information your app knows that the user didn't explicitly say.
How to Use It
When invoking the persona programmatically:
// When calling the persona from your app
ema.chat({
persona_id: "58bea418...",
message: "What's their portfolio?",
additional_context:
"Client: Michael Thompson (c_102), Advisor: Sarah Mitchell",
});
Use Cases
| Scenario | What to Pass |
|---|---|
| User context | Current logged-in user, their role, permissions |
| Page context | What page/screen they're viewing |
| Entity context | Account/client they're looking at |
| Session context | Recent actions, workflow state |
Why This Matters
Without additional_context:
User: "What's their portfolio?"
AI: "Whose portfolio would you like to see?" ← Needs clarification
With additional_context: "Client: Michael Thompson":
User: "What's their portfolio?"
AI: "Here's Michael Thompson's portfolio..." ← Already knows context
The AI Employee can answer immediately because your app pre-populated the context.
Accessing in Workflows
name: "search"
action: "actions.emainternal.search"
inputs:
query: " "
Combine additional_context with summarized conversation for maximum relevance.
Better Query Tuning Patterns
Sometimes conversation_summarizer with default settings isn't enough. Here are three patterns for more control.
Pattern 1: conversation_summarizer with Custom Instructions
Instead of generic summarization, customize what it extracts:
name: "conversation_summarizer"
action: "actions.emainternal.conversation_summarizer"
inputs:
conversation: ""
user_instructions: |
Extract:
1. Client name (if mentioned)
2. Specific request type (portfolio, compliance, market)
3. Any specific holdings or topics
Output as: "{request_type} for {client_name}: {details}"
outputs:
- summarized_conversation
Result: Instead of generic summaries, you get structured, search-optimized queries.
Pattern 2: call_llm as Query Builder
More control than conversation_summarizer—use call_llm to construct the exact query you need:
name: "query_builder"
action: "actions.emainternal.call_llm"
inputs:
query: ""
named_inputs:
- name: "Intent"
value: ""
- name: "History"
value: ""
user_instructions: |
Build a search query for the knowledge base.
Rules:
- If intent is CLIENT_REVIEW: focus on portfolio, holdings, performance
- If intent is COMPLIANCE: focus on TMD, concentration, limits
- Include client name if mentioned
- Be specific, avoid generic terms
Output ONLY the search query, nothing else.
outputs:
- response # This becomes the search query
When to use: When you need intent-aware query construction with specific formatting rules.
Pattern 3: Entity Extraction → Query Construction
Extract structured entities first, then build the query:
# Step 1: Extract entities
name: "entity_extraction"
action: "actions.emainternal.entity_extraction"
inputs:
text: ""
columns:
- client_name
- topic
- time_period
outputs:
- columns
# Step 2: Build query from entities
name: "query_constructor"
action: "actions.emainternal.call_llm"
inputs:
named_inputs:
- name: "Entities"
value: ""
- name: "Intent"
value: ""
user_instructions: |
Build query: "{client_name} {topic} {intent_context}"
outputs:
- response
When to use: When you need maximum control and the conversation contains structured data you can extract.
Handling "The Last Question Was About..." References
Multi-turn conversations often include references to previous topics:
- "What about that client?"
- "The same portfolio"
- "Add to the ticket we discussed"
The Simple Approach: Use Full History
The chat_conversation already contains this context. Pass it to agents that need to reason about history:
name: "custom_agent"
action: "actions.emainternal.custom_agent"
inputs:
named_inputs:
- name: "Conversation"
value: "" # Full history
- name: "Current"
value: "" # Just this message
task_instructions: |
The user may reference previous topics.
Use the Conversation history to understand context.
Current query:
The Explicit Approach: Extract Context
For more control, explicitly extract what was discussed:
name: "context_extractor"
action: "actions.emainternal.call_llm"
inputs:
query: ""
user_instructions: |
Extract any entities/topics discussed in previous messages.
Output JSON:
{
"previous_client": "...",
"previous_topic": "...",
"current_request": "..."
}
outputs:
- response
Now you have structured data about conversation history that downstream agents can use precisely.
When to Use What: Decision Matrix
By Scenario
| Scenario | Best Input | Why |
|---|---|---|
| Simple Q&A, no history needed | user_query |
Clean, single-turn, no noise |
| Need to know what was discussed before | chat_conversation |
Full context for reasoning |
| Semantic search query | conversation_summarizer output |
Optimized for retrieval |
| You know context from your app | additional_context |
Pre-populated, no clarification needed |
| Multi-turn with entity references ("that client") | chat_conversation → entity extraction |
Structured extraction from history |
| Intent-aware search | intent_classifier + call_llm query builder |
Custom query per intent |
By Pattern Complexity
flowchart LR
A["user_query<br/>Simplest"]:::accent --> B[conversation_summarizer]:::secondary
B --> C[call_llm builder]:::secondary
C --> D["entity extraction<br/>+ query construction<br/>Most Control"]:::primary
Start simple. Add complexity only when retrieval quality demands it.
Recommended Workflow Patterns
Basic Pattern: Summarizer → Search
For most use cases, this is sufficient:
flowchart TB
A[Trigger]:::primary --> B[conversation_summarizer]:::accent
B --> C[knowledge_search]:::accent
C --> D[respond_with_sources]:::secondary
B -.->|"query: summarized_conversation"| C
Advanced Pattern: Intent-Aware Query Building
When different intents need different search strategies:
flowchart TB
A[Trigger]:::primary --> B[intent_classifier]:::secondary
A --> C[entity_extraction]:::accent
A --> D[conversation_summarizer]:::accent
B --> E[call_llm: query_builder]:::accent
C --> E
D --> E
E --> F[knowledge_search]:::primary
F --> G[respond_with_sources]:::secondary
Why this works:
- Intent classifier tells you what kind of search to perform
- Entity extraction pulls out structured data (client name, topic)
- Conversation summarizer gives you the condensed query
- Query builder combines all three into an optimal search query
With additional_context
When your app can provide context:
flowchart TB
A[Trigger]:::primary -->|"additional_context"| B[query_builder]:::accent
B -->|"Combines context + query"| C[knowledge_search]:::accent
The AI Employee knows who you're asking about before the user even specifies.
Common Mistakes to Avoid
Mistake 1: Raw Conversation to Search
# ❌ DON'T DO THIS
search:
query: "" # Full conversation = noisy search
# ✅ DO THIS
search:
query: ""
Mistake 2: Ignoring additional_context
If you control the integration, you have the power to pre-populate context. Use it!
// ❌ Missing opportunity
ema.chat({
persona_id: "...",
message: "What's their status?",
});
// ✅ Rich context
ema.chat({
persona_id: "...",
message: "What's their status?",
additional_context: "Viewing: Client #1234 (Acme Corp), Tab: Compliance",
});
Mistake 3: Over-Engineering Simple Use Cases
If users ask simple, single-turn questions, you don't need entity extraction pipelines:
# ✅ Simple use case = simple solution
search:
query: ""
Add complexity when retrieval quality suffers, not preemptively.
Mistake 4: Forgetting Token Limits
Long conversations can exceed context windows. Use conversation_summarizer to condense:
conversation_summarizer:
max_turns: 10 # Limit history depth
user_instructions: "Focus on the current request, not pleasantries"
Putting It All Together
Scenario: Financial Advisor Chatbot
Requirements:
- Answer questions about client portfolios
- Handle references to previous conversation ("that client")
- App knows which advisor is logged in
Workflow:
flowchart TB
A["Chat Trigger"]:::primary --> B{"intent_classifier"}:::secondary
A --> C[entity_extraction]:::accent
B -->|"CLIENT_REVIEW"| D[query_builder: call_llm]:::accent
C -->|"client_name, topic"| D
D -->|"constructed query"| E[knowledge_search]:::primary
E -->|"Returns docs"| F[respond_with_sources]:::secondary
F --> G((Output)):::accent
Why this works:
additional_contextprovides advisor context without asking- Entity extraction catches "Michael Thompson" even if mentioned turns ago
- Intent-aware query builder optimizes search for the specific request type
- The search query is clean, specific, and relevant
TL;DR
| Need | Solution |
|---|---|
| Current message only | trigger.user_query |
| Full conversation history | trigger.chat_conversation |
| App-provided context | trigger.additional_context |
| Clean search query | conversation_summarizer output |
| Intent-aware search | intent_classifier → call_llm query builder |
| Reference resolution ("that client") | chat_conversation → agent reasoning or entity extraction |
| Maximum control | Entity extraction + custom query construction |
Start simple: conversation_summarizer → knowledge_search
Add complexity when: Retrieval quality suffers, or you need intent-specific search strategies.
Always consider: Can your app provide additional_context? It's often the easiest win for relevance.
For more on Ema workflows and concepts, see Ema Platform: A Complete Guide to Agentic AI Concepts.