April 27, 2026 • 6 min read
AI & Automation Specialist
I design AI-powered communication systems. My work focuses on voice agents, WhatsApp chatbots, AI assistants, and workflow automation built primarily on Twilio, n8n, and modern LLMs like OpenAI and Claude. Over the past 7 years, I've shipped 30+ automation projects handling 250k+ monthly interactions.
If you enjoy the content that I make, you can subscribe and receive insightful information through email. No spam is going to be sent, just updates about interesting posts or specialized content that I talk about.
Most businesses lose leads not because the product is wrong, but because nobody followed up in time. The contact form gets submitted, someone adds a product to the cart, a call goes unreturned, and by the time someone on the team gets to it, the window is closed. I built a system that handles exactly this, and it runs every 5 minutes without anyone touching it.
The system detects new leads from three sources (contact forms, abandoned carts, missed contact requests), classifies them, generates a contextual message using Claude, and reaches out via the appropriate channel: WhatsApp, SMS, or a real-time voice call with an AI agent. Everything is orchestrated in N8N. The full workflow files, agent prompts, and configuration instructions are in the repo linked in the description.
The data layer sits in Airtable. Three lead types feed into a single table:
- **Website contact forms** that never got a reply
- **Abandoned carts** where someone added a product but never completed the purchase
- **Missed contact requests** that fell through the cracks
Every record has a `status` field. The main workflow polls for records with `status = new` every 5 minutes and works through them one by one.
The two workflow files in the repo are:
1. **Main workflow**: fetches leads, classifies them, generates messages, routes to the right channel, updates Airtable
2. **Post-call webhook workflow**: receives the transcript from ElevenLabs after a voice call ends, extracts relevant data, updates the lead record
This separation matters. The main flow doesn't wait for call outcomes, it fires the outbound call and moves on. The transcript arrives asynchronously via webhook, which is why it lives in its own flow.
One of the first steps after fetching leads from Airtable is normalization. Before the Claude node sees anything, we pass leads through a normalization step that enforces a fixed structure, regardless of what fields exist in the source table.
This is worth doing even if it feels like overhead. Airtable schemas change. Someone adds a field, renames one, or removes it. Without normalization, any of that breaks the agent prompt. With normalization, the agent always receives the same shape of data and can tell you if something is missing instead of silently producing a bad message.
The prompt generation node does more than fill a template. It branches based on two dimensions: lead origin (website, cart, missed call) and contact mode (text or call).
This is where a lot of similar systems cut corners and produce generic messages. A WhatsApp message has a character limit, needs to respect approved templates if you're on the Business API, and reads differently than a spoken sentence. An AI agent making a voice call needs to know its opening line before the lead picks up, not after. You can't have the same instructions handle both.
For text channels, the prompt tells Claude to generate a concise, direct message appropriate for the lead origin. For voice calls, the prompt generates an `opening` variable, which is what the ElevenLabs agent says the moment the lead picks up. That variable gets passed dynamically to the ElevenLabs API call so the agent doesn't open cold.
For voice calls, the flow makes an API call to ElevenLabs with a payload that includes:
- The agent ID (configured in ElevenLabs with the system prompt and conversation rules)
- The Twilio phone number registered inside ElevenLabs
- The destination number (the lead's phone)
- Dynamic variables: `opening`, lead ID, lead origin
The agent system prompt lives in the repo. Key design decision: if the lead signals disinterest, the agent wraps up and ends the call. No persistence, no pressure. The prompt explicitly handles this case because an AI agent that keeps pushing when someone says no is worse than no agent at all.
The post-call webhook fires from ElevenLabs once the conversation ends and the transcript is processed. It hits the N8N webhook URL (ngrok tunnel if you're running locally, public IP if deployed) and the secondary workflow takes it from there: extracts the conversation transcript, the agent's lines and the lead's lines separately, and writes everything back to Airtable alongside the updated status and contact timestamp.
Having the full transcript in Airtable is what makes this system useful beyond just ""did we contact them"". You can run sentiment analysis on what the lead said, identify objections that keep coming up, or flag calls where the agent did something unexpected.
For SMS and WhatsApp: grab a phone number from the Twilio console (US numbers run $1.15/month, varies by country), copy the Account SID and Auth Token from the dashboard, and configure them as credentials in N8N. The WhatsApp sender and SMS sender can be different numbers, both defined as variables at the top of the main workflow so you change them in one place.
For voice: import the Twilio number into ElevenLabs under Phone Numbers, assign your agent to it, and configure the post-call webhook URL there. That's the URL that triggers the secondary N8N workflow after each call ends.
After any contact attempt, the record gets updated immediately: status flips to `contacted`, contact date is set, last message sent is stored. This is what prevents the 5-minute polling loop from re-contacting the same lead on the next run. Simple but critical. If you forget this or the update fails silently, leads start getting duplicate messages or repeat calls, which is worse than no outreach at all.
I want to address this directly because it comes up. This entire system runs on N8N with no custom backend. The trade-off is real: you give up some flexibility and debugging depth compared to writing this in code, and you're dependent on the N8N workflow format for portability.
But for this class of problem, the low-code approach was the right call. The logic here is orchestration: fetch, classify, branch, call API, update state. There's no algorithmic complexity that requires custom code. Building the same system from scratch would've taken significantly longer with no meaningful quality advantage. The AI agent behavior comes from the prompt, not from the surrounding infrastructure.
Where this would break down is if you needed to handle high concurrency (N8N's execution model has limits), complex retry logic with exponential backoff, or deeply custom conversation state management. For a lead recovery system handling dozens to low hundreds of leads per day, this holds up fine.
The design principle worth keeping: normalize your data early, define variables in one place, and keep the two flows separate. Those three decisions are what make this maintainable when Airtable fields change or you swap ElevenLabs for a different voice provider.
Liked the post? Share the knowledge!
-Gonza
Ready to automate your customer conversations?
Contact me