Purpose: Complete reference guide for rebuilding the IVR Flow system in OmniFlow
1. IVR ARCHITECTURE OVERVIEW
Database Schema
Core Tables:
ivr_flows
– Master flow definitions
id
(UUID, PK)tenant_id
(UUID, FK to tenant_organizations) – Multi-tenant isolationuser_id
(TEXT) – Creatorname
(VARCHAR 255) – Flow namedescription
(TEXT)flow_data
(JSONB) – Complete flow snapshot (nodes + connections)is_active
(BOOLEAN) – Enable/disable flowis_default
(BOOLEAN) – Default flow for incoming callsentry_point_node_id
(UUID, FK to ivr_nodes) – Starting nodecreated_at
,updated_at
(TIMESTAMP)
ivr_nodes
– Individual flow nodes
id
(UUID, PK)flow_id
(UUID, FK to ivr_flows)node_type
(VARCHAR 50) – See node types belowname
(VARCHAR 255) – Node labelconfig
(JSONB) – Node-specific configurationposition_x
,position_y
(INTEGER) – Visual editor positioncreated_at
,updated_at
(TIMESTAMP)
ivr_node_connections
– Flow logic connections
id
(UUID, PK)flow_id
(UUID, FK to ivr_flows)from_node_id
(UUID, FK to ivr_nodes) – Source nodeto_node_id
(UUID, FK to ivr_nodes) – Target nodecondition_type
(VARCHAR 50) – “keypress”, “timeout”, “default”, “condition”condition_value
(TEXT) – For keypress: “1”, “2”, etc.created_at
(TIMESTAMP)
ivr_executions
– Runtime execution tracking
id
(UUID, PK)tenant_id
(UUID) – Multi-tenant isolationivr_flow_id
(UUID, FK to ivr_flows)call_sid
(VARCHAR 255) – Twilio call identifiercurrent_node_id
(UUID, FK to ivr_nodes) – Current positioncaller_inputs
(JSONB) – User input historyexecution_path
(JSONB) – Nodes traversedstatus
(VARCHAR 50) – “active”, “completed”, “failed”started_at
,ended_at
(TIMESTAMP)
ivr_logs
– Detailed execution logs
id
(UUID, PK)tenant_id
(UUID)ivr_flow_id
(UUID)ivr_execution_id
(UUID, FK to ivr_executions)call_sid
(VARCHAR 255)node_id
(UUID)event_type
(VARCHAR 100)event_data
(JSONB)created_at
(TIMESTAMP)
Node Types & Configurations
Each node type has specific config
(JSONB) fields:
1. WELCOME Node – Play greeting message
{
"message": "Welcome to OmniFlow",
"voice": "alice",
"language": "en-US",
"audio_url": null,
"merge_fields": {"company_name": "OmniFlow"}
}
2. MENU Node – Present options and gather input
{
"message": "Main menu",
"voice": "alice",
"language": "en-US",
"menu_options": [
{"key": "1", "label": "Sales"},
{"key": "2", "label": "Support"}
],
"timeout_seconds": 5,
"max_retries": 3
}
3. INPUT Node – Collect digits from caller
{
"message": "Please enter your account number",
"voice": "alice",
"input_type": "digits",
"max_digits": 10,
"finish_on_key": "#",
"timeout_seconds": 5
}
4. TRANSFER Node – Route call to agent/phone
{
"message": "Transferring you now",
"transfer_type": "agent", // or "external", "warm", "cold"
"transfer_to": "+1234567890", // or "agent_queue"
"voice": "alice"
}
5. CONDITION Node – Branching logic
{
"condition_field": "caller_id",
"condition_operator": "equals", // or "contains", "greater_than"
"condition_value": "+1234567890"
}
6. HANGUP Node – End call
{
"message": "Thank you for calling. Goodbye.",
"voice": "alice"
}
Connection Types
Connections define flow logic between nodes:
- default – Unconditional next node (welcome → menu)
- keypress – Menu selection (condition_value: “1”, “2”, etc.)
- timeout – No input received
- condition – Custom conditional logic
2. CORE API ENDPOINTS
IVR Flow Management (/ivr-flows
)
POST /ivr-flows
– Create new flow
- Auth: PROTECTED (requires user authentication)
- Request Body:
{
"name": "Main IVR",
"description": "Customer service flow",
"nodes": [...],
"connections": [...],
"entry_point_node_id": "uuid"
}
- Creates flow + nodes + connections in transaction
- Returns: Complete IVRFlowResponse with all nodes/connections
GET /ivr-flows
– List all flows (paginated)
- Auth: PROTECTED
- Query params:
page
,per_page
,search
,is_active
- Returns: IVRFlowListResponse with flows array and total count
GET /ivr-flows/list
– Simple list (no pagination)
- Auth: PROTECTED
- Query params:
is_active
(optional boolean) - Returns: Simplified flow list for dropdowns
- Used by: Campaign creation page
GET /ivr-flows/{flow_id}
– Get single flow
- Auth: PROTECTED
- Returns: Complete flow with all nodes and connections
PUT /ivr-flows/{flow_id}
– Update flow
- Auth: PROTECTED
- Request Body: Partial update (name, description, nodes, connections, is_active)
- Deletes and recreates nodes/connections in transaction
DELETE /ivr-flows/{flow_id}
– Delete flow
- Auth: PROTECTED
- Cascades to nodes, connections, but NOT executions (keeps history)
POST /ivr-flows/{flow_id}/set-default
– Set as default
- Auth: PROTECTED
- Sets this flow as default for incoming calls
- Unsets all other flows for the tenant
POST /ivr-flows/test?flowId={flow_id}
– Test flow
- Auth: PROTECTED
- Makes actual Twilio call to test phone number
- Entry point: flow entry_point_node_id or custom start_node_id
IVR Execution Webhook (/twilio-ivr-webhook
)
POST /twilio-ivr-webhook
– Twilio callback handler
- Auth: OPEN (Twilio can’t authenticate)
- Query params:
flow_id
(UUID) – Which flow to executenode_id
(UUID) – Current nodeaction
(string) – “start”, “flow”, “gather”, “input”, “transfer”, “hangup”- Form data: Twilio call parameters
- Returns: TwiML XML response
Actions Explained:
- start: Begin flow execution (entry point)
- flow: Continue to next node
- gather: Process menu/welcome node user input
- input: Process collected digits
- transfer: Execute call transfer
- hangup: End call after transfer
IVR Analytics Endpoints
GET /call-analytics/ivr-flow/{flow_id}
– Flow-specific analytics
- Auth: PROTECTED
- Query params:
start_date
,end_date
- Returns: Execution stats, completion rate, avg duration
GET /call-analytics/ivr-campaign/{campaign_id}
– Campaign IVR stats
- Auth: PROTECTED
- Returns: Analytics for IVR campaigns
3. IVR EXECUTION ENGINE (ivr_engine.py
)
Core Engine Class: IVRExecutionEngine
Initialization:
self.base_webhook_url = "https://omnicallflow.com/api/twilio-ivr-webhook" # Production
# or DEV: "https://api.databutton.com/_projects/{id}/dbtn/devx/app/routes/twilio-ivr-webhook"
TwiML Generation Process
Main Flow:
generate_flow_twiml()
– Entry point_generate_node_twiml()
– Route to node type handler- Node-specific generators (welcome, menu, input, transfer, etc.)
- Return TwiML XML string to Twilio
Key Design Pattern: Sequential TwiML Responses
Instead of inline processing, nodes use <Redirect>
to chain TwiML responses:
<!-- Welcome Node TwiML -->
<Response>
<Say voice="alice">Welcome to OmniFlow</Say>
<Redirect method="POST">
https://omnicallflow.com/api/twilio-ivr-webhook?flow_id=xxx&node_id=yyy&action=flow
</Redirect>
</Response>
This creates separate HTTP requests for each node, allowing:
- Better error isolation
- Cleaner state management
- Easier debugging
Node-Specific TwiML Generators
Welcome Node (_generate_welcome_twiml
):
- Plays message or audio
- Redirects to next node (usually menu)
- No user input expected
Menu Node (_generate_menu_twiml
):
- Says menu message + options
- Uses
<Gather>
for keypress input - Callback:
?action=gather
- Timeout handling: redirects to timeout node or hangs up
Input Node (_generate_input_twiml
):
- Prompts for digits (account number, etc.)
- Uses
<Gather numDigits="{max}" finishOnKey="#">
- Callback:
?action=input
Transfer Node (_generate_transfer_twiml
):
- Most complex node type
- Agent transfer: Looks up available agents from
agent_presence
table - External transfer: Dials phone number directly
- Uses
<Dial>
verb with<Client>
or<Number>
- Callback:
?action=hangup
(post-transfer)
Condition Node (_generate_condition_twiml
):
- Evaluates condition (caller_id, time, custom field)
- Routes to true/false connection targets
- No TwiML interaction, just routing logic
Hangup Node (_generate_hangup_twiml
):
- Says goodbye message
<Hangup/>
verb
Connection Resolution
_find_connection_target()
:
# Example: Find next node for menu keypress "1"
next_node = _find_connection_target(
current_node=menu_node,
flow=flow,
condition_type="keypress",
condition_value="1"
)
Searches ivr_node_connections
for matching:
from_node_id
= current_node.idcondition_type
= “keypress”condition_value
= “1”
Returns the connected to_node_id
node object.
Webhook Processing (process_webhook_response
)
Flow:
- Extract action from query params
- Load flow from database (
get_ivr_flow_by_id
) - Parse Twilio request (form data):
From
,To
,CallSid
Digits
(keypress input)SpeechResult
(voice input)DialBridged
,DialCallStatus
(transfer results)
- Route based on action:
- start: Begin at entry_point_node_id
- gather: Process menu selection
- input: Store collected digits
- transfer: Execute agent/phone transfer
- hangup: End call gracefully
- Generate next TwiML response
4. TESTING STRATEGY
Test Endpoints
1. /ivr-flows/test?flowId={uuid}
(Primary testing endpoint)
- Purpose: Make real Twilio call to test IVR flow
- Method: POST
- Auth: PROTECTED
- Request Body:
{
"flow_id": "uuid",
"test_phone": "+1234567890",
"start_node_id": "optional-uuid"
}
- What it does:
- Validates flow exists and user has access
- Gets tenant’s voice service (Twilio credentials)
- Makes outbound call via Twilio
- Directs call to IVR webhook with
?action=start
- Returns:
{
"success": true,
"message": "Test call initiated",
"call_sid": "CA123...",
"test_url": "https://omnicallflow.com/api/twilio-ivr-webhook?flow_id=xxx&action=start"
}
2. /test-ivr-webhook/...
(Debug endpoints)
/test-ivr-webhook/test-campaign-detection
- Tests if campaign settings correctly parse IVR flow ID
- Simulates webhook logic without making calls
/test-ivr-webhook/test-production-ivr-campaign
- Tests production IVR campaign configuration
- Validates JSON parsing and flow routing
3. /test-auto-dialer/...
(Auto-dialer IVR testing)
/test-auto-dialer/test-ivr-webhook
- Simulates auto-dialer webhook with IVR campaign
- Returns TwiML that would be generated
- Validates
<Redirect>
to IVR webhook
4. /webhook-debugging/debug-ivr-webhook
- Purpose: Log and inspect IVR webhook calls
- Method: POST
- Auth: OPEN (for Twilio)
- What it does:
- Logs all request params (query, form, headers)
- Forwards to actual IVR webhook
- Logs response TwiML
- Stores call trace for debugging
- Use when: Debugging production IVR issues
Testing Workflow
Step 1: Create Test Flow
- Navigate to
/ivr-flows
page - Create flow with nodes: Welcome → Menu → Transfer
- Set connections: Menu “1” → Transfer to agent
- Save flow
Step 2: Test in Editor
- Click “Test Flow” button
- Enter test phone number
- System makes real call
- Answer phone and interact with IVR
- Verify menu selections work
- Confirm transfers execute
Step 3: Debug Issues
- Check
/call-logs
for call_sid - Query
ivr_executions
table for execution trace - Check
ivr_logs
for detailed event log - Use
/webhook-debugging/debug-ivr-webhook
for TwiML inspection
Step 4: Production Testing
- Set flow as default: POST
/ivr-flows/{id}/set-default
- Make inbound call to company number
- Verify default IVR flow triggers
- Test all menu paths
- Monitor analytics: GET
/call-analytics/ivr-flow/{id}
Common Testing Issues & Fixes
Issue 1: “Flow not found” error
- Cause: flow_id not in query params or invalid UUID
- Fix: Verify webhook URL format in TwiML
Issue 2: Menu selections not working
- Cause: Missing connections or wrong condition_value
- Fix: Check
ivr_node_connections
for keypress connections
Issue 3: Agent transfer fails
- Cause: No available agents or invalid transfer_type
- Fix: Verify
agent_presence
table has online agents
Issue 4: Infinite redirect loop
- Cause: Welcome node redirects to itself
- Fix: Ensure connections form DAG (no cycles)
Issue 5: TwiML application error
- Cause: Unclosed XML tags or invalid TwiML syntax
- Fix: Use
/webhook-debugging
to inspect generated TwiML
5. COMPLETE WORKFLOWS
Workflow 1: Create IVR Flow
Frontend (IVRFlows page):
- User clicks “Create Flow”
- Visual editor allows dragging nodes
- User connects nodes with lines
- User configures each node (messages, options)
- User sets entry point node
API Call:
const response = await brain.ivr_flows_create({
name: "Main IVR",
description: "Customer service",
nodes: [
{
node_type: "welcome",
name: "Welcome",
config: {
message: "Welcome to OmniFlow",
voice: "alice"
},
position_x: 100,
position_y: 100
},
{
node_type: "menu",
name: "Main Menu",
config: {
message: "Main menu",
menu_options: [
{key: "1", label: "Sales"},
{key: "2", label: "Support"}
]
},
position_x: 300,
position_y: 100
}
],
connections: [
{
from_node_id: "welcome-uuid",
to_node_id: "menu-uuid",
condition_type: "default"
},
{
from_node_id: "menu-uuid",
to_node_id: "transfer-sales-uuid",
condition_type: "keypress",
condition_value: "1"
}
],
entry_point_node_id: "welcome-uuid"
});
Backend Processing:
- Validate flow structure (no orphan nodes, valid entry point)
- Begin database transaction
- Insert into
ivr_flows
- Insert all nodes into
ivr_nodes
- Insert all connections into
ivr_node_connections
- Commit transaction
- Return complete flow object
Workflow 2: Execute IVR Flow (Inbound Call)
Step 1: Call Arrives
- Twilio receives inbound call
- Twilio sends POST to
/call-routing-webhook
Step 2: Routing Decision
# Check for default IVR flow
default_flow = await get_default_ivr_flow_for_tenant(tenant_id)
if default_flow:
ivr_url = f"{base_url}/twilio-ivr-webhook?flow_id={default_flow.id}&node_id={entry_point}&action=start"
# Redirect to IVR
Step 3: IVR Execution Begins
- POST
/twilio-ivr-webhook?flow_id=xxx&node_id=yyy&action=start
- Creates record in
ivr_executions
(status: “active”) - Generates welcome node TwiML
- Returns to Twilio
Step 4: Node Processing
- Twilio plays welcome message
- Follows
<Redirect>
to menu node - POST
/twilio-ivr-webhook?flow_id=xxx&node_id=menu&action=flow
- Generates menu TwiML with
<Gather>
Step 5: User Input
- Caller presses “1”
- Twilio sends POST with
Digits=1
- POST
/twilio-ivr-webhook?flow_id=xxx&node_id=menu&action=gather&Digits=1
- Logs input to
ivr_logs
- Finds connection for keypress “1”
- Generates transfer node TwiML
Step 6: Transfer
- Transfer TwiML uses
<Dial><Client>agent_username</Client></Dial>
- Twilio rings agent’s softphone
- Agent answers
- Call bridged
Step 7: Completion
- Agent ends call
- Twilio sends status update
- Update
ivr_executions
(status: “completed”) - Log final event to
ivr_logs
Workflow 3: Campaign with IVR
Campaign Creation:
{
"name": "Sales Campaign",
"type": "voice",
"voice_campaign_type": "ivr", // NOT "script"
"voice_ivr_flow_id": "uuid-of-flow",
"settings": {
"voice_campaign_type": "ivr",
"voice_ivr_flow_id": "uuid-of-flow"
}
}
Auto-Dialer Execution:
- Progressive dialer makes call
- Call connects
- POST
/auto-dialer-webhook?campaign_id=xxx
- Webhook reads campaign settings
- Detects
voice_campaign_type == "ivr"
- Generates TwiML redirect to IVR webhook
- IVR flow executes normally
Critical Code:
# In auto-dialer-webhook
settings = json.loads(campaign['settings'])
campaign_type = settings.get('voice_campaign_type', 'agent')
ivr_flow_id = settings.get('voice_ivr_flow_id')
if campaign_type == 'ivr' and ivr_flow_id:
# Route to IVR
flow = await get_ivr_flow_by_id(ivr_flow_id)
entry_node = flow.entry_point_node_id
ivr_url = f"{base_url}/twilio-ivr-webhook?flow_id={ivr_flow_id}&node_id={entry_node}&action=start"
twiml = f'<Response><Redirect>{ivr_url}</Redirect></Response>'
else:
# Route to agent
twiml = generate_agent_dial_twiml()
6. INTEGRATION POINTS
Twilio Integration
Required Configuration:
- Twilio Account SID + Auth Token (in
user_voice_settings
) - Twilio Phone Number
- TwiML App SID (for softphone)
Webhook Configuration:
- Voice URL:
https://omnicallflow.com/api/call-routing-webhook
- Status Callback:
https://omnicallflow.com/api/call-status-webhook
TwiML Verbs Used:
<Say>
– Text-to-speech<Play>
– Audio file playback<Gather>
– Collect DTMF input<Dial>
– Transfer calls<Client>
– Connect to agent softphone<Number>
– Dial phone number<Redirect>
– Chain TwiML responses<Hangup>
– End call
Campaign Integration
Voice Campaign Types:
- script: Agent reads script (TTS or agent-led)
- ivr: Automated IVR flow execution
Campaign Settings Storage:
{
"voice_campaign_type": "ivr",
"voice_ivr_flow_id": "uuid",
"voice_script": null, // Not used for IVR
"voice_phone_number": "+1234567890"
}
Call Routing Integration
Routing Priority:
- Check for bypass_ivr flag (manual calls)
- Check for default IVR flow (
is_default=true
) - If default IVR exists, redirect to IVR webhook
- Otherwise, route to agent assignment
Default IVR Flow Logic:
# In call_routing_webhook
if not bypass_ivr:
default_flow = await get_default_ivr_flow_for_tenant(tenant_id)
if default_flow:
return redirect_to_ivr(default_flow)
# Fallback to agent routing
return assign_to_available_agent()
Analytics Integration
Data Collection:
ivr_executions
table tracks each IVR sessionivr_logs
table logs every node transitioncall_logs
table stores overall call metadata
Metrics Calculated:
- Total IVR executions
- Completion rate (completed / total)
- Average duration
- Node visit frequency
- Drop-off points (where callers hang up)
- Menu selection distribution
7. KEY TECHNICAL DECISIONS
Why Sequential TwiML (Redirect Pattern)?
Original Problem: Inline TwiML generation created massive XML responses.
Solution: Each node returns TwiML that redirects to next node.
Benefits:
- Smaller TwiML responses
- Better error isolation
- Easier debugging (each request logged separately)
- Twilio handles state between nodes
Why JSONB for flow_data?
Purpose: Denormalized cache of complete flow structure.
Benefits:
- Fast read access (no joins for simple flow fetch)
- Complete flow snapshot at creation time
- Easier backup/restore
Tradeoff: Must update both flow_data and nodes/connections tables.
Why tenant_id on executions/logs?
Purpose: Multi-tenant data isolation.
Benefits:
- Super admin can query across tenants
- Tenant users only see their data
- Prevents data leaks
Why is IVR webhook unprotected?
Reason: Twilio can’t send authentication headers.
Security:
- Validate Twilio signatures (optional, not implemented yet)
- Use UUIDs for flow_id/node_id (non-guessable)
- Log all requests for audit trail
8. REBUILD CHECKLIST
Phase 1: Database Setup
- [ ] Create ivr_flows table
- [ ] Create ivr_nodes table
- [ ] Create ivr_node_connections table
- [ ] Create ivr_executions table
- [ ] Create ivr_logs table
- [ ] Set up foreign keys and indexes
Phase 2: Core Engine
- [ ] Implement IVRExecutionEngine class
- [ ] Implement TwiML generators for each node type
- [ ] Implement connection resolution logic
- [ ] Implement webhook processing
Phase 3: API Endpoints
- [ ] CRUD endpoints for flows
- [ ] Simple list endpoint for dropdowns
- [ ] Set default flow endpoint
- [ ] Test flow endpoint
- [ ] IVR webhook endpoint
Phase 4: Frontend
- [ ] IVR flows list page
- [ ] Visual flow editor (or simple form)
- [ ] Node configuration modals
- [ ] Connection drawing interface
- [ ] Test flow button
Phase 5: Integration
- [ ] Add IVR flow selector to campaign creation
- [ ] Modify auto-dialer webhook for IVR routing
- [ ] Modify call routing webhook for default IVR
- [ ] Add IVR analytics to dashboard
Phase 6: Testing
- [ ] Create test flows
- [ ] Test each node type
- [ ] Test all connection types
- [ ] Test campaign integration
- [ ] Test default flow routing
- [ ] Load testing with concurrent calls
Phase 7: Monitoring & Analytics
- [ ] IVR execution tracking
- [ ] Node visit analytics
- [ ] Drop-off analysis
- [ ] Menu selection reports
9. COMMON PITFALLS & SOLUTIONS
Pitfall 1: Circular flows
- Problem: Node A → B → A (infinite loop)
- Solution: Validate flow is DAG before saving
Pitfall 2: Missing entry point
- Problem: No entry_point_node_id set
- Solution: Require entry point on flow creation
Pitfall 3: Orphan nodes
- Problem: Nodes with no connections
- Solution: Allow but warn user in editor
Pitfall 4: Invalid TwiML XML
- Problem: Unclosed tags, special characters
- Solution: Use
_escape_xml()
helper for all dynamic content
Pitfall 5: Agent not available
- Problem: Transfer node but no agents online
- Solution: Fallback to voicemail or queue
Pitfall 6: IVR not triggering for campaigns
- Problem: Campaign settings not parsed correctly
- Solution: Ensure JSON parsing in webhook, validate
voice_campaign_type == "ivr"
END OF GUIDE
Generated: 2025-10-13
Purpose: Rebuild reference for OmniFlow IVR Flow system