The WhatsApp Conversations feature enables automated, personalized WhatsApp messaging flows for customers who interact with places through Forkast. When a customer taps the WhatsApp button in a PasosModule step, a conversation is automatically created and tracked in the database, enabling contextual, fork-specific replies without requiring the customer to send any codes or identifiers.
Implementation Status: β Production Ready
- β conversations collection with 9 indexes (including TTL)
- β Multi-channel form_templates architecture (pasos-form, whatsapp, email)
- β Default WhatsApp templates created
- β Conversation creation API (fully implemented)
- β Webhook conversation flow processing (fully implemented)
- β Phone lookup and message routing (fully implemented)
- β Two-tree architecture (bot-initiated + user-initiated flows)
- β Variable replacement engine (fully implemented)
- β Message history tracking (fully implemented)
- π§ Place.forks.forms database migration (script ready, not executed)
- β Visual conversation flow builder UI (implemented)
- β Place edit form integration (implemented)
Instead of parsing codes from customer messages, we use phone number lookup to match incoming WhatsApp messages to existing conversations. This provides a seamless user experience while maintaining full conversation tracking and history.
- Conversations Collection - New MongoDB collection storing all WhatsApp interactions
- Conversation Document - Created when customer taps WhatsApp button in PasosModule
- Phone Number Lookup - Incoming messages matched to conversations by customer phone
- 72-Hour Expiration - Conversations remain active for 72 hours, then new ones are created
- Message History - All received and sent messages tracked in arrays
interface Conversation {
_id: ObjectId;
shortId: string; // Unique ID with "co_" prefix (e.g., "co_a1b2c3")
type: 'whatsapp' | 'email' | 'sms' | 'voice'; // Communication channel type
// Customer Information
customer: {
phone: string; // E.164 format: "+1234567890" - for quick lookup
id: ObjectId; // Reference to customers._id (source of truth for name, email, etc.)
};
// Place Information
place: {
shortId: string; // "restaurantabc" - reference to places.shortId
id?: ObjectId; // Optional: reference to places._id
};
// Visit Information
visit: {
id: ObjectId; // Reference to visits._id (source of truth for visit metadata)
fork: 'instagram' | 'google'; // Which fork they came from (denormalized for quick filtering)
};
// Message History
receivedMessages: Array<{
messageId: string; // Twilio MessageSid
body: string; // Message content
receivedAt: Date; // When received
from: string; // Sender (whatsapp:+1234567890)
profileName?: string; // WhatsApp profile name
}>;
sentMessages: Array<{
messageId?: string; // Twilio MessageSid (if available)
body: string; // Message content
sentAt: Date; // When sent
to: string; // Recipient (whatsapp:+1234567890)
status?: 'sent' | 'delivered' | 'read' | 'failed'; // Delivery status
}>;
// Lifecycle
status: 'active' | 'expired'; // Active within 72 hours
createdAt: Date; // When conversation was created
lastActivityAt: Date; // Last message sent or received
expiresAt: Date; // createdAt + 72 hours
// Metadata
metadata?: {
source?: string; // How conversation was initiated
deviceType?: string;
userAgent?: string;
};
}Database: forkast.conversations
Created by: scripts/create-conversation-indexes-correct-db.js
// β
9 indexes created and active:
1. idx_customer_phone_type_expires - { 'customer.phone': 1, type: 1, expiresAt: 1 }
2. idx_place_created - { placeId: 1, createdAt: -1 }
3. idx_status_updated - { status: 1, updatedAt: -1 }
4. idx_current_step - { 'flow.currentStepId': 1 }
5. idx_short_id - { shortId: 1 } (unique)
6. idx_visit_id - { visitId: 1 }
7. idx_active_conversations - { status: 1, expiresAt: 1 }
8. idx_expiresAt_ttl - { expiresAt: 1 } (TTL index, expireAfterSeconds: 0)
9. _id - Default MongoDB index
Note: Actual schema uses placeId (ObjectId) instead of place.shortId for better performanceCustomer scans QR code β /ig/restaurantabc
Step 1: Enter email β Creates/updates customer record
Step 2: Enter phone β Updates customer with phone number
Step 3: Tap WhatsApp button
Location: src/components/place/InputTypes/WhatsAppButton.tsx
Process:
- Customer has already entered phone number in previous step
- Create conversation document via API call (links phone β customer β visit β place)
- Replace
{{place.shortId}}and{{place.name}}in pre-filled message - Open WhatsApp with processed message
Pre-filled Message Example:
"Hola! Me gustarΓa participar en el sorteo semanal en {{place.name}}."
Important Notes:
- β No code/ID needed in message - phone number lookup handles everything!
- β Conversation already created - when they send the message, we match by phone
- β Seamless UX - customer doesn't need to remember or type any codes
β οΈ Don't include{{conversation.shortId}}- not needed for lookup (phone is the key)
β
Endpoint: POST /api/conversations/create (FULLY IMPLEMENTED)
Location: src/app/api/conversations/create/route.ts
Request:
{
"type": "whatsapp",
"customer": {
"phone": "+16463345377",
"id": "507f1f77bcf86cd799439011"
},
"place": {
"shortId": "restaurantabc"
},
"visit": {
"id": "507f1f77bcf86cd799439012",
"fork": "instagram"
}
}Response:
{
"success": true,
"conversation": {
"shortId": "co_a1b2c3",
"expiresAt": "2025-10-29T12:00:00Z"
}
}Logic:
// Check if active conversation exists for this phone and type
const existingConversation = await conversations.findOne({
'customer.phone': phone,
type: 'whatsapp', // Only match same communication channel
expiresAt: { $gte: new Date() }
});
if (existingConversation) {
// Update existing conversation
return existingConversation;
} else {
// Create new conversation
const newConversation = {
shortId: generateShortId('co_'),
type: 'whatsapp',
customer: {
phone: phone,
id: new ObjectId(customerId)
},
place: {
shortId: placeShortId
},
visit: {
id: new ObjectId(visitId),
fork: fork
},
receivedMessages: [],
sentMessages: [],
status: 'active',
createdAt: new Date(),
lastActivityAt: new Date(),
expiresAt: new Date(Date.now() + 72 * 60 * 60 * 1000) // 72 hours
};
await conversations.insertOne(newConversation);
return newConversation;
}Customer taps "Send" in WhatsApp β Message sent to Twilio number
β
Endpoint: POST /api/twilio/whatsapp (FULLY IMPLEMENTED)
Location: src/app/api/twilio/whatsapp/route.ts
Current Status:
- β Webhook endpoint receives and validates Twilio messages
- β Phone lookup and customer creation fully operational
- β Conversation lookup by phone + place implemented
- β Template-based reply messages with variable replacement
- β Message history tracking (receivedMessages + sentMessages)
- β Two-tree architecture (bot-initiated + user-initiated flows)
- β Priority-based keyword/regex matching
- β Step progression and state management
Incoming Data:
{
MessageSid: "SM1234567890",
From: "whatsapp:+16463345377",
To: "whatsapp:+14155238886",
Body: "Hola! Me gustarΓa participar en el sorteo semanal.",
ProfileName: "Joe Saavedra"
}β ACTUAL Processing Logic (FULLY IMPLEMENTED):
The webhook implements a sophisticated two-tree architecture for handling conversations:
1. Main Conversation Tree (conversationTree):
- Sequential, bot-initiated steps
- Automatically progresses through predefined steps
- Tracks
currentStepIndexin conversation - Example: Welcome β Ask Question β Confirmation β End
2. User-Initiated Tree (userInitiatedTree):
- Triggered AFTER main tree completes
- Responds to customer-initiated messages
- Three trigger types:
keyword_match: Match specific keywordsregex_match: Match regex patternssequential: Cycle through responses in order
- Priority-based matching (higher priority checked first)
// 1. Extract phone number and find most recent visit
const customerPhone = extractPhoneNumber(webhook.From); // "+16463345377"
const visit = await findMostRecentVisitByPhone(customerPhone);
if (!visit) {
return genericResponse();
}
// 2. Find active conversation for this phone + place
const conversation = await conversations.findOne({
'customer.phone': customerPhone,
'place.shortId': visit.placeShortId,
type: 'whatsapp',
status: 'active',
expiresAt: { $gte: new Date() }
});
// 3. Create new conversation if none exists or expired
if (!conversation) {
conversation = await createOrRenewConversation(customerPhone, visit);
}
// 4. Load place and WhatsApp template
const place = await places.findOne({ shortId: visit.placeShortId });
const whatsappTemplateId = place.forks?.[visit.fork]?.forms?.whatsapp?.formId || 'default-whatsapp';
const template = await getFormTemplates(whatsappTemplateId);
// 5. Determine which message to send
let responseMessage;
if (conversation.currentStepIndex < template.conversationTree.length) {
// Still in main conversation tree
const currentStep = template.conversationTree[conversation.currentStepIndex];
responseMessage = currentStep.message.text;
// Advance to next step
await conversations.updateOne(
{ _id: conversation._id },
{ $inc: { currentStepIndex: 1 } }
);
} else if (template.userInitiatedTree) {
// Main tree complete, check user-initiated responses
const matchedStep = findMatchingUserInitiatedStep(template.userInitiatedTree, webhook.Body);
responseMessage = matchedStep?.message.text || defaultResponse;
} else {
// No user-initiated tree, use default response
responseMessage = "Thanks for your message!";
}
// 6. Replace variables in message
const context = {
customer: await getCustomer(conversation.customer.id),
place: place,
conversation: conversation,
currentFork: visit.fork
};
const finalMessage = replaceVariables(responseMessage, context);
// 7. Track message history
await conversations.updateOne(
{ _id: conversation._id },
{
$push: {
receivedMessages: {
messageId: webhook.MessageSid,
body: webhook.Body,
receivedAt: new Date(),
from: webhook.From,
profileName: webhook.ProfileName
},
sentMessages: {
body: finalMessage,
sentAt: new Date(),
to: webhook.From,
status: 'sent'
}
},
$set: { lastActivityAt: new Date() }
}
);
// 8. Send TwiML reply
const twiml = new MessagingResponse();
twiml.message(finalMessage);
return twiml;When multiple user-initiated steps exist, the webhook:
- Sorts by priority (higher first)
- Checks keyword_match steps
- Checks regex_match steps
- Falls back to sequential or default
Example:
userInitiatedTree: [
{
stepId: "urgent_help",
trigger: "keyword_match",
keywords: ["urgent", "help", "ayuda"],
priority: 100,
message: "I'll connect you with support right away!"
},
{
stepId: "general_followup",
trigger: "sequential",
priority: 0,
message: "Still here if you need anything!"
}
]Admin and business owners can use these variables in WhatsApp reply messages:
| Variable | Description | Example |
|---|---|---|
{{place.name}} |
Place name | Restaurant La Casa |
{{place.shortId}} |
Unique place identifier | restaurantabc |
{{place.forks.instagram.url}} |
Instagram profile URL | https://instagram.com/restaurant |
{{place.forks.instagram.username}} |
Instagram username | @restaurant |
{{place.forks.google.writeReview}} |
Google review URL | https://g.page/r/... |
{{customer.firstName}} |
Customer first name | Jose |
{{customer.email}} |
Customer email | [email protected] |
{{customer.phone}} |
Customer phone | +16463345377 |
{{conversation.shortId}} |
Conversation ID | co_a1b2c3 |
{{currentFork}} |
Current fork context | instagram or google |
PasosModule WhatsApp Step Configuration:
{
inputType: 'whatsapp',
phoneNumber: '+51999999999',
prefilledMessage: 'Hola! Estoy en {{place.name}} ({{place.shortId}}) y me gustarΓa participar.',
buttonText: 'Enviar WhatsApp'
}Processed Message:
"Hola! Estoy en Restaurant La Casa (restaurantabc) y me gustarΓa participar."
forms field. The migration script exists at scripts/migrate-places-add-forms.js but needs to be run.
Designed Structure:
Each Place will define fork-specific WhatsApp configuration:
// Place document structure
{
forks: {
instagram: {
forms: {
whatsapp: {
formId: "default-whatsapp", // References form_templates.id
action: "Β‘SΓguenos en Instagram para ganar S./100 esta semana! {{place.forks.instagram.url}}"
}
}
},
google: {
forms: {
whatsapp: {
formId: "raffle-with-review", // Custom multi-step template
action: "DΓ©janos una reseΓ±a en Google y gana 10% de descuento! {{place.forks.google.writeReview}}"
}
}
}
}
}Instagram Fork Reply Template:
{{forkType.forms.whatsapp.action}}
Thanks for signing up to win S./100 this week at {{place.name}}!
We will message you here if you are this week's winner!
Rendered for Instagram:
Β‘SΓguenos en Instagram para ganar S./100 esta semana! https://instagram.com/restaurant
Thanks for signing up to win S./100 this week at Restaurant La Casa!
We will message you here if you are this week's winner!
Google Fork Reply Template:
{{forkType.forms.whatsapp.action}}
Thanks for signing up at {{place.name}}!
We appreciate your feedback!
Rendered for Google:
DΓ©janos una reseΓ±a en Google y gana 10% de descuento! https://g.page/r/...
Thanks for signing up at Restaurant La Casa!
We appreciate your feedback!
Advanced Example with Multi-Step Conversation:
// Template in form_templates (type: "whatsapp")
{
id: "favorite_dish",
type: "whatsapp",
conversationTree: [
{
stepId: "welcome",
message: {
text: "Thanks {{customer.firstName}} for signing up at {{place.name}}! What's your favorite dish?"
},
expectedResponse: { type: "text", required: true },
nextStep: { default: "confirm" }
},
{
stepId: "confirm",
message: {
text: "Great choice! We'll let the chef know. Good luck in this week's raffle!"
},
completeConversation: true
}
]
}β
Endpoint: POST /api/conversations/create (FULLY IMPLEMENTED)
Location: src/app/api/conversations/create/route.ts
Purpose: Create or retrieve active conversation when customer taps WhatsApp button
Features:
- Checks for existing active conversations (72-hour window)
- Returns existing conversation if found
- Creates new conversation with TTL expiration if none exists
- Supports preview mode for testing without database writes
Request:
{
type: 'whatsapp' | 'email' | 'sms' | 'voice'; // Required: Communication channel
customer: {
phone: string; // Required: E.164 format (for quick lookup)
id: string; // Required: Customer ObjectId as string (customers._id)
};
place: {
shortId: string; // Required: Place shortId (places.shortId)
id?: string; // Optional: Place ObjectId as string (places._id)
};
visit: {
id: string; // Required: Visit ObjectId as string (visits._id)
};
fork: 'instagram' | 'google'; // Required: Fork type (denormalized for filtering)
metadata?: {
source?: string;
deviceType?: string;
userAgent?: string;
};
}Response:
{
success: boolean;
conversation: {
shortId: string;
expiresAt: string; // ISO date
isNew: boolean; // true if newly created, false if existing
};
error?: string;
}π§ Endpoint: GET /api/conversations/[shortId] (NOT YET IMPLEMENTED)
Purpose: Retrieve conversation details (admin only)
Response:
{
success: boolean;
conversation: Conversation;
}π§ Endpoint: GET /api/conversations?placeShortId=abc&status=active (NOT YET IMPLEMENTED)
Purpose: List conversations with filters (admin only)
Query Parameters:
placeShortId- Filter by placecustomerId- Filter by customerstatus- Filter by status (active/expired)limit- Pagination limitoffset- Pagination offset
Created β Active (0-72 hours) β Expired (>72 hours)
Automatic Expiration:
- Conversations expire 72 hours after
createdAt - Expired conversations have
status: 'expired' - New messages from same phone create new conversation after expiration
Why 72 Hours?
- WhatsApp business messaging best practice
- Keeps conversations contextually relevant
- Prevents stale conversation threads
- Balances between continuity and freshness
Option 1: TTL Index (Recommended)
db.conversations.createIndex(
{ expiresAt: 1 },
{ expireAfterSeconds: 0 }
);Option 2: Status Update
// Cron job or scheduled function
await conversations.updateMany(
{ expiresAt: { $lt: new Date() }, status: 'active' },
{ $set: { status: 'expired' } }
);Add reference to conversation:
interface Visit {
// ... existing fields
conversationId?: ObjectId; // Reference to conversation
conversationShortId?: string; // Denormalized for quick lookup
}Track conversation history:
interface Customer {
// ... existing fields
conversations?: Array<{
conversationId: ObjectId;
shortId: string;
placeShortId: string;
createdAt: Date;
status: 'active' | 'expired';
}>;
}View Conversations:
- List all conversations for a place
- Filter by status, date range, customer
- Search by phone number or conversation ID
Conversation Details:
- Full message history (received & sent)
- Customer information
- Associated visit and place data
- Conversation timeline
Analytics:
- Total conversations per place
- Response rate
- Average conversation length
- Popular message times
- Phone Number Encryption - Consider encrypting phone numbers at rest
- Message Retention - Define retention policy (e.g., 90 days)
- GDPR Compliance - Allow customers to request conversation deletion
- Access Control - Only admins and business owners can view conversations
Prevent abuse:
- Max 10 messages per phone per hour
- Max 3 new conversations per phone per day
- Cooldown period after spam detection
- Customer scans QR code β creates visit
- Enters email/phone β creates customer
- Taps WhatsApp button β creates conversation
- Sends message β receives fork-specific reply
- Verify conversation document created correctly
- Customer who messaged 24 hours ago
- Sends new message
- Should use existing conversation
- Verify message added to
receivedMessagesarray
- Customer who messaged 80 hours ago
- Old conversation expired
- Sends new message
- Should create new conversation
- Verify new conversation document created
- Random phone sends message to Twilio number
- No conversation found
- Should send generic reply
- Optional: Create orphaned conversation for tracking
- Variable replacement engine
- Step router logic with conditional routing
- Condition evaluation (keyword_match, regex_match, equals)
- Phone number normalization
- Short ID generation
- Full conversation flow from start to end
- Timeout handling and reminder steps
- Conditional routing based on user responses
- Template variable injection and replacement
- Message history tracking
- Conversation expiration and renewal
- Test conversations via ngrok + real WhatsApp
- Verify each branch of conversation tree
- Test edge cases:
- Unexpected responses
- Timeouts (24+ hours)
- Multiple customers messaging simultaneously
- Expired conversations
- Unknown phone numbers
- Invalid message formats
- Create conversation via WhatsAppButton
- Send initial message and receive first step
- Test keyword matching (yes/no responses)
- Test conditional routing
- Verify variable replacement
- Check message history in database
- Test conversation after 72 hours (should create new)
- Test user-initiated responses after main tree
- Verify priority-based matching
- Test with multiple places for same customer
Initial Message β Ask to Follow β Ask to Share Story β Confirmation
Flow Details:
- Customer taps WhatsApp button
- Receives welcome message with instructions
- Asked to follow Instagram account
- If yes: Asked to share story
- If no: Encouraged to follow with link
- Upon completion: Confirmation message
- After main tree: User-initiated responses for follow-up questions
Initial Message β Ask for Rating (1-5) β If 5: Request Review β If <5: Gather Feedback
Flow Details:
- Welcome message thanking customer for visit
- Ask for rating 1-5
- If 5 stars: Send Google review link
- If <5 stars: Ask what could be improved
- Collect feedback and thank customer
- Update visit status based on completion
Greeting β Ask Date β Ask Time β Ask Party Size β Confirm β Send Calendar Invite
Flow Details:
- Welcome message
- Ask preferred date
- Ask preferred time
- Ask party size
- Confirm all details
- Send confirmation with calendar invite link
- Update internal reservation system
Welcome β Ask Product Quality β Ask Service Quality β Ask Recommendation Likelihood β Thank You
Flow Details:
- Thank customer for purchase
- Rate product quality (1-10)
- Rate service quality (1-10)
- Net Promoter Score question
- Optional: Open feedback text
- Thank you and discount code for next visit
Enable conversational flows:
{
flow: {
step1: {
trigger: 'initial',
message: 'Gracias! ΒΏPrefieres mesa o barra?',
options: ['mesa', 'barra']
},
step2: {
trigger: 'response',
message: 'Perfecto! ΒΏCuΓ‘ntas personas?',
validation: 'number'
}
}
}- Natural language understanding
- Automated FAQ responses
- Smart routing to human agents
- Message templates
- Media messages (images, PDFs)
- Interactive buttons and lists
- Payment integration
- Create
conversationscollection - Add required indexes (9 indexes including TTL)
- Update Visit schema with
conversationId(optional enhancement) - Update Customer schema with conversation tracking (optional enhancement)
- Set up TTL index for automatic cleanup
- Create TypeScript types for Conversation
- Implement
POST /api/conversations/createendpoint - Update Twilio webhook to use phone lookup
- Add conversation message tracking (received/sent)
- Implement conversation expiration logic
- Implement two-tree architecture
- Build variable replacement engine
- Add priority-based keyword/regex matching
- Update WhatsAppButton component to create conversation
- Handle conversation creation errors gracefully
- Add conversation template variables
- Add forms.whatsapp configuration to Place edit form
- Build visual conversation flow builder UI
- Unit tests for conversation creation logic
- Integration tests for webhook processing
- Test 72-hour expiration
- Test phone number lookup edge cases
- Load testing for concurrent conversations
- Create WHATSAPP_CONVERSATIONS.md
- Update API documentation
- Create admin user guide
- Add inline code comments
For places that already have WhatsApp integration:
- No Migration Needed - Old conversations won't have references
- New Conversations Only - New interactions create conversation documents
- Graceful Fallback - Webhook handles messages without conversations
- Deploy database schema (create collection + indexes)
- Deploy backend API endpoints
- Deploy updated webhook logic
- Deploy frontend changes
- Test with staging environment
- Monitor logs for errors
- Gradual rollout to production
- Conversation Creation Rate - Track new conversations per hour
- Message Processing Time - Webhook response time
- Failed Lookups - Messages without matching conversations
- Expiration Rate - How many conversations expire daily
- High Error Rate - >5% webhook errors
- Slow Response - Webhook taking >2 seconds
- Database Issues - Connection failures
- Spam Detection - Unusual message patterns
Issue: Customer not receiving reply
- Check conversation exists and is active
- Verify place has whatsappReplyMessage configured
- Check Twilio delivery status
- Verify phone number format
Issue: Duplicate conversations
- Check phone number normalization
- Verify TTL index is working
- Check for race conditions in creation
Issue: Wrong reply message
- Verify fork type in conversation document
- Check place configuration
- Verify template variable replacement
// Find conversation by phone
db.conversations.findOne({ 'customer.phone': "+1234567890" })
// Check active conversations
db.conversations.find({
status: "active",
expiresAt: { $gte: new Date() }
})
// View conversation history
db.conversations.aggregate([
{ $match: { shortId: "co_abc123" } },
{ $project: {
messageCount: {
$add: [
{ $size: "$receivedMessages" },
{ $size: "$sentMessages" }
]
},
lastMessage: { $arrayElemAt: ["$receivedMessages", -1] }
}}
])Beyond simple auto-replies, the system supports automated conversation flows using templates. These templates define multi-step conversations with conditional logic, similar to web forms but via WhatsApp messaging.
The form_templates collection now supports two types:
pasos-form: Traditional web-based forms (PasosModule)whatsapp: WhatsApp conversation templates (new)
The system stores all templates in a single collection with type-specific schemas:
// Pasos-form template (existing, now with type field)
{
_id: ObjectId("..."),
id: "default-pasos",
type: "pasos-form", // REQUIRED field
name: "Default (2-Step)",
description: "Standard form for Instagram places",
active: true,
steps: [
{
fieldId: "email",
inputType: "email",
title: "Tu correo electrΓ³nico",
cta: "Registra tu correo y participa:",
buttonText: "Participar",
required: true
}
],
createdAt: Date,
updatedAt: Date
}
// WhatsApp conversation template (simple 1-step)
{
_id: ObjectId("..."),
id: "default-whatsapp",
type: "whatsapp", // REQUIRED field
name: "Default WhatsApp",
description: "Simple auto-reply message",
active: true,
// Main conversation flow (bot-initiated)
conversationTree: [
{
stepId: "welcome",
stepNumber: 1,
trigger: "initial",
message: {
text: "Thanks for signing up! {{place.name}} will be in touch soon.",
attachments: []
},
expectedResponse: { type: "any", required: false },
nextStep: { default: "end", conditional: [] },
autoAdvanceDelay: null
},
{
stepId: "end",
stepNumber: 99,
trigger: "auto",
isTerminal: true,
message: null
}
],
// Optional: User-initiated responses (after main tree completes)
userInitiatedTree: [
{
stepId: "help_response",
trigger: "keyword_match",
keywords: ["help", "ayuda", "info"],
priority: 10,
message: {
text: "How can I help you? Reply with your question."
}
},
{
stepId: "sequential_followup",
trigger: "sequential",
priority: 0,
message: {
text: "Thanks for your message! We'll get back to you soon."
}
}
],
createdAt: Date,
updatedAt: Date
}
// Multi-step WhatsApp template example (favorite_dish)
{
_id: ObjectId("..."),
id: "favorite_dish",
type: "whatsapp",
name: "Favorite Dish Survey",
description: "4-step conversation to collect favorite dish",
active: true,
conversationTree: [
{
stepId: "welcome",
stepNumber: 1,
trigger: "initial",
message: {
text: "Β‘Hola {{customer.firstName}}! π\n\nGracias por registrarte en {{place.name}}.\n\nPara participar en el sorteo semanal de S./100, necesito que sigas estos pasos:",
attachments: []
},
expectedResponse: {
type: "any",
required: false,
keywords: [],
validation: null
},
nextStep: {
default: "ask_instagram_follow",
conditional: []
},
autoAdvanceDelay: 2000
},
{
stepId: "ask_instagram_follow",
stepNumber: 2,
trigger: "auto",
message: {
text: "Paso 1: SΓguenos en Instagram\n{{place.forks.instagram.url}}\n\nΒΏYa nos sigues? Responde SΓ o No"
},
expectedResponse: {
type: "keyword",
required: true,
keywords: ["si", "sΓ", "yes", "no"],
caseSensitive: false
},
nextStep: {
default: "thank_you",
conditional: [
{
if: { type: "keyword_match", keywords: ["si", "sΓ", "yes"] },
then: "ask_story_share"
},
{
if: { type: "keyword_match", keywords: ["no"] },
then: "encourage_follow"
}
]
},
autoAdvanceDelay: null,
maxWaitTime: 86400000, // 24 hours
onTimeout: "remind_instagram_follow"
},
{
stepId: "ask_story_share",
stepNumber: 4,
trigger: "auto",
message: {
text: "Β‘Perfecto! π\n\nPaso 2: Comparte esta historia en tu Instagram\n\nCuando lo hayas compartido, responde 'Listo'"
},
expectedResponse: {
type: "keyword",
required: true,
keywords: ["listo", "ready", "done", "ya"],
caseSensitive: false
},
nextStep: { default: "confirmation" }
},
{
stepId: "confirmation",
stepNumber: 5,
trigger: "auto",
message: {
text: "Β‘Excelente! β
\n\nYa estΓ‘s participando en el sorteo semanal de S./100.\n\nΒ‘Mucha suerte! π"
},
expectedResponse: { type: "any", required: false },
nextStep: { default: "end" },
completeConversation: true,
updateVisitStatus: "completed"
},
{
stepId: "end",
stepNumber: 99,
trigger: "auto",
isTerminal: true,
message: null
}
],
// Template variables available
variables: {
customer: ["firstName", "lastName", "email", "phone"],
place: ["name", "shortId", "forks.instagram.url", "forks.instagram.username"],
raffle: ["nextDrawDate", "nextDrawTime", "prizeAmount"]
},
createdAt: Date,
updatedAt: Date
}interface WhatsAppTemplate {
_id?: ObjectId;
type: "whatsapp";
id: string;
name: string;
description?: string;
// Main bot-initiated conversation flow (sequential steps)
conversationTree: ConversationStep[];
// User-initiated responses (conditional/keyword-based)
userInitiatedTree?: UserInitiatedStep[];
active?: boolean;
createdAt?: Date;
updatedAt?: Date;
}
interface ConversationStep {
stepId: string;
stepNumber: number;
trigger: "initial" | "auto" | "conditional" | "timeout";
message: {
text: string; // Supports {{variable}} replacement
attachments?: string[];
};
expectedResponse: {
type: "any" | "keyword" | "number" | "text" | "confirmation";
required: boolean;
keywords?: string[];
validation?: string;
caseSensitive?: boolean;
};
nextStep: {
default: string;
conditional?: Array<{
if: {
type: "keyword_match" | "regex_match" | "equals";
keywords?: string[];
pattern?: string;
value?: any;
};
then: string;
}>;
};
autoAdvanceDelay?: number;
maxWaitTime?: number;
onTimeout?: string;
completeConversation?: boolean;
updateVisitStatus?: string;
isTerminal?: boolean;
}
interface UserInitiatedStep {
stepId: string;
trigger: "sequential" | "keyword_match" | "regex_match" | "sentiment";
// Matching conditions
keywords?: string[];
pattern?: string;
caseSensitive?: boolean;
priority?: number; // Higher priority checked first
message: {
text: string;
attachments?: string[];
};
nextStep?: string;
completeConversation?: boolean;
isTerminal?: boolean;
}{
type: "whatsapp",
templateId: "instagram_raffle_flow",
name: "Instagram Weekly Raffle Flow",
description: "Automated conversation for Instagram raffle participation",
config: {
fork: "instagram",
language: "es",
defaultReplyDelay: 1000,
maxInactivityHours: 24,
},
conversationTree: [
{
stepId: "welcome",
stepNumber: 1,
trigger: "initial",
message: {
text: "Β‘Hola {{customer.firstName}}! π\n\nGracias por registrarte en {{place.name}}.\n\nPara participar en el sorteo semanal de S./100, necesito que sigas estos pasos:",
},
expectedResponse: { type: "any", required: false },
nextStep: { default: "ask_instagram_follow" },
autoAdvanceDelay: 2000,
},
{
stepId: "ask_instagram_follow",
stepNumber: 2,
trigger: "auto",
message: {
text: "Paso 1: SΓguenos en Instagram\n{{place.forks.instagram.url}}\n\nΒΏYa nos sigues? Responde SΓ o No",
},
expectedResponse: {
type: "keyword",
required: true,
keywords: ["si", "sΓ", "yes", "no"],
caseSensitive: false,
},
nextStep: {
default: "thank_you",
conditional: [
{
if: { type: "keyword_match", keywords: ["si", "sΓ", "yes"] },
then: "ask_story_share"
},
{
if: { type: "keyword_match", keywords: ["no"] },
then: "encourage_follow"
}
]
},
maxWaitTime: 86400000, // 24 hours
onTimeout: "remind_instagram_follow",
},
{
stepId: "ask_story_share",
stepNumber: 3,
trigger: "auto",
message: {
text: "Β‘Perfecto! π\n\nPaso 2: Comparte esta historia en tu Instagram\n\nCuando lo hayas compartido, responde 'Listo'",
},
expectedResponse: {
type: "keyword",
required: true,
keywords: ["listo", "ready", "done", "ya"],
},
nextStep: { default: "confirmation" },
},
{
stepId: "confirmation",
stepNumber: 4,
trigger: "auto",
message: {
text: "Β‘Excelente! β
\n\nYa estΓ‘s participando en el sorteo semanal de S./100.\n\nΒ‘Mucha suerte! π",
},
expectedResponse: { type: "any", required: false },
nextStep: { default: "end" },
completeConversation: true,
updateVisitStatus: "completed",
},
{
stepId: "end",
stepNumber: 99,
trigger: "auto",
isTerminal: true,
message: null,
}
],
variables: {
customer: ["firstName", "lastName", "email", "phone"],
place: ["name", "shortId", "forks.instagram.url", "forks.instagram.username"],
raffle: ["nextDrawDate", "nextDrawTime", "prizeAmount"],
},
}When using templates, the Conversation document includes flow state:
interface Conversation {
// ... existing fields
flow?: {
templateId: string; // Which template is being used
currentStepId: string; // Current step in conversation tree
stepHistory: Array<{
stepId: string;
enteredAt: Date;
exitedAt?: Date;
userResponse?: string;
}>;
variables: Record<string, any>; // Computed variables
isComplete: boolean;
completedAt?: Date;
};
}Templates support dynamic variable replacement:
| Variable Category | Examples |
|---|---|
| Customer | {{customer.firstName}}, {{customer.email}} |
| Place | {{place.name}}, {{place.shortId}} |
| Place Forks | {{place.forks.instagram.url}}, {{place.forks.google.writeReview}} |
| Conversation | {{conversation.shortId}}, {{conversation.createdAt}} |
| Custom | {{raffle.nextDrawDate}}, {{raffle.prizeAmount}} |
Message Processing with Templates:
async function handleIncomingMessage(webhook) {
const conversation = await findActiveConversation(phone);
if (conversation?.flow) {
// Using conversation template
const template = await loadTemplate(conversation.flow.templateId);
const currentStep = findStep(template, conversation.flow.currentStepId);
// Determine next step based on user response
const nextStepId = determineNextStep(currentStep, webhook.Body, template);
const nextStep = findStep(template, nextStepId);
// Replace variables in message
const message = replaceVariables(nextStep.message.text, {
conversation,
customer: await getCustomer(conversation.customer.id),
place: await getPlace(conversation.place.shortId),
});
// Update conversation flow state
await updateConversationFlow(conversation._id, {
currentStepId: nextStepId,
stepHistory: [...conversation.flow.stepHistory, {
stepId: nextStepId,
enteredAt: new Date(),
userResponse: webhook.Body,
}],
isComplete: nextStep.completeConversation || false,
});
return sendTwiMLMessage(message);
} else {
// Fallback to simple auto-reply
return sendSimpleReply(place.forks[fork].whatsappReplyMessage);
}
}ACTUAL IMPLEMENTATION:
Places use unified forms structure across all channels:
interface Place {
// ... existing fields
forks: {
instagram?: {
// ... existing fields (url, username, etc.)
// β
NEW: Unified forms structure
forms?: {
whatsapp?: {
formId: string; // References form_templates.id
action: string; // Customizable CTA with {{variables}}
};
email?: {
formId: string;
action: string;
};
pasos?: {
formId: string;
action: string; // Used as form CTA button text
};
};
// π§ DEPRECATED (still present, to be migrated):
whatsappReplyMessage?: string; // Old simple auto-reply
};
google?: {
// Same structure
};
};
}Key Design:
- β
Fork-agnostic templates use
{{forkType.forms.whatsapp.action}} - β Place-level customization via action text
- π§ Migration script ready:
scripts/migrate-places-add-forms.js
β Template Management (IMPLEMENTED):
- β
List all templates at
/admin/form-types - β Filter by type (Pasos Web, WhatsApp, Email)
- β Create new templates with type selector
- β Edit existing templates
- β Delete templates (with confirmation)
- β Duplicate templates
- β Preview templates (basic)
π§ Template Editor UI (BASIC ONLY):
- β Basic form fields (name, description, active)
- π§ Visual conversation flow builder (NOT YET BUILT)
- π§ Step-by-step conversation builder (NOT YET BUILT)
- π§ Conditional routing configuration (NOT YET BUILT)
- π§ Variable insertion helper (NOT YET BUILT)
- π§ Visual flow diagram (NOT YET BUILT)
- π§ Template testing interface (NOT YET BUILT)
π§ Place Assignment (NOT YET BUILT):
- π§ Add forms.whatsapp configuration to Place edit form
- π§ Template selector dropdown
- π§ Action text editor with variable helper
β Template CRUD (IMPLEMENTED):
β
GET /api/admin/form-templates - List all templates (all types)
β
GET /api/admin/form-templates?id=[id] - Get single template
β
POST /api/admin/form-templates - Create template
β
PUT /api/admin/form-templates - Update template
β
DELETE /api/admin/form-templates?id=[id] - Delete template
π§ POST /api/admin/form-templates/test - Test template (NOT YET BUILT)
Note: The API path is /api/admin/form-templates (not /api/admin/form-types).
- β
Added
typefield to all form_templates - β Created conversations collection with 9 indexes
- β Created default WhatsApp templates (default-whatsapp, favorite_dish)
- β Extended TypeScript types for multi-channel support
- β Designed Place.forks.forms structure
- β Built template list UI with type filters
- β Built type selector for creating templates
- π§ Build visual conversation flow builder
- π§ Add forms configuration to Place edit form
- π§ Run Place migration script
- β Implemented conversation creation API
- β Updated webhook to support phone lookup and template loading
- β Implemented variable replacement engine
- β Built step router with two-tree architecture
- β Added message history tracking
- β Implemented priority-based keyword/regex matching
- Media support (images, videos, PDFs)
- Interactive buttons and lists
- AI-powered responses
- Analytics and A/B testing
Instagram Raffle:
Welcome β Ask to Follow β Ask to Share Story β Confirmation
Google Review Request:
Greeting β Ask Rating (1-5) β If 5: Request Review β If <5: Gather Feedback
Restaurant Reservation:
Greeting β Ask Date β Ask Time β Ask Party Size β Confirm β Calendar Invite
The WhatsApp Conversations feature provides a robust, scalable foundation for automated customer messaging. By using phone number lookup instead of code parsing, we deliver a seamless user experience while maintaining comprehensive conversation tracking and history.
Key Benefits:
- β No codes needed - natural messaging flow
- β Full conversation history and audit trail
- β Contextual, personalized replies based on fork type
- β 72-hour conversation windows for relevance
- β Scalable architecture for future enhancements
- β Multi-channel support: Unified system for WhatsApp, Email, Web Forms
- β Reusable templates: Create once, use across multiple places
- β Dynamic variables: Personalization with {{customer.firstName}}, etc.
- β Fork-agnostic: Templates work across Instagram and Google forks
β Completed (January 2025):
- β Created conversations collection with 9 indexes (including TTL)
- β Extended form_templates to support multi-channel templates (pasos-form, whatsapp, email)
- β Created default WhatsApp templates (default-whatsapp, favorite_dish)
- β Built frontend UI for template management (list, filter, create, edit, delete)
- β Designed Place.forks.forms architecture (TypeScript types complete)
- β Implemented template CRUD APIs (/api/admin/form-templates)
- β Full TypeScript type system with discriminated unions
- β Implemented conversation creation API (/api/conversations/create)
- β Updated Twilio webhook with full conversation flow support
- β Implemented two-tree architecture (bot-initiated + user-initiated)
- β Implemented variable replacement engine with rich context
- β Built step router with priority-based matching
- β Added complete message history tracking
- β Phone lookup and conversation matching by place
- β 72-hour TTL expiration with automatic renewal
π§ In Progress / Next Steps:
- π§ Build visual conversation flow builder UI (templates must be created via DB currently)
- π§ Add forms.whatsapp configuration to Place edit form
- π§ Run Place migration script (migrate-places-add-forms.js) to update production data
- π§ Create template testing/simulation interface
- π§ Add variable autocomplete helper for template editors
- π§ Build conversation analytics dashboard
- π§ Monitor and iterate based on usage patterns
Conversation & Template Setup:
create-conversation-indexes-correct-db.js- Creates 9 indexes on conversations collection (EXECUTED)create-whatsapp-templates-correct-db.js- Creates default WhatsApp templates (EXECUTED)add-type-to-existing-form-types.js- Adds type field to existing templates (EXECUTED)fix-form-type-types.js- Fixes specific template types (EXECUTED)migrate-whatsapp-two-tree-architecture.js- Migrates to two-tree architecture (PREPARED)migrate-places-add-forms.js- Migrates Place schema to new forms structure (NOT YET RUN)run-multi-channel-migrations.js- Comprehensive migration runner
Core Type Files:
formTemplates.ts- Form template types, conversation step types, user-initiated step typesFormTemplateType,ForkType,ChannelTypeFormTemplateDocument,WhatsAppTemplate,PasosFormTemplateConversationStep,UserInitiatedStepChannelFormConfig,ForkForms- Type guards:
isWhatsAppTemplate(),isPasosFormTemplate()
conversation.ts- Conversation document types, message typesConversation,ConversationStatus,ConversationTypeReceivedMessage,SentMessageCreateConversationRequest,CreateConversationResponseVariableReplacementContext
database.ts- Place schema typesPlaceUpdateFields,GoogleForkData,InstagramForkData
twilio.ts- Twilio webhook typesTwilioWhatsAppWebhook
Admin UI:
page.tsx- Form Templates list page with type filterscomponents/FormTypeList.tsx- Template list with badges and action buttonscomponents/FormTypeFormContainer.tsx- Type selector UI (card-based)components/FormTypeForm.tsx- Template editor (basic fields only)
Core Endpoints:
admin/form-templates/route.ts- Template CRUD API (GET, POST, PUT, DELETE)conversations/create/route.ts- Conversation creation endpointtwilio/whatsapp/route.ts- Twilio webhook with two-tree architecture
Utilities:
lib/utils/variableReplacement.ts- Variable replacement enginelib/utils/twilioHelpers.ts- Twilio helper functionslib/utils/customerHelpers.ts- Customer utilities (shortId generation)lib/settings/serverUtils.ts- Server-side settings utilities
Customer-Facing:
place/InputTypes/WhatsAppButton.tsx- WhatsApp button in PasosModuleplace/PasosModule.tsx- Multi-step form component
forkast_admin database:
form_templates- All templates (pasos-form, whatsapp, email)- Currently: 10 templates (6 pasos-form, 3 whatsapp, 1 email)
- Indexes: _id, id (with unique constraint)
forkast database:
conversations- WhatsApp conversation state- 9 indexes including TTL for auto-expiration
- Currently: Ready for production use
customers- Customer informationvisits- Visit trackingplaces- Place/business information
Related Documentation:
- TypeScript types in
/src/lib/types/formTemplates.ts - Migration scripts in
/scriptsdirectory - Frontend components in
/src/app/admin/form-types/ - Backend APIs in
/src/app/api/
Last Updated: October 2025