How We Built a Multi-Industry WhatsApp AI Agent in Israel
At AI Buddy, we've deployed AI agents for 200+ Israeli businesses — real estate agencies, dental clinics, law firms, gyms, and beauty salons. This is the technical story of how we built a WhatsApp AI agent that works across all of them.
The Problem: WhatsApp Is Israel's Business Channel
In Israel, over 90% of business communication happens on WhatsApp. Not email. Not web chat. WhatsApp. This means:
- Leads come in at 11pm asking about services
- Customers expect responses in minutes, not hours
- A 3-hour response time loses 80%+ of conversion opportunities
- One person can't respond fast enough at scale
The solution isn't "hire someone to watch WhatsApp 24/7." It's an AI agent that handles the entire conversation flow autonomously.
Architecture Overview
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ WhatsApp API │────▶│ Agent Layer │────▶│ Integrations │
│ (Green API / │ │ (LLM + Tools) │ │ CRM / Calendar │
│ 360Dialog) │◀────│ │◀────│ / Booking │
└─────────────────┘ └──────────────────┘ └─────────────────┘
The core components:
- WhatsApp ingress — Webhook receives messages, normalizes them
- Context manager — Maintains conversation history per phone number
- LLM orchestrator — Decides what to do next
- Tool executor — Takes real-world actions (book, CRM update, etc.)
- Response generator — Crafts natural Hebrew responses
Step 1: WhatsApp Integration
We use Green API for WhatsApp Business API access (approved provider). Here's the webhook handler:
// webhook.js
const express = require('express');
const app = express();
app.post('/webhook', express.json(), async (req, res) => {
const { typeWebhook, messageData } = req.body;
if (typeWebhook !== 'incomingMessageReceived') {
return res.sendStatus(200);
}
const { idMessage, chatId, textMessageData } = messageData;
const phoneNumber = chatId.replace('@c.us', '');
const messageText = textMessageData?.textMessage;
if (!messageText) return res.sendStatus(200);
// Process asynchronously, respond immediately to webhook
res.sendStatus(200);
await processIncomingMessage({
phoneNumber,
message: messageText,
messageId: idMessage
});
});
Step 2: Context Manager
Each phone number gets its own conversation context. We store this in Redis with a 24-hour TTL:
// context.js
const Redis = require('ioredis');
const redis = new Redis(process.env.REDIS_URL);
const CONTEXT_TTL = 86400; // 24 hours
async function getContext(phoneNumber) {
const raw = await redis.get(`ctx:${phoneNumber}`);
if (!raw) return { messages: [], leadData: {}, stage: 'initial' };
return JSON.parse(raw);
}
async function updateContext(phoneNumber, context) {
await redis.setex(
`ctx:${phoneNumber}`,
CONTEXT_TTL,
JSON.stringify(context)
);
}
async function appendMessage(phoneNumber, role, content) {
const ctx = await getContext(phoneNumber);
ctx.messages.push({ role, content, timestamp: Date.now() });
// Keep last 20 messages to stay within token limits
if (ctx.messages.length > 20) {
ctx.messages = ctx.messages.slice(-20);
}
await updateContext(phoneNumber, ctx);
return ctx;
}
Step 3: The LLM Orchestrator
This is where the intelligence lives. We use function calling (tool use) to allow the LLM to take actions:
// orchestrator.js
const Anthropic = require('@anthropic-ai/sdk');
const client = new Anthropic();
const TOOLS = [
{
name: 'check_availability',
description: 'Check available appointment slots',
input_schema: {
type: 'object',
properties: {
date: { type: 'string', description: 'Date in YYYY-MM-DD format' },
service_type: { type: 'string' }
},
required: ['service_type']
}
},
{
name: 'book_appointment',
description: 'Book an appointment for the customer',
input_schema: {
type: 'object',
properties: {
name: { type: 'string' },
phone: { type: 'string' },
service: { type: 'string' },
datetime: { type: 'string' }
},
required: ['name', 'phone', 'service', 'datetime']
}
},
{
name: 'update_crm',
description: 'Update the CRM with lead information',
input_schema: {
type: 'object',
properties: {
phone: { type: 'string' },
name: { type: 'string' },
interest: { type: 'string' },
budget: { type: 'string' },
status: { type: 'string', enum: ['new', 'qualified', 'booked', 'lost'] }
},
required: ['phone', 'status']
}
},
{
name: 'escalate_to_human',
description: 'Flag this conversation for human review',
input_schema: {
type: 'object',
properties: {
reason: { type: 'string' },
priority: { type: 'string', enum: ['low', 'medium', 'high'] }
},
required: ['reason', 'priority']
}
}
];
async function runAgent(businessConfig, phoneNumber, conversationHistory) {
const systemPrompt = buildSystemPrompt(businessConfig);
const response = await client.messages.create({
model: 'claude-opus-4-5',
max_tokens: 1024,
system: systemPrompt,
messages: conversationHistory,
tools: TOOLS
});
return response;
}
function buildSystemPrompt(config) {
return `You are an AI agent for ${config.businessName}, a ${config.businessType} in Israel.
Your job is to:
1. Respond to incoming WhatsApp messages in natural Hebrew
2. Qualify leads by understanding their needs
3. Book appointments when appropriate
4. Update the CRM with relevant information
5. Escalate to a human when the situation requires it
Business information:
- Services: ${config.services.join(', ')}
- Operating hours: ${config.operatingHours}
- Pricing: ${config.pricingInfo}
- Tone: ${config.tone}
IMPORTANT:
- Always respond in Hebrew
- Be helpful and conversational, not robotic
- Don't make up information about services or prices
- If unsure, say you'll check and get back to them
- Never be pushy about booking
Current date/time in Israel: ${new Date().toLocaleString('he-IL', {timeZone: 'Asia/Jerusalem'})}`;
}
Step 4: Tool Execution
// tools.js
async function executeTool(toolName, toolInput, businessConfig) {
switch (toolName) {
case 'check_availability':
return await checkCalendarAvailability(toolInput, businessConfig.calendarId);
case 'book_appointment':
const booking = await createCalendarEvent(toolInput, businessConfig.calendarId);
await sendConfirmationSMS(toolInput.phone, booking);
return { success: true, bookingId: booking.id, datetime: booking.start };
case 'update_crm':
return await upsertCRMLead(toolInput, businessConfig.crmId);
case 'escalate_to_human':
await notifyHumanAgent(toolInput, businessConfig.alertPhone);
return { escalated: true };
default:
throw new Error(`Unknown tool: ${toolName}`);
}
}
Step 5: The Main Processing Loop
// process.js
async function processIncomingMessage({ phoneNumber, message, messageId }) {
// Get or create conversation context
const ctx = await appendMessage(phoneNumber, 'user', message);
// Get business config (could be from DB based on which WhatsApp number received the message)
const businessConfig = await getBusinessConfig(phoneNumber);
let response = await runAgent(businessConfig, phoneNumber, ctx.messages);
// Handle tool calls in a loop (agent may call multiple tools)
while (response.stop_reason === 'tool_use') {
const toolResults = [];
for (const block of response.content) {
if (block.type === 'tool_use') {
const result = await executeTool(block.name, block.input, businessConfig);
toolResults.push({
type: 'tool_result',
tool_use_id: block.id,
content: JSON.stringify(result)
});
}
}
// Add assistant response and tool results to history
ctx.messages.push({ role: 'assistant', content: response.content });
ctx.messages.push({ role: 'user', content: toolResults });
// Run agent again with tool results
response = await runAgent(businessConfig, phoneNumber, ctx.messages);
}
// Extract the final text response
const textResponse = response.content
.filter(b => b.type === 'text')
.map(b => b.text)
.join('');
if (textResponse) {
// Save to context
ctx.messages.push({ role: 'assistant', content: textResponse });
await updateContext(phoneNumber, ctx);
// Send WhatsApp response
await sendWhatsAppMessage(phoneNumber, textResponse);
}
}
Industry-Specific Customization
The businessConfig object is where vertical customization happens. Here's how it differs by industry:
// Real estate agency config
const realEstateConfig = {
businessType: 'real estate agency',
qualificationQuestions: ['budget', 'area', 'property_type', 'timeline'],
keyQualifiers: {
minBudget: 1500000, // 1.5M NIS minimum
seriousBuyer: ['mortgage_approved', 'cash_buyer']
},
escalationTriggers: ['over_5M', 'commercial_property', 'negative']
};
// Dental clinic config
const dentalConfig = {
businessType: 'dental clinic',
qualificationQuestions: ['treatment_type', 'urgency', 'insurance'],
services: ['checkup', 'cleaning', 'whitening', 'implants', 'orthodontics'],
urgencyTriggers: ['pain', 'כאב', 'broken', 'שבור', 'emergency', 'חירום']
};
Performance in Production
After 6 months running across 200+ businesses:
- Average response time: < 3 seconds (vs 3-12 hours manually)
- Lead response rate: 97% (vs 40-60% manually)
- Booking conversion: +35-60% depending on industry
- Human escalation rate: 8-15% of conversations
- False escalations (unnecessary human involvement): < 2%
What We Learned the Hard Way
1. Context window management is critical. An LLM that can see 100 past messages will cost 10x more than one seeing 20. We learned to summarize and compress old context.
2. Hebrew prompting needs extra care. LLMs are trained mostly on English. Hebrew system prompts need to be more explicit about tone and formality levels.
3. Tool call loops need safeguards. Occasionally an agent would loop in tool calls. We added a max-iterations limit (5) and a fallback to human escalation.
4. Test edge cases with real WhatsApp messages. Users send voice notes (which we now transcribe), images, PDFs of documents. The agent needs to handle all of these gracefully.
5. Shabbat and Jewish holidays require explicit handling. The agent needs to know not to book appointments on Shabbat/holidays and to handle Friday afternoon lead surges appropriately.
Try It
If you're building WhatsApp AI agents or want to see one in action for your Israeli business, visit aibuddy.co.il. We've open-sourced some of the utility functions from this stack — reach out via the site.
Happy to answer questions in the comments about any specific part of the architecture.
Top comments (0)